@camstack/addon-provider-unraid 0.2.147 → 0.2.148

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 (3) hide show
  1. package/dist/addon.js +334 -530
  2. package/dist/addon.mjs +334 -530
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5434,7 +5434,7 @@ var ZodIssueCode = {
5434
5434
  var ZodFirstPartyTypeKind;
5435
5435
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5436
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5437
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5438
5438
  /**
5439
5439
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5440
  * window to float samples (D455).
@@ -7988,6 +7988,52 @@ var RecordKindDeclarationSchema = object({
7988
7988
  }).strict();
7989
7989
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7990
7990
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7991
+ var READING_QUANTITIES = [
7992
+ "temperature",
7993
+ "pressure",
7994
+ "speed",
7995
+ "precipitation",
7996
+ "precipitation-rate",
7997
+ "length",
7998
+ "area",
7999
+ "illuminance",
8000
+ "irradiance",
8001
+ "power",
8002
+ "energy",
8003
+ "voltage",
8004
+ "current",
8005
+ "apparent-power",
8006
+ "humidity",
8007
+ "percentage",
8008
+ "concentration-volume",
8009
+ "concentration-mass",
8010
+ "concentration-ppb",
8011
+ "volume",
8012
+ "duration",
8013
+ "duration-ms",
8014
+ "information",
8015
+ "sound-level",
8016
+ "signal-level",
8017
+ "colour-temperature",
8018
+ "angle",
8019
+ "bitrate",
8020
+ "rate",
8021
+ "mass",
8022
+ "index",
8023
+ "ratio",
8024
+ "count",
8025
+ "none",
8026
+ "device"
8027
+ ];
8028
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8029
+ new Set(READING_QUANTITIES);
8030
+ function readingMeta(quantity, extras) {
8031
+ return {
8032
+ quantity,
8033
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8034
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8035
+ };
8036
+ }
7991
8037
  /** Shared Zod schemas used across streaming capabilities. */
7992
8038
  var CamProfileSchema = _enum([
7993
8039
  "high",
@@ -8015,8 +8061,8 @@ var CamStreamKindSchema = _enum([
8015
8061
  "derived"
8016
8062
  ]);
8017
8063
  var CamStreamResolutionSchema = object({
8018
- width: number().int().positive(),
8019
- height: number().int().positive()
8064
+ width: number().int().positive().meta(readingMeta("none")),
8065
+ height: number().int().positive().meta(readingMeta("none"))
8020
8066
  });
8021
8067
  var CameraStreamSchema = object({
8022
8068
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8663,14 +8709,6 @@ Object.freeze({
8663
8709
  {
8664
8710
  "path": "aqi",
8665
8711
  "valueType": "number"
8666
- },
8667
- {
8668
- "path": "unit",
8669
- "valueType": "string"
8670
- },
8671
- {
8672
- "path": "precision",
8673
- "valueType": "number"
8674
8712
  }
8675
8713
  ],
8676
8714
  "alarm-panel": [
@@ -8706,20 +8744,10 @@ Object.freeze({
8706
8744
  "valueType": "boolean"
8707
8745
  }
8708
8746
  ],
8709
- "ambient-light-sensor": [
8710
- {
8711
- "path": "lux",
8712
- "valueType": "number"
8713
- },
8714
- {
8715
- "path": "unit",
8716
- "valueType": "string"
8717
- },
8718
- {
8719
- "path": "precision",
8720
- "valueType": "number"
8721
- }
8722
- ],
8747
+ "ambient-light-sensor": [{
8748
+ "path": "lux",
8749
+ "valueType": "number"
8750
+ }],
8723
8751
  "audio-metrics": [
8724
8752
  {
8725
8753
  "path": "ts",
@@ -8799,6 +8827,10 @@ Object.freeze({
8799
8827
  "path": "lastUpdated",
8800
8828
  "valueType": "number"
8801
8829
  },
8830
+ {
8831
+ "path": "volts",
8832
+ "valueType": "number"
8833
+ },
8802
8834
  {
8803
8835
  "path": "lastContactAt",
8804
8836
  "valueType": "number"
@@ -9041,10 +9073,6 @@ Object.freeze({
9041
9073
  "datetime"
9042
9074
  ]
9043
9075
  },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
9076
  {
9049
9077
  "path": "min",
9050
9078
  "valueType": "number"
@@ -9056,19 +9084,6 @@ Object.freeze({
9056
9084
  {
9057
9085
  "path": "step",
9058
9086
  "valueType": "number"
9059
- },
9060
- {
9061
- "path": "precision",
9062
- "valueType": "number"
9063
- },
9064
- {
9065
- "path": "format",
9066
- "valueType": "enum",
9067
- "enumValues": [
9068
- "date",
9069
- "time",
9070
- "datetime"
9071
- ]
9072
9087
  }
9073
9088
  ],
9074
9089
  "cover": [
@@ -9309,13 +9324,8 @@ Object.freeze({
9309
9324
  "path": "value",
9310
9325
  "valueType": "string"
9311
9326
  }, {
9312
- "path": "format",
9313
- "valueType": "enum",
9314
- "enumValues": [
9315
- "date",
9316
- "time",
9317
- "datetime"
9318
- ]
9327
+ "path": "timestamp",
9328
+ "valueType": "boolean"
9319
9329
  }],
9320
9330
  "event-emitter": [
9321
9331
  {
@@ -9422,20 +9432,10 @@ Object.freeze({
9422
9432
  "valueType": "number"
9423
9433
  }
9424
9434
  ],
9425
- "humidity-sensor": [
9426
- {
9427
- "path": "percent",
9428
- "valueType": "number"
9429
- },
9430
- {
9431
- "path": "unit",
9432
- "valueType": "string"
9433
- },
9434
- {
9435
- "path": "precision",
9436
- "valueType": "number"
9437
- }
9438
- ],
9435
+ "humidity-sensor": [{
9436
+ "path": "percent",
9437
+ "valueType": "number"
9438
+ }],
9439
9439
  "image": [{
9440
9440
  "path": "url",
9441
9441
  "valueType": "string"
@@ -9774,20 +9774,10 @@ Object.freeze({
9774
9774
  "valueType": "boolean"
9775
9775
  }
9776
9776
  ],
9777
- "numeric-sensor": [
9778
- {
9779
- "path": "value",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9777
+ "numeric-sensor": [{
9778
+ "path": "value",
9779
+ "valueType": "number"
9780
+ }],
9791
9781
  "pet-feeder": [
9792
9782
  {
9793
9783
  "path": "foodLevel",
@@ -9879,14 +9869,6 @@ Object.freeze({
9879
9869
  {
9880
9870
  "path": "powerFactor",
9881
9871
  "valueType": "number"
9882
- },
9883
- {
9884
- "path": "unit",
9885
- "valueType": "string"
9886
- },
9887
- {
9888
- "path": "precision",
9889
- "valueType": "number"
9890
9872
  }
9891
9873
  ],
9892
9874
  "presence": [
@@ -9915,20 +9897,10 @@ Object.freeze({
9915
9897
  "valueType": "number"
9916
9898
  }
9917
9899
  ],
9918
- "pressure-sensor": [
9919
- {
9920
- "path": "hpa",
9921
- "valueType": "number"
9922
- },
9923
- {
9924
- "path": "unit",
9925
- "valueType": "string"
9926
- },
9927
- {
9928
- "path": "precision",
9929
- "valueType": "number"
9930
- }
9931
- ],
9900
+ "pressure-sensor": [{
9901
+ "path": "hpa",
9902
+ "valueType": "number"
9903
+ }],
9932
9904
  "privacy-mask": [{
9933
9905
  "path": "enabled",
9934
9906
  "valueType": "boolean"
@@ -10239,20 +10211,10 @@ Object.freeze({
10239
10211
  "path": "tampered",
10240
10212
  "valueType": "boolean"
10241
10213
  }],
10242
- "temperature-sensor": [
10243
- {
10244
- "path": "celsius",
10245
- "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
- }
10255
- ],
10214
+ "temperature-sensor": [{
10215
+ "path": "celsius",
10216
+ "valueType": "number"
10217
+ }],
10256
10218
  "update": [
10257
10219
  {
10258
10220
  "path": "currentVersion",
@@ -10363,10 +10325,6 @@ Object.freeze({
10363
10325
  "path": "temperature",
10364
10326
  "valueType": "number"
10365
10327
  },
10366
- {
10367
- "path": "temperatureUnit",
10368
- "valueType": "string"
10369
- },
10370
10328
  {
10371
10329
  "path": "humidity",
10372
10330
  "valueType": "number"
@@ -10375,18 +10333,10 @@ Object.freeze({
10375
10333
  "path": "pressure",
10376
10334
  "valueType": "number"
10377
10335
  },
10378
- {
10379
- "path": "pressureUnit",
10380
- "valueType": "string"
10381
- },
10382
10336
  {
10383
10337
  "path": "windSpeed",
10384
10338
  "valueType": "number"
10385
10339
  },
10386
- {
10387
- "path": "windSpeedUnit",
10388
- "valueType": "string"
10389
- },
10390
10340
  {
10391
10341
  "path": "windBearing",
10392
10342
  "valueType": "number"
@@ -17864,7 +17814,8 @@ var CompositionProblemSchema = object({
17864
17814
  "item-field-reserved",
17865
17815
  "target-missing",
17866
17816
  "target-unreadable",
17867
- "not-replaceable"
17817
+ "not-replaceable",
17818
+ "native-capability-replace"
17868
17819
  ]),
17869
17820
  path: string(),
17870
17821
  message: string()
@@ -17922,9 +17873,11 @@ var CompositionFieldPlanSchema = object({
17922
17873
  reads: array(CompositionFieldReadSchema)
17923
17874
  });
17924
17875
  /**
17925
- * What a feature does to its target (D663): `new` builds a capability on a new
17926
- * device; on an existing device it `add`s a capability the device lacks, or
17927
- * `replace`s named fields of one it has natively.
17876
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17877
+ * new device; on an existing device it `add`s a capability the device lacks.
17878
+ * Replacing a capability the device already serves is refused
17879
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17880
+ * wire for a claim the hub already holds.
17928
17881
  */
17929
17882
  var CompositionFeatureModeSchema = _enum([
17930
17883
  "new",
@@ -19923,12 +19876,12 @@ function buildDeviceAdminUrl(parts) {
19923
19876
  * providers detect renames by matching `uniqueId` and patch `id`
19924
19877
  * in-place so dispatch keeps working.
19925
19878
  *
19926
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19927
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19928
- * state push and are the single source of truth. The `device_class` tag is
19929
- * an HA-specific input that is consumed at the discovery boundary
19930
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19931
- * stored after that point.
19879
+ * **Display unit and precision are NOT stored here.** They are a read-time
19880
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
19881
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
19882
+ * neither is repeated on each sample. The `device_class` tag is an
19883
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
19884
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19932
19885
  *
19933
19886
  * The `raw` escape hatch carries provider-specific attributes that don't
19934
19887
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19942,6 +19895,14 @@ var SourceInfoSchema = object({
19942
19895
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19943
19896
  * Used to detect rename when `id` changes. */
19944
19897
  uniqueId: string().optional(),
19898
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
19899
+ quantity: ReadingQuantitySchema.optional(),
19900
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
19901
+ dateTimeShape: _enum([
19902
+ "date",
19903
+ "time",
19904
+ "datetime"
19905
+ ]).optional(),
19945
19906
  /** Provider-specific extras that don't fit the structured slots.
19946
19907
  * Whitelist what goes here per provider — do NOT dump entire HA
19947
19908
  * attribute blobs (would explode the meta JSON column). */
@@ -41046,7 +41007,8 @@ method(object({
41046
41007
  "enum"
41047
41008
  ]),
41048
41009
  enumValues: array(string()).optional(),
41049
- item: boolean().optional()
41010
+ item: boolean().optional(),
41011
+ quantity: ReadingQuantitySchema.optional()
41050
41012
  })).readonly(),
41051
41013
  itemArray: object({
41052
41014
  path: string(),
@@ -44890,8 +44852,8 @@ var MaskPolygonShapeSchema = object({
44890
44852
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44891
44853
  var MaskGridShapeSchema = object({
44892
44854
  kind: literal("grid"),
44893
- gridWidth: number(),
44894
- gridHeight: number(),
44855
+ gridWidth: number().meta(readingMeta("none")),
44856
+ gridHeight: number().meta(readingMeta("none")),
44895
44857
  cells: array(boolean())
44896
44858
  });
44897
44859
  discriminatedUnion("kind", [
@@ -56371,27 +56333,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56371
56333
  */
56372
56334
  var AirQualitySensorStatusSchema = object({
56373
56335
  /** Carbon dioxide concentration in ppm. */
56374
- co2Ppm: number().min(0).optional(),
56336
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56375
56337
  /** Total volatile organic compounds in ppb. */
56376
- vocPpb: number().min(0).optional(),
56338
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56377
56339
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56378
- pm25: number().min(0).optional(),
56340
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56379
56341
  /** Particulate matter ≤ 10 μm in µg/m³. */
56380
- pm10: number().min(0).optional(),
56342
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56381
56343
  /** Composite AQI value (typically 0..500). */
56382
- aqi: number().optional(),
56344
+ aqi: number().optional().meta(readingMeta("index")),
56383
56345
  /** Ms epoch when the slice was last updated. */
56384
- lastFetchedAt: number(),
56385
- /** Live display unit of the single metric this slice carries (e.g. HA
56386
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56387
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56388
- * per slice is unambiguous. */
56389
- unit: string().optional(),
56390
- /** Suggested decimal places for numeric display.
56391
- * Populated live from the upstream source when provided (e.g. HA
56392
- * `attributes.suggested_display_precision`). Falls back to
56393
- * auto-formatting when absent. */
56394
- precision: number().int().min(0).max(10).optional()
56346
+ lastFetchedAt: number()
56395
56347
  });
56396
56348
  var airQualitySensorCapability = {
56397
56349
  name: "air-quality-sensor",
@@ -56428,12 +56380,12 @@ var airQualitySensorCapability = {
56428
56380
  },
56429
56381
  pm25: {
56430
56382
  kind: "gauge",
56431
- unit: "ug/m3",
56383
+ unit: "µg/m³",
56432
56384
  deadband: 1
56433
56385
  },
56434
56386
  pm10: {
56435
56387
  kind: "gauge",
56436
- unit: "ug/m3",
56388
+ unit: "µg/m³",
56437
56389
  deadband: 1
56438
56390
  },
56439
56391
  aqi: {
@@ -56451,22 +56403,13 @@ var airQualitySensorCapability = {
56451
56403
  * entries with `device_class: illuminance`.
56452
56404
  */
56453
56405
  var AmbientLightSensorStatusSchema = object({
56454
- /** Current illuminance in lux (lx). */
56455
- lux: number().min(0).meta({
56456
- unit: "lx",
56406
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56407
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56408
+ precision: 0,
56457
56409
  label: "Illuminance"
56458
- }),
56410
+ })),
56459
56411
  /** Ms epoch when the slice was last updated. */
56460
- lastFetchedAt: number(),
56461
- /** Live display unit from the upstream source (e.g. HA
56462
- * `attributes.unit_of_measurement`). The UI prefers this over the
56463
- * role's canonical unit. Absent → fall back to the canonical unit. */
56464
- unit: string().optional(),
56465
- /** Suggested decimal places for numeric display.
56466
- * Populated live from the upstream source when provided (e.g. HA
56467
- * `attributes.suggested_display_precision`). Falls back to
56468
- * auto-formatting when absent. */
56469
- precision: number().int().min(0).max(10).optional()
56412
+ lastFetchedAt: number()
56470
56413
  });
56471
56414
  var ambientLightSensorCapability = {
56472
56415
  name: "ambient-light-sensor",
@@ -56498,12 +56441,7 @@ var ambientLightSensorCapability = {
56498
56441
  } } },
56499
56442
  /** Clock fields: written, but excluded from the compare that decides
56500
56443
  * whether persisting is worth a SQLite commit. */
56501
- volatileStateFields: ["lastFetchedAt"],
56502
- customize: {
56503
- version: 1,
56504
- advanced: "derive",
56505
- targets: []
56506
- }
56444
+ volatileStateFields: ["lastFetchedAt"]
56507
56445
  };
56508
56446
  /**
56509
56447
  * Per-class audio metrics aggregated over a sliding window.
@@ -56730,10 +56668,7 @@ var BatteryStatusSchema = object({
56730
56668
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56731
56669
  * declines to band a non-finite reading, which is the correct shape.
56732
56670
  */
56733
- percentage: number().min(0).max(100).nullable().meta({
56734
- unit: "%",
56735
- label: "Charge"
56736
- }),
56671
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56737
56672
  /**
56738
56673
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56739
56674
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56760,7 +56695,14 @@ var BatteryStatusSchema = object({
56760
56695
  */
56761
56696
  sleeping: boolean().meta({ label: "Sleeping" }),
56762
56697
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56763
- lastUpdated: number(),
56698
+ lastUpdated: number().meta(readingMeta("none")),
56699
+ /**
56700
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56701
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56702
+ * that only report charge. A second reading on this cap, not a second
56703
+ * `numeric-sensor` on the same device.
56704
+ */
56705
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56764
56706
  /**
56765
56707
  * Ms epoch of the last time the device PROVED it was reachable — a
56766
56708
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56812,7 +56754,6 @@ var BATTERY_DEVICE_TYPES = [
56812
56754
  /** Generic Customize target for the percentage reading (D936). */
56813
56755
  var batteryCustomize = {
56814
56756
  version: 1,
56815
- advanced: "derive",
56816
56757
  targets: [{
56817
56758
  id: "percentage",
56818
56759
  cardinality: "scalar",
@@ -56821,7 +56762,7 @@ var batteryCustomize = {
56821
56762
  stateField: "percentage"
56822
56763
  },
56823
56764
  deviceTypes: BATTERY_DEVICE_TYPES,
56824
- apply: "add-or-replace",
56765
+ apply: "add",
56825
56766
  path: "percentage",
56826
56767
  sources: [{
56827
56768
  kind: "field",
@@ -56942,10 +56883,16 @@ onStatusChanged: { data: object({
56942
56883
  /**
56943
56884
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56944
56885
  */
56945
- history: { fields: { percentage: {
56946
- kind: "gauge",
56947
- unit: "%"
56948
- } } },
56886
+ history: { fields: {
56887
+ percentage: {
56888
+ kind: "gauge",
56889
+ unit: "%"
56890
+ },
56891
+ volts: {
56892
+ kind: "gauge",
56893
+ unit: "V"
56894
+ }
56895
+ } },
56949
56896
  customize: batteryCustomize,
56950
56897
  /** Clock fields: written, but excluded from the compare that decides
56951
56898
  * whether persisting is worth a SQLite commit. */
@@ -56992,12 +56939,7 @@ var binaryCapability = {
56992
56939
  history: { fields: { on: { kind: "boolean" } } },
56993
56940
  /** Clock fields: written, but excluded from the compare that decides
56994
56941
  * whether persisting is worth a SQLite commit. */
56995
- volatileStateFields: ["lastChangedAt"],
56996
- customize: {
56997
- version: 1,
56998
- advanced: "derive",
56999
- targets: []
57000
- }
56942
+ volatileStateFields: ["lastChangedAt"]
57001
56943
  };
57002
56944
  /**
57003
56945
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57013,7 +56955,7 @@ var binaryCapability = {
57013
56955
  */
57014
56956
  var BrightnessStatusSchema = object({
57015
56957
  /** Current level as 0..100 inclusive. Firmware-reported. */
57016
- percentage: number().min(0).max(100),
56958
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57017
56959
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57018
56960
  lastChangedAt: number()
57019
56961
  });
@@ -57484,21 +57426,21 @@ var ClimateControlStatusSchema = object({
57484
57426
  /** Single setpoint in Celsius. Used by single-target modes
57485
57427
  * (heat / cool / dry). Null when the device is in a range mode
57486
57428
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57487
- target: number().nullable(),
57429
+ target: number().nullable().meta(readingMeta("temperature")),
57488
57430
  /** Upper bound of the dual setpoint in Celsius. Populated only
57489
57431
  * in `heat_cool` mode. */
57490
- targetHigh: number().nullable(),
57432
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57491
57433
  /** Lower bound of the dual setpoint in Celsius. Populated only
57492
57434
  * in `heat_cool` mode. */
57493
- targetLow: number().nullable(),
57435
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57494
57436
  /** Target relative humidity (0..100). Null when the device has no
57495
57437
  * humidity control. */
57496
- targetHumidity: number().min(0).max(100).nullable(),
57438
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57497
57439
  /** Current measured temperature in Celsius. Read-only — pushed by
57498
57440
  * the device's internal sensor. */
57499
- currentTemp: number().nullable(),
57441
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57500
57442
  /** Current measured relative humidity (0..100). Read-only. */
57501
- currentHumidity: number().min(0).max(100).nullable(),
57443
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57502
57444
  /** Vertical louver swing on/off. Null when the device has no vertical
57503
57445
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57504
57446
  swingVertical: boolean().nullable(),
@@ -57506,11 +57448,11 @@ var ClimateControlStatusSchema = object({
57506
57448
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57507
57449
  swingHorizontal: boolean().nullable(),
57508
57450
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57509
- minTemp: number().nullable().optional(),
57451
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57510
57452
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57511
- maxTemp: number().nullable().optional(),
57453
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57512
57454
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57513
- tempStep: number().positive().nullable().optional(),
57455
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57514
57456
  /** Ms epoch when the slice was last updated (push or command). */
57515
57457
  lastFetchedAt: number()
57516
57458
  });
@@ -57652,17 +57594,17 @@ var climateControlCapability = {
57652
57594
  * modes to expose in the picker without a slice fetch.
57653
57595
  */
57654
57596
  var RgbTripletSchema = object({
57655
- r: number().int().min(0).max(255),
57656
- g: number().int().min(0).max(255),
57657
- b: number().int().min(0).max(255)
57597
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57598
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57599
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57658
57600
  });
57659
57601
  var HsvTripletSchema = object({
57660
57602
  /** Hue in degrees, 0..360. */
57661
- h: number().min(0).max(360),
57603
+ h: number().min(0).max(360).meta(readingMeta("none")),
57662
57604
  /** Saturation as 0..100 inclusive. */
57663
- s: number().min(0).max(100),
57605
+ s: number().min(0).max(100).meta(readingMeta("none")),
57664
57606
  /** Value/brightness as 0..100 inclusive. */
57665
- v: number().min(0).max(100)
57607
+ v: number().min(0).max(100).meta(readingMeta("none"))
57666
57608
  });
57667
57609
  var ColorInputSchema = discriminatedUnion("mode", [
57668
57610
  object({
@@ -57691,7 +57633,7 @@ var ColorStatusSchema = object({
57691
57633
  /** Populated when `mode === 'hsv'`. */
57692
57634
  hsv: HsvTripletSchema.optional(),
57693
57635
  /** Populated when `mode === 'mired'`. */
57694
- mireds: number().int().optional(),
57636
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57695
57637
  /** Ms epoch of the last operator-driven change. */
57696
57638
  lastChangedAt: number()
57697
57639
  });
@@ -57886,18 +57828,12 @@ _enum([
57886
57828
  "empty",
57887
57829
  "full"
57888
57830
  ]);
57889
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57890
- * each. */
57891
- var ConsumableRemainingUnitSchema = _enum([
57892
- "days",
57893
- "hours",
57894
- "cycles"
57895
- ]);
57896
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57897
- * days). `value` null = unknown (D393). */
57831
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57832
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57833
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57898
57834
  var ConsumableRemainingSchema = object({
57899
- value: number().min(0).nullable(),
57900
- unit: ConsumableRemainingUnitSchema
57835
+ value: number().min(0).nullable().meta(readingMeta("device")),
57836
+ kind: ConsumableRemainingKindSchema
57901
57837
  });
57902
57838
  /** A single consumable item. Either a continuous `level` (remaining
57903
57839
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57910,7 +57846,7 @@ var ConsumableItemSchema = object({
57910
57846
  /** Display name. */
57911
57847
  label: string().min(1),
57912
57848
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57913
- level: number().min(0).max(100).nullable(),
57849
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57914
57850
  /** The provider's discrete verdict when known (binary mode). */
57915
57851
  status: ConsumableStatusSchema.nullable(),
57916
57852
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57992,8 +57928,8 @@ var consumablesCustomize = {
57992
57928
  transforms: [{ kind: "identity" }]
57993
57929
  }],
57994
57930
  literals: [{
57995
- path: "remaining.unit",
57996
- values: [...ConsumableRemainingUnitSchema.options]
57931
+ path: "remaining.kind",
57932
+ values: [...ConsumableRemainingKindSchema.options]
57997
57933
  }]
57998
57934
  },
57999
57935
  {
@@ -58122,7 +58058,6 @@ reset: method(object({
58122
58058
  },
58123
58059
  "remaining.value": {
58124
58060
  kind: "gauge",
58125
- unitFrom: "remaining.unit",
58126
58061
  deadband: 1,
58127
58062
  displayName: "Remaining"
58128
58063
  }
@@ -58192,10 +58127,9 @@ var contactCapability = {
58192
58127
  * (empty array for the other kinds). It lives in the slice because
58193
58128
  * HA `select.*` entities may update their options at runtime.
58194
58129
  *
58195
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58196
- * `kind === 'numeric'` and are absent (undefined) for all other
58197
- * kinds. They are populated live from HA attributes on each state
58198
- * push — consistent with `options` and the sensor-unit approach.
58130
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58131
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58132
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58199
58133
  */
58200
58134
  var ControlStatusSchema = object({
58201
58135
  kind: _enum([
@@ -58207,39 +58141,17 @@ var ControlStatusSchema = object({
58207
58141
  /** Current value. Numeric kind → number; select/text/datetime → string.
58208
58142
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58209
58143
  * or time-only — HA accepts all three forms). */
58210
- value: union([number(), string()]),
58144
+ value: union([number().meta(readingMeta("device")), string()]),
58211
58145
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58212
58146
  options: array(string()),
58213
58147
  /** Ms epoch when the slice was last updated. */
58214
58148
  lastChangedAt: number(),
58215
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58216
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58217
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58218
- unit: string().optional(),
58219
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58220
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58221
- min: number().optional(),
58222
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58223
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58224
- max: number().optional(),
58225
- /** Step increment. Populated live from HA `attributes.step`.
58226
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58227
- step: number().optional(),
58228
- /** Suggested decimal places for numeric display. Populated live from HA
58229
- * `attributes.suggested_display_precision`, or derived from `step` when the
58230
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58231
- * other kinds. Falls back to auto-formatting when absent. */
58232
- precision: number().int().min(0).max(10).optional(),
58233
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58234
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58235
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58236
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58237
- * `has_date`/`has_time` attributes on each push. */
58238
- format: _enum([
58239
- "date",
58240
- "time",
58241
- "datetime"
58242
- ]).optional()
58149
+ /** Minimum allowed value, in the device quantity's base unit. */
58150
+ min: number().optional().meta(readingMeta("device")),
58151
+ /** Maximum allowed value, in the device quantity's base unit. */
58152
+ max: number().optional().meta(readingMeta("device")),
58153
+ /** Step increment, in the device quantity's base unit. */
58154
+ step: number().optional().meta(readingMeta("device"))
58243
58155
  });
58244
58156
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58245
58157
  object({
@@ -58305,9 +58217,9 @@ var CoverStatusSchema = object({
58305
58217
  ]),
58306
58218
  /** 0 = fully closed, 100 = fully open. Null when the device has
58307
58219
  * no intermediate position surface. */
58308
- position: number().min(0).max(100).nullable(),
58220
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58309
58221
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58310
- tiltPosition: number().min(0).max(100).nullable(),
58222
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58311
58223
  /** Ms epoch when the slice was last updated. */
58312
58224
  lastChangedAt: number()
58313
58225
  });
@@ -58410,9 +58322,9 @@ var NormalizedRangeSchema$1 = object({
58410
58322
  var DayNightStatusSchema = object({
58411
58323
  mode: DayNightModeSchema,
58412
58324
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58413
- sensitivity: number().optional(),
58325
+ sensitivity: number().optional().meta(readingMeta("none")),
58414
58326
  /** Delay before the IR-cut filter flips, in seconds. */
58415
- switchDelaySec: number().optional(),
58327
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58416
58328
  lastFetchedAt: number()
58417
58329
  });
58418
58330
  /**
@@ -58630,11 +58542,11 @@ var DishwasherStatusSchema = object({
58630
58542
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58631
58543
  availablePrograms: array(string()),
58632
58544
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58633
- remainingSeconds: number().int().nonnegative().nullable(),
58545
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58634
58546
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58635
- delayedStartSeconds: number().int().nonnegative().nullable(),
58547
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58636
58548
  /** Cycle completion 0..100, or null when not applicable. */
58637
- progressPercent: number().min(0).max(100).nullable(),
58549
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58638
58550
  /** True when the door is reported open; null when the door state is not known. */
58639
58551
  doorOpen: boolean().nullable(),
58640
58552
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58658,7 +58570,7 @@ var DishwasherStatusSchema = object({
58658
58570
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58659
58571
  machineCareDue: boolean().nullable(),
58660
58572
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58661
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58573
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58662
58574
  /** Option states: null when the appliance does not expose the option or has not said. */
58663
58575
  halfLoad: boolean().nullable(),
58664
58576
  hygienePlus: boolean().nullable(),
@@ -58673,7 +58585,7 @@ var DishwasherStatusSchema = object({
58673
58585
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58674
58586
  activeEvents: array(string()),
58675
58587
  /** Numeric error code when the appliance reports one; null otherwise. */
58676
- errorCode: number().nullable(),
58588
+ errorCode: number().nullable().meta(readingMeta("none")),
58677
58589
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58678
58590
  errorLabel: string().nullable(),
58679
58591
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58904,7 +58816,7 @@ var DoorbellStatusSchema = object({
58904
58816
  /** Ms epoch of the last press. null = never observed since this provider started. */
58905
58817
  lastPressedAt: number().nullable(),
58906
58818
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58907
- pressCountSinceStart: number()
58819
+ pressCountSinceStart: number().meta(readingMeta("count"))
58908
58820
  });
58909
58821
  var DoorbellPressEventSchema = object({
58910
58822
  deviceId: number(),
@@ -58969,17 +58881,7 @@ onPressed: {
58969
58881
  history: "none",
58970
58882
  customize: doorbellCustomize
58971
58883
  };
58972
- /**
58973
- * Enum-state sensor — a string value picked from a finite option set.
58974
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58975
- * states, weather conditions, contact-source channels, mode strings).
58976
- * The option set is stable across observations; it is persisted in the
58977
- * device config blob (via the control slice at adoption) and is NOT
58978
- * stored in `sourceInfo`.
58979
- */
58980
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58981
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58982
- var EnumSensorDateTimeFormatSchema = _enum([
58884
+ _enum([
58983
58885
  "date",
58984
58886
  "time",
58985
58887
  "datetime"
@@ -58987,12 +58889,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58987
58889
  var EnumSensorStatusSchema = object({
58988
58890
  value: string(),
58989
58891
  /**
58990
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58991
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58992
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58993
- * a time-only sensor → `'time'`.
58892
+ * Set when `value` is a timestamp rather than an enum state, so history
58893
+ * does not learn ISO strings as states. The display shape (date / time /
58894
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58994
58895
  */
58995
- format: EnumSensorDateTimeFormatSchema.optional(),
58896
+ timestamp: boolean().optional(),
58996
58897
  /** Ms epoch when the slice was last updated. */
58997
58898
  lastFetchedAt: number()
58998
58899
  });
@@ -59019,11 +58920,11 @@ var enumSensorCapability = {
59019
58920
  * History (D850): the value as an `enum` series (an appliance's program or
59020
58921
  * operation state, an HVAC action). The option set is per device, so the
59021
58922
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59022
- * `format` carries a timestamp, not a state, and is not recorded.
58923
+ * `timestamp` carries a clock, not a state, and is not recorded.
59023
58924
  */
59024
58925
  history: { fields: { value: {
59025
58926
  kind: "enum",
59026
- skipWhenSet: "format"
58927
+ skipWhenSet: "timestamp"
59027
58928
  } } },
59028
58929
  /** Clock fields: written, but excluded from the compare that decides
59029
58930
  * whether persisting is worth a SQLite commit. */
@@ -59043,13 +58944,13 @@ var EventFireSchema = object({
59043
58944
  deviceId: number(),
59044
58945
  eventType: string(),
59045
58946
  data: record(string(), unknown()).nullable(),
59046
- timestamp: number(),
59047
- seq: number()
58947
+ timestamp: number().meta(readingMeta("none")),
58948
+ seq: number().meta(readingMeta("count"))
59048
58949
  });
59049
58950
  var EventEmitterStatusSchema = object({
59050
58951
  eventTypes: array(string()),
59051
58952
  lastEvent: EventFireSchema.nullable(),
59052
- eventCountSinceStart: number()
58953
+ eventCountSinceStart: number().meta(readingMeta("count"))
59053
58954
  });
59054
58955
  var eventEmitterCapability = {
59055
58956
  name: "event-emitter",
@@ -59462,10 +59363,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59462
59363
  var FanControlStatusSchema = object({
59463
59364
  /** Active speed as 0..100 inclusive. Null when the device has no
59464
59365
  * speed surface (single-speed fan). */
59465
- percentage: number().min(0).max(100).nullable(),
59366
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59466
59367
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59467
59368
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59468
- percentageStep: number().positive().optional(),
59369
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59469
59370
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59470
59371
  * — empty string when no preset is active or supported. */
59471
59372
  preset: string(),
@@ -59586,7 +59487,7 @@ var FeatureProbeStatusSchema = object({
59586
59487
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59587
59488
  model: string().nullable(),
59588
59489
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59589
- channelCount: number().nullable(),
59490
+ channelCount: number().nullable().meta(readingMeta("count")),
59590
59491
  /**
59591
59492
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59592
59493
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59711,7 +59612,7 @@ var gasCapability = {
59711
59612
  };
59712
59613
  var HeatingValveStatusSchema = object({
59713
59614
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59714
- position: number().min(0).max(100).nullable(),
59615
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59715
59616
  /** Ms epoch when the slice was last updated (push or poll). */
59716
59617
  lastFetchedAt: number()
59717
59618
  });
@@ -59754,9 +59655,9 @@ var HumidifierStatusSchema = object({
59754
59655
  /** Whether the humidifier is currently on. */
59755
59656
  on: boolean(),
59756
59657
  /** Current measured relative humidity (0..100). Null when not reported. */
59757
- currentHumidity: number().min(0).max(100).nullable(),
59658
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59758
59659
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59759
- targetHumidity: number().min(0).max(100).nullable(),
59660
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59760
59661
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59761
59662
  * device has no mode surface. */
59762
59663
  mode: string().nullable(),
@@ -59766,9 +59667,9 @@ var HumidifierStatusSchema = object({
59766
59667
  * `idle` / `off`). Null when the device doesn't report it. */
59767
59668
  action: string().nullable(),
59768
59669
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59769
- minHumidity: number().nullable(),
59670
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59770
59671
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59771
- maxHumidity: number().nullable(),
59672
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59772
59673
  /** Ms epoch when the slice was last updated. */
59773
59674
  lastChangedAt: number()
59774
59675
  });
@@ -59838,22 +59739,13 @@ var humidifierCapability = {
59838
59739
  * `sourceInfo`.
59839
59740
  */
59840
59741
  var HumiditySensorStatusSchema = object({
59841
- /** Current relative humidity, 0..100. */
59842
- percent: number().min(0).max(100).meta({
59843
- unit: "%",
59742
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59743
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59744
+ precision: 0,
59844
59745
  label: "Humidity"
59845
- }),
59746
+ })),
59846
59747
  /** Ms epoch when the slice was last updated. */
59847
- lastFetchedAt: number(),
59848
- /** Live display unit from the upstream source (e.g. HA
59849
- * `attributes.unit_of_measurement`). The UI prefers this over the
59850
- * role's canonical unit. Absent → fall back to the canonical unit. */
59851
- unit: string().optional(),
59852
- /** Suggested decimal places for numeric display.
59853
- * Populated live from the upstream source when provided (e.g. HA
59854
- * `attributes.suggested_display_precision`). Falls back to
59855
- * auto-formatting when absent. */
59856
- precision: number().int().min(0).max(10).optional()
59748
+ lastFetchedAt: number()
59857
59749
  });
59858
59750
  var humiditySensorCapability = {
59859
59751
  name: "humidity-sensor",
@@ -59884,12 +59776,7 @@ var humiditySensorCapability = {
59884
59776
  } } },
59885
59777
  /** Clock fields: written, but excluded from the compare that decides
59886
59778
  * whether persisting is worth a SQLite commit. */
59887
- volatileStateFields: ["lastFetchedAt"],
59888
- customize: {
59889
- version: 1,
59890
- advanced: "derive",
59891
- targets: []
59892
- }
59779
+ volatileStateFields: ["lastFetchedAt"]
59893
59780
  };
59894
59781
  /**
59895
59782
  * Image display cap. Models a single still image exposed by an integration —
@@ -59910,7 +59797,7 @@ var ImageStatusSchema = object({
59910
59797
  * entity exposes no `entity_picture` (yet). */
59911
59798
  url: string().nullable(),
59912
59799
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59913
- lastUpdated: number().nullable()
59800
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59914
59801
  });
59915
59802
  var imageCapability = {
59916
59803
  name: "image",
@@ -59998,19 +59885,19 @@ var NormalizedRangeSchema = object({
59998
59885
  */
59999
59886
  var ImageSettingsStatusSchema = object({
60000
59887
  /** Normalized 0–100. */
60001
- brightness: number().optional(),
59888
+ brightness: number().optional().meta(readingMeta("none")),
60002
59889
  /** Normalized 0–100. */
60003
- contrast: number().optional(),
59890
+ contrast: number().optional().meta(readingMeta("none")),
60004
59891
  /** Normalized 0–100. */
60005
- saturation: number().optional(),
59892
+ saturation: number().optional().meta(readingMeta("none")),
60006
59893
  /** Normalized 0–100. */
60007
- sharpness: number().optional(),
59894
+ sharpness: number().optional().meta(readingMeta("none")),
60008
59895
  mirror: boolean().optional(),
60009
59896
  flip: boolean().optional(),
60010
59897
  rotate: ImageRotateSchema.optional(),
60011
59898
  whiteBalance: WhiteBalanceModeSchema.optional(),
60012
59899
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60013
- warmth: number().optional(),
59900
+ warmth: number().optional().meta(readingMeta("none")),
60014
59901
  exposureMode: ExposureModeSchema.optional(),
60015
59902
  backlightMode: BacklightModeSchema.optional(),
60016
59903
  lastFetchedAt: number()
@@ -60289,10 +60176,10 @@ var IntercomAbilitySchema = object({
60289
60176
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60290
60177
  codecs: array(string()),
60291
60178
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60292
- sampleRate: number(),
60179
+ sampleRate: number().meta(readingMeta("none")),
60293
60180
  duplex: _enum(["half", "full"]),
60294
60181
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60295
- maxBacklogMs: number()
60182
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60296
60183
  });
60297
60184
  var IntercomStatusSchema = object({
60298
60185
  /** True while a talk session is open. */
@@ -60519,13 +60406,13 @@ var LawnMowerControlStatusSchema = object({
60519
60406
  activity: LawnMowerActivitySchema,
60520
60407
  /** 0..100 battery percentage. Null when the device has no battery
60521
60408
  * reading. */
60522
- batteryLevel: number().min(0).max(100).nullable(),
60409
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60523
60410
  /** 0..100 mowing-completion percentage of the current task, or null when no
60524
60411
  * task is active / progress is unavailable. */
60525
- progressPercent: number().min(0).max(100).nullable(),
60412
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60526
60413
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60527
60414
  * or null when unknown. */
60528
- currentCode: number().nullable(),
60415
+ currentCode: number().nullable().meta(readingMeta("none")),
60529
60416
  /** Human label for {@link currentCode}, or null when undecodable. */
60530
60417
  currentCodeLabel: string().nullable(),
60531
60418
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61058,7 +60945,7 @@ var MediaPlayerStatusSchema = object({
61058
60945
  state: MediaPlayerStateSchema,
61059
60946
  /** Volume as 0..100 inclusive. Null when the device has no volume
61060
60947
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61061
- volumeLevel: number().min(0).max(100).nullable(),
60948
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61062
60949
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61063
60950
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61064
60951
  isMuted: boolean().nullable(),
@@ -61071,10 +60958,10 @@ var MediaPlayerStatusSchema = object({
61071
60958
  currentMedia: MediaInfoSchema.nullable(),
61072
60959
  /** Current playback position in ms. Null when not seekable or
61073
60960
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61074
- positionMs: number().int().nonnegative().nullable(),
60961
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61075
60962
  /** Total duration of the current media in ms. Null when unknown
61076
60963
  * (live stream) or nothing is playing. */
61077
- durationMs: number().int().nonnegative().nullable(),
60964
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61078
60965
  /** Shuffle toggle. Null when no shuffle surface.
61079
60966
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61080
60967
  shuffle: boolean().nullable(),
@@ -61383,7 +61270,7 @@ var motionCustomize = {
61383
61270
  eventName: "onMotionChanged"
61384
61271
  },
61385
61272
  deviceTypes: [DeviceType.Camera],
61386
- apply: "replace",
61273
+ apply: "add",
61387
61274
  applyAuthority: {
61388
61275
  kind: "camera-sensors",
61389
61276
  timeoutField: "motionCooldownMs"
@@ -61455,7 +61342,7 @@ var MotionStatusSchema = object({
61455
61342
  * it, every provider seeded it at boot, and every session left it
61456
61343
  * `detected: true` after its detach.
61457
61344
  */
61458
- autoClearAfterMs: number().nullable()
61345
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61459
61346
  });
61460
61347
  /**
61461
61348
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61637,7 +61524,7 @@ var MotionZoneRegionSchema = object({
61637
61524
  * the grid region(s). */
61638
61525
  var MotionZoneStatusSchema = object({
61639
61526
  enabled: boolean(),
61640
- sensitivity: number(),
61527
+ sensitivity: number().meta(readingMeta("none")),
61641
61528
  /** Grid region(s). Today exactly one `grid` shape. */
61642
61529
  regions: array(MotionZoneRegionSchema),
61643
61530
  lastFetchedAt: number()
@@ -62007,7 +61894,7 @@ var NavigationStatusSchema = object({
62007
61894
  following: boolean(),
62008
61895
  flash: boolean(),
62009
61896
  lightMode: NavigationLightModeSchema,
62010
- lightLevel: number().min(40).max(100),
61897
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62011
61898
  /** Ms epoch when the slice was last updated. */
62012
61899
  lastChangedAt: number()
62013
61900
  });
@@ -62141,13 +62028,13 @@ var NetworkLinkStatusSchema = object({
62141
62028
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62142
62029
  * SKIP a null rather than coerce it.
62143
62030
  */
62144
- signalPercent: number().min(0).max(100).nullable(),
62031
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62145
62032
  /** Raw received signal strength in dBm, when the firmware reports one. */
62146
- rssiDbm: number().optional(),
62033
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62147
62034
  /** Network name of a wireless link, when the firmware reports it. */
62148
62035
  ssid: string().optional(),
62149
62036
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62150
- lastUpdated: number()
62037
+ lastUpdated: number().meta(readingMeta("none"))
62151
62038
  });
62152
62039
  var networkLinkCapability = {
62153
62040
  name: "network-link",
@@ -62532,7 +62419,7 @@ var NotifierStatusSchema = object({
62532
62419
  lastError: string().nullable(),
62533
62420
  /** Number of deliveries currently buffered server-side (rate-limited
62534
62421
  * notifiers may queue). 0 when the provider sends synchronously. */
62535
- queueDepth: number().int().nonnegative(),
62422
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62536
62423
  /** Per-feature capability matrix — authoritative source for the
62537
62424
  * compose-form field gating. */
62538
62425
  supports: NotifierSupportsSchema
@@ -62624,11 +62511,9 @@ onSent: { data: object({
62624
62511
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62625
62512
  * for: water flow, distance, weight, frequency, signal strength, …).
62626
62513
  *
62627
- * The `unit` and `precision` fields carry the live display metadata that
62628
- * the upstream source provides with each state push (HA
62629
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62630
- * The UI reads them directly from the slice — they are the single source
62631
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
62514
+ * The quantity is not on this slice. It is chosen when the device is
62515
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
62516
+ * that quantity's base unit. `null` is a refused sample (D393).
62632
62517
  *
62633
62518
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62634
62519
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62637,15 +62522,8 @@ onSent: { data: object({
62637
62522
  * numeric values land here.
62638
62523
  */
62639
62524
  var NumericSensorStatusSchema = object({
62640
- value: number(),
62641
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62642
- * Populated live from the upstream source on each state push.
62643
- * Absent for unitless sensors. */
62644
- unit: string().optional(),
62645
- /** Suggested decimal places for numeric display.
62646
- * Populated live from the upstream source when provided.
62647
- * Falls back to auto-formatting when absent. */
62648
- precision: number().int().min(0).max(10).optional(),
62525
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
62526
+ value: number().nullable().meta(readingMeta("device")),
62649
62527
  /** Ms epoch when the slice was last updated. */
62650
62528
  lastFetchedAt: number()
62651
62529
  });
@@ -62669,11 +62547,11 @@ var numericSensorCapability = {
62669
62547
  */
62670
62548
  durability: "restored",
62671
62549
  /**
62672
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
62550
+ * History (D850): recorded series. The unit is the device quantity's base,
62551
+ * resolved when the series is read — the cap has no single unit.
62673
62552
  */
62674
62553
  history: { fields: { value: {
62675
62554
  kind: "gauge",
62676
- unitFrom: "unit",
62677
62555
  minIntervalMs: 3e4
62678
62556
  } } },
62679
62557
  /** Clock fields: written, but excluded from the compare that decides
@@ -62965,23 +62843,26 @@ var PetFeederStatusSchema = object({
62965
62843
  /** Food currently in the bowl (grams). Null when the device has not
62966
62844
  * reported a reading yet. On dual-hopper models this is the combined
62967
62845
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62968
- foodLevel: number().nullable(),
62846
+ foodLevel: number().nullable().meta(readingMeta("none")),
62969
62847
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62970
62848
  * single-hopper models. */
62971
- food1: number().nullable(),
62849
+ food1: number().nullable().meta(readingMeta("none")),
62972
62850
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62973
62851
  * single-hopper models. */
62974
- food2: number().nullable(),
62852
+ food2: number().nullable().meta(readingMeta("none")),
62975
62853
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62976
62854
  * (`device_class: problem`, on = low). True when the bowl is empty /
62977
62855
  * below the feeder's low threshold. */
62978
62856
  lowFood: boolean(),
62979
62857
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62980
62858
  * device has no battery reading. */
62981
- batteryPower: number().min(0).max(100).nullable(),
62982
- /** Days of desiccant life remaining. Null when the model has no
62983
- * desiccant sensor. */
62984
- desiccantLeftDays: number().nullable(),
62859
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62860
+ /**
62861
+ * Desiccant life remaining, stored in seconds (duration base, D944).
62862
+ * Providers convert from the days the feeder reports. Null when the
62863
+ * model has no desiccant sensor.
62864
+ */
62865
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62985
62866
  /** True while a feed is in progress. */
62986
62867
  feeding: boolean(),
62987
62868
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62992,7 +62873,7 @@ var PetFeederStatusSchema = object({
62992
62873
  * with `errorCode` for consumers that want the raw integer. */
62993
62874
  error: string().nullable(),
62994
62875
  /** Raw device error code (0 / null = no error). */
62995
- errorCode: number().nullable(),
62876
+ errorCode: number().nullable().meta(readingMeta("none")),
62996
62877
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62997
62878
  isDualHopper: boolean(),
62998
62879
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63002,7 +62883,7 @@ var PetFeederStatusSchema = object({
63002
62883
  /** Play a chime when dispensing. */
63003
62884
  feedSound: boolean(),
63004
62885
  /** Speaker / prompt volume level (device-scaled integer). */
63005
- volume: number(),
62886
+ volume: number().meta(readingMeta("none")),
63006
62887
  /** Ms epoch when the slice was last refreshed from the cloud. */
63007
62888
  lastFetchedAt: number()
63008
62889
  });
@@ -63121,7 +63002,7 @@ var petFeederCapability = {
63121
63002
  },
63122
63003
  desiccantLeftDays: {
63123
63004
  kind: "gauge",
63124
- unit: "days"
63005
+ unit: "s"
63125
63006
  }
63126
63007
  } }
63127
63008
  };
@@ -63522,36 +63403,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63522
63403
  */
63523
63404
  var PowerMeterStatusSchema = object({
63524
63405
  /** Instantaneous power draw in watts. */
63525
- watts: number().optional().meta({
63526
- unit: "W",
63527
- label: "Power"
63528
- }),
63406
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63529
63407
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63530
- kwhTotal: number().optional().meta({
63531
- unit: "kWh",
63532
- label: "Energy"
63533
- }),
63408
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63534
63409
  /** Voltage in volts. */
63535
- volts: number().optional(),
63410
+ volts: number().optional().meta(readingMeta("voltage")),
63536
63411
  /** Current in amperes. */
63537
- amps: number().optional(),
63538
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63539
- va: number().optional(),
63412
+ amps: number().optional().meta(readingMeta("current")),
63413
+ /** Apparent power in volt-amperes. */
63414
+ va: number().optional().meta(readingMeta("apparent-power")),
63540
63415
  /** Power factor (0..1) if reported. */
63541
- powerFactor: number().min(0).max(1).optional(),
63416
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63542
63417
  /** Ms epoch when the slice was last updated. */
63543
- lastFetchedAt: number(),
63544
- /** Live display unit of the single metric this slice carries (e.g. HA
63545
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63546
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63547
- * unit per slice is unambiguous. The UI prefers this over the role's
63548
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63549
- unit: string().optional(),
63550
- /** Suggested decimal places for numeric display.
63551
- * Populated live from the upstream source when provided (e.g. HA
63552
- * `attributes.suggested_display_precision`). Falls back to
63553
- * auto-formatting when absent. */
63554
- precision: number().int().min(0).max(10).optional()
63418
+ lastFetchedAt: number()
63555
63419
  });
63556
63420
  var powerMeterCapability = {
63557
63421
  name: "power-meter",
@@ -63615,12 +63479,7 @@ var powerMeterCapability = {
63615
63479
  } },
63616
63480
  /** Clock fields: written, but excluded from the compare that decides
63617
63481
  * whether persisting is worth a SQLite commit. */
63618
- volatileStateFields: ["lastFetchedAt"],
63619
- customize: {
63620
- version: 1,
63621
- advanced: "derive",
63622
- targets: []
63623
- }
63482
+ volatileStateFields: ["lastFetchedAt"]
63624
63483
  };
63625
63484
  /**
63626
63485
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63640,11 +63499,11 @@ var powerMeterCapability = {
63640
63499
  */
63641
63500
  var GpsLocationSchema = object({
63642
63501
  /** Latitude in decimal degrees, -90..90. */
63643
- latitude: number().min(-90).max(90),
63502
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63644
63503
  /** Longitude in decimal degrees, -180..180. */
63645
- longitude: number().min(-180).max(180),
63504
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63646
63505
  /** Reported accuracy in meters (lower = better). */
63647
- accuracyMeters: number().nonnegative()
63506
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63648
63507
  });
63649
63508
  var PresenceStatusSchema = object({
63650
63509
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63656,7 +63515,7 @@ var PresenceStatusSchema = object({
63656
63515
  gps: GpsLocationSchema.nullable(),
63657
63516
  /** Optional battery level of the tracking device (0..100). Null
63658
63517
  * when not reported. */
63659
- batteryPercent: number().min(0).max(100).nullable(),
63518
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63660
63519
  /** Ms epoch when the slice was last updated. */
63661
63520
  lastChangedAt: number()
63662
63521
  });
@@ -63706,19 +63565,10 @@ var presenceCapability = {
63706
63565
  * `sourceInfo`.
63707
63566
  */
63708
63567
  var PressureSensorStatusSchema = object({
63709
- /** Current pressure in hPa. */
63710
- hpa: number(),
63568
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
63569
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63711
63570
  /** Ms epoch when the slice was last updated. */
63712
- lastFetchedAt: number(),
63713
- /** Live display unit from the upstream source (e.g. HA
63714
- * `attributes.unit_of_measurement`). The UI prefers this over the
63715
- * role's canonical unit. Absent → fall back to the canonical unit. */
63716
- unit: string().optional(),
63717
- /** Suggested decimal places for numeric display.
63718
- * Populated live from the upstream source when provided (e.g. HA
63719
- * `attributes.suggested_display_precision`). Falls back to
63720
- * auto-formatting when absent. */
63721
- precision: number().int().min(0).max(10).optional()
63571
+ lastFetchedAt: number()
63722
63572
  });
63723
63573
  var pressureSensorCapability = {
63724
63574
  name: "pressure-sensor",
@@ -64132,8 +63982,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64132
63982
  }), _void(), { kind: "mutation" });
64133
63983
  var PtzAutotrackSettingsSchema = object({
64134
63984
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64135
- stopDelaySeconds: number().int().min(0).max(300),
64136
- disappearDelaySeconds: number().int().min(0).max(300)
63985
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
63986
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64137
63987
  });
64138
63988
  /**
64139
63989
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64287,7 +64137,7 @@ var RunningStateStatusSchema = object({
64287
64137
  /** null = unknown — never coerced to false (D393). */
64288
64138
  running: boolean().nullable(),
64289
64139
  /** Epoch ms the current boolean began; null while `running` is null. */
64290
- since: number().nullable(),
64140
+ since: number().nullable().meta(readingMeta("none")),
64291
64141
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64292
64142
  phase: string().nullable(),
64293
64143
  /** null until a reading has been published (D393). */
@@ -64302,7 +64152,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64302
64152
  /** Generic Customize target for the running boolean (D936). */
64303
64153
  var runningCustomize = {
64304
64154
  version: 1,
64305
- advanced: "derive",
64306
64155
  targets: [{
64307
64156
  id: "running",
64308
64157
  cardinality: "scalar",
@@ -64322,7 +64171,7 @@ var runningCustomize = {
64322
64171
  DeviceType.Thermostat,
64323
64172
  DeviceType.PetFeeder
64324
64173
  ],
64325
- apply: "add-or-replace",
64174
+ apply: "add",
64326
64175
  path: "running",
64327
64176
  sources: [
64328
64177
  {
@@ -65586,9 +65435,9 @@ var RecordTriggerSchema = _enum([
65586
65435
  */
65587
65436
  var RecordWindowSchema = object({
65588
65437
  trigger: RecordTriggerSchema,
65589
- day: number().int().min(0).max(6),
65590
- startMinute: number().int().min(0).max(1439),
65591
- endMinute: number().int().min(1).max(1440)
65438
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65439
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65440
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65592
65441
  });
65593
65442
  /** Status of one physical volume, as the camera itself describes it. */
65594
65443
  var OnboardStorageVolumeSchema = object({
@@ -65664,10 +65513,10 @@ var RecordingOnboardStatusSchema = object({
65664
65513
  /** Master "record to the card at all" switch. */
65665
65514
  enabled: boolean().nullable(),
65666
65515
  overwriteWhenFull: boolean().nullable(),
65667
- preRecordSec: number().nullable(),
65668
- postRecordSec: number().nullable(),
65516
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
65517
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65669
65518
  /** Length of one recorded file, in minutes. */
65670
- segmentMinutes: number().nullable(),
65519
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65671
65520
  /** The primary track's weekly windows, flattened. */
65672
65521
  windows: array(RecordWindowSchema),
65673
65522
  /**
@@ -65683,7 +65532,7 @@ var RecordingOnboardStatusSchema = object({
65683
65532
  * provider reporting a non-zero count also reports the schedule as not
65684
65533
  * writable.
65685
65534
  */
65686
- unreadableWindows: number(),
65535
+ unreadableWindows: number().meta(readingMeta("count")),
65687
65536
  /**
65688
65537
  * The camera is scheduled to record and has NO usable storage.
65689
65538
  *
@@ -66528,7 +66377,7 @@ var CamStreamDescriptorSchema = object({
66528
66377
  url: string().optional(),
66529
66378
  codec: string().optional(),
66530
66379
  resolution: CamStreamResolutionSchema.optional(),
66531
- fps: number().positive().optional(),
66380
+ fps: number().positive().optional().meta(readingMeta("rate")),
66532
66381
  label: string().optional(),
66533
66382
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66534
66383
  deviceFeatures: array(string()).optional(),
@@ -66615,18 +66464,18 @@ var StreamProfileSchema = _enum([
66615
66464
  ]);
66616
66465
  /** The current encoding config of one stream profile. */
66617
66466
  var StreamProfileConfigSchema = object({
66618
- width: number(),
66619
- height: number(),
66467
+ width: number().meta(readingMeta("none")),
66468
+ height: number().meta(readingMeta("none")),
66620
66469
  codec: _enum(["h264", "h265"]),
66621
- framerate: number(),
66622
- bitrate: number(),
66470
+ framerate: number().meta(readingMeta("rate")),
66471
+ bitrate: number().meta(readingMeta("bitrate")),
66623
66472
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66624
66473
  encoderProfile: _enum([
66625
66474
  "high",
66626
66475
  "main",
66627
66476
  "baseline"
66628
66477
  ]).optional(),
66629
- gop: number().optional(),
66478
+ gop: number().optional().meta(readingMeta("none")),
66630
66479
  /**
66631
66480
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66632
66481
  *
@@ -66847,12 +66696,7 @@ var switchCapability = {
66847
66696
  label: "On",
66848
66697
  kind: "boolean"
66849
66698
  }
66850
- }] },
66851
- customize: {
66852
- version: 1,
66853
- advanced: "derive",
66854
- targets: []
66855
- }
66699
+ }] }
66856
66700
  };
66857
66701
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66858
66702
  function hasControlCharacter(value) {
@@ -67435,35 +67279,21 @@ var tamperCapability = {
67435
67279
  * entries with `device_class: temperature` and any future native
67436
67280
  * thermometer.
67437
67281
  *
67438
- * Unit normalisation: providers convert to Celsius before storing.
67439
- * The slice value is always Celsius so cross-cap aggregators
67440
- * (climate-control's `currentTemp`, energy analytics) can compose
67441
- * without per-source unit fixups. The canonical display unit (`°C`) is
67442
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67443
- * `sourceInfo`.
67282
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67283
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67444
67284
  *
67445
67285
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67446
67286
  * frozen feed (provider hung) distinct from a "temperature hasn't
67447
67287
  * changed" steady state.
67448
67288
  */
67449
67289
  var TemperatureSensorStatusSchema = object({
67450
- /** Current temperature in Celsius. */
67451
- celsius: number().meta({
67452
- unit: "°C",
67290
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67291
+ celsius: number().nullable().meta(readingMeta("temperature", {
67292
+ precision: 1,
67453
67293
  label: "Temperature"
67454
- }),
67294
+ })),
67455
67295
  /** Ms epoch when the slice was last updated (push or poll). */
67456
- lastFetchedAt: number(),
67457
- /** Live display unit from the upstream source (e.g. HA
67458
- * `attributes.unit_of_measurement`). The UI prefers this over the
67459
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67460
- * Absent → the UI falls back to the role's canonical unit. */
67461
- unit: string().optional(),
67462
- /** Suggested decimal places for numeric display.
67463
- * Populated live from the upstream source when provided (e.g. HA
67464
- * `attributes.suggested_display_precision`). Falls back to
67465
- * auto-formatting when absent. */
67466
- precision: number().int().min(0).max(10).optional()
67296
+ lastFetchedAt: number()
67467
67297
  });
67468
67298
  var temperatureSensorCapability = {
67469
67299
  name: "temperature-sensor",
@@ -67494,12 +67324,7 @@ var temperatureSensorCapability = {
67494
67324
  } } },
67495
67325
  /** Clock fields: written, but excluded from the compare that decides
67496
67326
  * whether persisting is worth a SQLite commit. */
67497
- volatileStateFields: ["lastFetchedAt"],
67498
- customize: {
67499
- version: 1,
67500
- advanced: "derive",
67501
- targets: []
67502
- }
67327
+ volatileStateFields: ["lastFetchedAt"]
67503
67328
  };
67504
67329
  /**
67505
67330
  * toast — system-scoped singleton capability that streams toast
@@ -67782,16 +67607,16 @@ var VacuumControlStatusSchema = object({
67782
67607
  ]),
67783
67608
  /** 0..100 battery percentage. Null when the device has no battery
67784
67609
  * reading. */
67785
- batteryLevel: number().min(0).max(100).nullable(),
67610
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67786
67611
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67787
67612
  * the vacuum has no speed control. */
67788
67613
  fanSpeed: string().nullable(),
67789
67614
  /** Speed tokens the hardware accepts — drives the UI selector. */
67790
67615
  availableFanSpeeds: array(string()),
67791
67616
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67792
- progressPercent: number().min(0).max(100).nullable(),
67617
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67793
67618
  /** Current error code (0 / null = no error). */
67794
- errorCode: number().nullable(),
67619
+ errorCode: number().nullable().meta(readingMeta("none")),
67795
67620
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67796
67621
  errorLabel: string().nullable(),
67797
67622
  /** Ms epoch when the slice was last updated. */
@@ -67869,7 +67694,7 @@ var ValveStatusSchema = object({
67869
67694
  ]),
67870
67695
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67871
67696
  * intermediate position surface. */
67872
- position: number().min(0).max(100).nullable(),
67697
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67873
67698
  /** Ms epoch when the slice was last updated. */
67874
67699
  lastChangedAt: number()
67875
67700
  });
@@ -67977,9 +67802,9 @@ var vibrationCapability = {
67977
67802
  */
67978
67803
  var WaterHeaterStatusSchema = object({
67979
67804
  /** Current measured temperature. Null when not reported. */
67980
- currentTemp: number().nullable(),
67805
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67981
67806
  /** Target temperature setpoint. Null when no setpoint surface. */
67982
- targetTemp: number().nullable(),
67807
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67983
67808
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67984
67809
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67985
67810
  * device reports an unknown state. */
@@ -67990,9 +67815,9 @@ var WaterHeaterStatusSchema = object({
67990
67815
  * has no away surface. */
67991
67816
  away: boolean().nullable(),
67992
67817
  /** HA `min_temp` attribute. Null when not reported. */
67993
- minTemp: number().nullable(),
67818
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67994
67819
  /** HA `max_temp` attribute. Null when not reported. */
67995
- maxTemp: number().nullable(),
67820
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67996
67821
  /** Ms epoch when the slice was last updated. */
67997
67822
  lastChangedAt: number()
67998
67823
  });
@@ -68077,25 +67902,18 @@ var waterHeaterCapability = {
68077
67902
  * Forecast deferred — current conditions only for v1.
68078
67903
  */
68079
67904
  var WeatherStatusSchema = object({
68080
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68081
- * when no condition has been reported yet. */
67905
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68082
67906
  condition: string().nullable(),
68083
- /** Current temperature in the reported unit. Null when not provided. */
68084
- temperature: number().nullable(),
68085
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68086
- temperatureUnit: string().nullable(),
67907
+ /** Current temperature in Celsius. Null when not provided or refused. */
67908
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68087
67909
  /** Relative humidity (0..100). Null when not provided. */
68088
- humidity: number().min(0).max(100).nullable(),
68089
- /** Barometric pressure in the reported unit. Null when not provided. */
68090
- pressure: number().nullable(),
68091
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68092
- pressureUnit: string().nullable(),
68093
- /** Wind speed in the reported unit. Null when not provided. */
68094
- windSpeed: number().nullable(),
68095
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68096
- windSpeedUnit: string().nullable(),
67910
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
67911
+ /** Barometric pressure in hPa. Null when not provided or refused. */
67912
+ pressure: number().nullable().meta(readingMeta("pressure")),
67913
+ /** Wind speed in metres per second. Null when not provided or refused. */
67914
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68097
67915
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68098
- windBearing: number().nullable(),
67916
+ windBearing: number().nullable().meta(readingMeta("angle")),
68099
67917
  /** Ms epoch when the slice was last updated. */
68100
67918
  lastFetchedAt: number()
68101
67919
  });
@@ -68123,12 +67941,12 @@ var weatherCapability = {
68123
67941
  */
68124
67942
  durability: "session",
68125
67943
  /**
68126
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
67944
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68127
67945
  */
68128
67946
  history: { fields: {
68129
67947
  temperature: {
68130
67948
  kind: "gauge",
68131
- unitFrom: "temperatureUnit",
67949
+ unit: "°C",
68132
67950
  deadband: .2
68133
67951
  },
68134
67952
  humidity: {
@@ -68138,12 +67956,12 @@ var weatherCapability = {
68138
67956
  },
68139
67957
  pressure: {
68140
67958
  kind: "gauge",
68141
- unitFrom: "pressureUnit",
67959
+ unit: "hPa",
68142
67960
  deadband: .5
68143
67961
  },
68144
67962
  windSpeed: {
68145
67963
  kind: "gauge",
68146
- unitFrom: "windSpeedUnit",
67964
+ unit: "m/s",
68147
67965
  deadband: .5
68148
67966
  }
68149
67967
  } }
@@ -80332,7 +80150,7 @@ function t(en, it) {
80332
80150
  * covers, valves, locks, climate, fans, alarm panels.
80333
80151
  */
80334
80152
  var ON = t("On", "Acceso");
80335
- var PERCENT$1 = t("Percentage", "Percentuale");
80153
+ var PERCENT = t("Percentage", "Percentuale");
80336
80154
  var OPEN_CLOSE_STATES = [
80337
80155
  {
80338
80156
  value: "open",
@@ -80560,7 +80378,7 @@ var ACTUATOR_CAP_VOCABULARY = {
80560
80378
  icon: "sun-medium",
80561
80379
  openArgs: [{
80562
80380
  key: "percentage",
80563
- label: PERCENT$1,
80381
+ label: PERCENT,
80564
80382
  offer: {
80565
80383
  kind: "number",
80566
80384
  min: 0,
@@ -83713,7 +83531,6 @@ var SENSOR_CAP_VOCABULARY = {
83713
83531
  field: "value",
83714
83532
  quantity: t("the reading", "il valore"),
83715
83533
  label: t("Reading", "Valore"),
83716
- unitField: "unit",
83717
83534
  icon: "gauge"
83718
83535
  }]
83719
83536
  },
@@ -83849,7 +83666,7 @@ var SENSOR_CAP_VOCABULARY = {
83849
83666
  field: "temperature",
83850
83667
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83851
83668
  label: t("Temperature", "Temperatura"),
83852
- unitField: "temperatureUnit",
83669
+ unit: "°C",
83853
83670
  icon: "thermometer"
83854
83671
  },
83855
83672
  {
@@ -83865,7 +83682,7 @@ var SENSOR_CAP_VOCABULARY = {
83865
83682
  field: "windSpeed",
83866
83683
  quantity: t("the wind speed", "la velocità del vento"),
83867
83684
  label: t("Wind", "Vento"),
83868
- unitField: "windSpeedUnit",
83685
+ unit: "m/s",
83869
83686
  icon: "wind"
83870
83687
  },
83871
83688
  {
@@ -83873,7 +83690,7 @@ var SENSOR_CAP_VOCABULARY = {
83873
83690
  field: "pressure",
83874
83691
  quantity: t("the air pressure", "la pressione atmosferica"),
83875
83692
  label: t("Pressure", "Pressione"),
83876
- unitField: "pressureUnit",
83693
+ unit: "hPa",
83877
83694
  icon: "gauge"
83878
83695
  }
83879
83696
  ]
@@ -88764,17 +88581,15 @@ function entityIdentity(descriptor) {
88764
88581
  function slugifyKey(value) {
88765
88582
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
88766
88583
  }
88767
- var PERCENT = "%";
88768
- var WATT = "W";
88769
88584
  /** Clamp a raw percentage into 0..100 (defensive — a hung feed can overshoot). */
88770
88585
  function pct(value) {
88771
88586
  if (!Number.isFinite(value)) return 0;
88772
88587
  return Math.max(0, Math.min(100, value));
88773
88588
  }
88774
- /** Bytes → whole GiB, rounded to 1 decimal (array/disk capacity display). */
88775
- function toGiB(bytes) {
88776
- if (!Number.isFinite(bytes) || bytes <= 0) return 0;
88777
- return Math.round(bytes / 1024 ** 3 * 10) / 10;
88589
+ /** A byte count. Negative or non-finite input is an empty reading, not a guess. */
88590
+ function bytes(value) {
88591
+ if (!Number.isFinite(value) || value < 0) return 0;
88592
+ return value;
88778
88593
  }
88779
88594
  /** Array capacity + parity + CPU + notification-count metric descriptors. */
88780
88595
  function arrayMetricDescriptors(snapshot) {
@@ -88801,8 +88616,7 @@ function arrayMetricDescriptors(snapshot) {
88801
88616
  role: DeviceRole.NumericSensor,
88802
88617
  numeric: {
88803
88618
  value: usagePercent,
88804
- unit: PERCENT,
88805
- precision: 1
88619
+ quantity: "percentage"
88806
88620
  }
88807
88621
  },
88808
88622
  {
@@ -88813,9 +88627,8 @@ function arrayMetricDescriptors(snapshot) {
88813
88627
  name: "Array total",
88814
88628
  role: DeviceRole.NumericSensor,
88815
88629
  numeric: {
88816
- value: toGiB(totalKb * 1024),
88817
- unit: "GiB",
88818
- precision: 1
88630
+ value: bytes(totalKb * 1024),
88631
+ quantity: "information"
88819
88632
  }
88820
88633
  },
88821
88634
  {
@@ -88826,9 +88639,8 @@ function arrayMetricDescriptors(snapshot) {
88826
88639
  name: "Array used",
88827
88640
  role: DeviceRole.NumericSensor,
88828
88641
  numeric: {
88829
- value: toGiB(usedKb * 1024),
88830
- unit: "GiB",
88831
- precision: 1
88642
+ value: bytes(usedKb * 1024),
88643
+ quantity: "information"
88832
88644
  }
88833
88645
  },
88834
88646
  {
@@ -88839,9 +88651,8 @@ function arrayMetricDescriptors(snapshot) {
88839
88651
  name: "Array free",
88840
88652
  role: DeviceRole.NumericSensor,
88841
88653
  numeric: {
88842
- value: toGiB(array.capacityKb.free * 1024),
88843
- unit: "GiB",
88844
- precision: 1
88654
+ value: bytes(array.capacityKb.free * 1024),
88655
+ quantity: "information"
88845
88656
  }
88846
88657
  },
88847
88658
  {
@@ -88853,8 +88664,7 @@ function arrayMetricDescriptors(snapshot) {
88853
88664
  role: DeviceRole.NumericSensor,
88854
88665
  numeric: {
88855
88666
  value: pct(cpu.percent),
88856
- unit: PERCENT,
88857
- precision: 1
88667
+ quantity: "percentage"
88858
88668
  }
88859
88669
  },
88860
88670
  {
@@ -88866,8 +88676,7 @@ function arrayMetricDescriptors(snapshot) {
88866
88676
  role: DeviceRole.NumericSensor,
88867
88677
  numeric: {
88868
88678
  value: pct(array.parityCheck?.progress ?? 0),
88869
- unit: PERCENT,
88870
- precision: 0
88679
+ quantity: "percentage"
88871
88680
  }
88872
88681
  },
88873
88682
  {
@@ -88879,7 +88688,7 @@ function arrayMetricDescriptors(snapshot) {
88879
88688
  role: DeviceRole.NumericSensor,
88880
88689
  numeric: {
88881
88690
  value: Math.max(0, notifications.activeCount),
88882
- precision: 0
88691
+ quantity: "count"
88883
88692
  }
88884
88693
  }
88885
88694
  ];
@@ -88901,8 +88710,7 @@ function arrayMetricDescriptors(snapshot) {
88901
88710
  role: DeviceRole.PowerSensor,
88902
88711
  numeric: {
88903
88712
  value: Math.max(0, cpu.powerW),
88904
- unit: WATT,
88905
- precision: 1
88713
+ quantity: "power"
88906
88714
  }
88907
88715
  });
88908
88716
  return out;
@@ -88986,8 +88794,7 @@ function diskDescriptors(disks) {
88986
88794
  role: DeviceRole.NumericSensor,
88987
88795
  numeric: {
88988
88796
  value: usagePercent,
88989
- unit: PERCENT,
88990
- precision: 1
88797
+ quantity: "percentage"
88991
88798
  }
88992
88799
  });
88993
88800
  out.push({
@@ -89026,8 +88833,7 @@ function dockerDescriptors(containers) {
89026
88833
  role: DeviceRole.NumericSensor,
89027
88834
  numeric: {
89028
88835
  value: pct(container.cpuPercent),
89029
- unit: PERCENT,
89030
- precision: 1
88836
+ quantity: "percentage"
89031
88837
  }
89032
88838
  });
89033
88839
  out.push({
@@ -89039,8 +88845,7 @@ function dockerDescriptors(containers) {
89039
88845
  role: DeviceRole.NumericSensor,
89040
88846
  numeric: {
89041
88847
  value: pct(container.memPercent),
89042
- unit: PERCENT,
89043
- precision: 1
88848
+ quantity: "percentage"
89044
88849
  }
89045
88850
  });
89046
88851
  out.push(buttonDescriptor(`restart_container_${slug}`, `Restart ${container.name}`, {
@@ -89222,10 +89027,9 @@ var UnraidEntityDevice = class extends BaseDevice {
89222
89027
  if (numeric === void 0) return;
89223
89028
  const slice = {
89224
89029
  value: numeric.value,
89225
- lastFetchedAt: now,
89226
- ...numeric.unit !== void 0 ? { unit: numeric.unit } : {},
89227
- ...numeric.precision !== void 0 ? { precision: numeric.precision } : {}
89030
+ lastFetchedAt: now
89228
89031
  };
89032
+ if (this.sourceInfo.quantity !== numeric.quantity) this.updateSourceInfo({ quantity: numeric.quantity });
89229
89033
  this.runtimeState.setCapState(numericSensorCapability.name, slice);
89230
89034
  return;
89231
89035
  }