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