@camstack/addon-post-analysis 1.2.636 → 1.2.637

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.
@@ -5663,7 +5663,7 @@ var ZodIssueCode = {
5663
5663
  var ZodFirstPartyTypeKind;
5664
5664
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5665
5665
  //#endregion
5666
- //#region ../types/dist/sleep-BErAxhul.mjs
5666
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5667
5667
  /**
5668
5668
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5669
5669
  * window to float samples (D455).
@@ -8363,6 +8363,52 @@ function defineRecordKinds(specs) {
8363
8363
  }
8364
8364
  return out;
8365
8365
  }
8366
+ var READING_QUANTITIES = [
8367
+ "temperature",
8368
+ "pressure",
8369
+ "speed",
8370
+ "precipitation",
8371
+ "precipitation-rate",
8372
+ "length",
8373
+ "area",
8374
+ "illuminance",
8375
+ "irradiance",
8376
+ "power",
8377
+ "energy",
8378
+ "voltage",
8379
+ "current",
8380
+ "apparent-power",
8381
+ "humidity",
8382
+ "percentage",
8383
+ "concentration-volume",
8384
+ "concentration-mass",
8385
+ "concentration-ppb",
8386
+ "volume",
8387
+ "duration",
8388
+ "duration-ms",
8389
+ "information",
8390
+ "sound-level",
8391
+ "signal-level",
8392
+ "colour-temperature",
8393
+ "angle",
8394
+ "bitrate",
8395
+ "rate",
8396
+ "mass",
8397
+ "index",
8398
+ "ratio",
8399
+ "count",
8400
+ "none",
8401
+ "device"
8402
+ ];
8403
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8404
+ new Set(READING_QUANTITIES);
8405
+ function readingMeta(quantity, extras) {
8406
+ return {
8407
+ quantity,
8408
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8409
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8410
+ };
8411
+ }
8366
8412
  /** Shared Zod schemas used across streaming capabilities. */
8367
8413
  var CamProfileSchema = _enum([
8368
8414
  "high",
@@ -8390,8 +8436,8 @@ var CamStreamKindSchema = _enum([
8390
8436
  "derived"
8391
8437
  ]);
8392
8438
  var CamStreamResolutionSchema = object({
8393
- width: number().int().positive(),
8394
- height: number().int().positive()
8439
+ width: number().int().positive().meta(readingMeta("none")),
8440
+ height: number().int().positive().meta(readingMeta("none"))
8395
8441
  });
8396
8442
  var CameraStreamSchema = object({
8397
8443
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9120,14 +9166,6 @@ Object.freeze({
9120
9166
  {
9121
9167
  "path": "aqi",
9122
9168
  "valueType": "number"
9123
- },
9124
- {
9125
- "path": "unit",
9126
- "valueType": "string"
9127
- },
9128
- {
9129
- "path": "precision",
9130
- "valueType": "number"
9131
9169
  }
9132
9170
  ],
9133
9171
  "alarm-panel": [
@@ -9163,20 +9201,10 @@ Object.freeze({
9163
9201
  "valueType": "boolean"
9164
9202
  }
9165
9203
  ],
9166
- "ambient-light-sensor": [
9167
- {
9168
- "path": "lux",
9169
- "valueType": "number"
9170
- },
9171
- {
9172
- "path": "unit",
9173
- "valueType": "string"
9174
- },
9175
- {
9176
- "path": "precision",
9177
- "valueType": "number"
9178
- }
9179
- ],
9204
+ "ambient-light-sensor": [{
9205
+ "path": "lux",
9206
+ "valueType": "number"
9207
+ }],
9180
9208
  "audio-metrics": [
9181
9209
  {
9182
9210
  "path": "ts",
@@ -9256,6 +9284,10 @@ Object.freeze({
9256
9284
  "path": "lastUpdated",
9257
9285
  "valueType": "number"
9258
9286
  },
9287
+ {
9288
+ "path": "volts",
9289
+ "valueType": "number"
9290
+ },
9259
9291
  {
9260
9292
  "path": "lastContactAt",
9261
9293
  "valueType": "number"
@@ -9498,10 +9530,6 @@ Object.freeze({
9498
9530
  "datetime"
9499
9531
  ]
9500
9532
  },
9501
- {
9502
- "path": "unit",
9503
- "valueType": "string"
9504
- },
9505
9533
  {
9506
9534
  "path": "min",
9507
9535
  "valueType": "number"
@@ -9513,19 +9541,6 @@ Object.freeze({
9513
9541
  {
9514
9542
  "path": "step",
9515
9543
  "valueType": "number"
9516
- },
9517
- {
9518
- "path": "precision",
9519
- "valueType": "number"
9520
- },
9521
- {
9522
- "path": "format",
9523
- "valueType": "enum",
9524
- "enumValues": [
9525
- "date",
9526
- "time",
9527
- "datetime"
9528
- ]
9529
9544
  }
9530
9545
  ],
9531
9546
  "cover": [
@@ -9766,13 +9781,8 @@ Object.freeze({
9766
9781
  "path": "value",
9767
9782
  "valueType": "string"
9768
9783
  }, {
9769
- "path": "format",
9770
- "valueType": "enum",
9771
- "enumValues": [
9772
- "date",
9773
- "time",
9774
- "datetime"
9775
- ]
9784
+ "path": "timestamp",
9785
+ "valueType": "boolean"
9776
9786
  }],
9777
9787
  "event-emitter": [
9778
9788
  {
@@ -9879,20 +9889,10 @@ Object.freeze({
9879
9889
  "valueType": "number"
9880
9890
  }
9881
9891
  ],
9882
- "humidity-sensor": [
9883
- {
9884
- "path": "percent",
9885
- "valueType": "number"
9886
- },
9887
- {
9888
- "path": "unit",
9889
- "valueType": "string"
9890
- },
9891
- {
9892
- "path": "precision",
9893
- "valueType": "number"
9894
- }
9895
- ],
9892
+ "humidity-sensor": [{
9893
+ "path": "percent",
9894
+ "valueType": "number"
9895
+ }],
9896
9896
  "image": [{
9897
9897
  "path": "url",
9898
9898
  "valueType": "string"
@@ -10231,20 +10231,10 @@ Object.freeze({
10231
10231
  "valueType": "boolean"
10232
10232
  }
10233
10233
  ],
10234
- "numeric-sensor": [
10235
- {
10236
- "path": "value",
10237
- "valueType": "number"
10238
- },
10239
- {
10240
- "path": "unit",
10241
- "valueType": "string"
10242
- },
10243
- {
10244
- "path": "precision",
10245
- "valueType": "number"
10246
- }
10247
- ],
10234
+ "numeric-sensor": [{
10235
+ "path": "value",
10236
+ "valueType": "number"
10237
+ }],
10248
10238
  "pet-feeder": [
10249
10239
  {
10250
10240
  "path": "foodLevel",
@@ -10336,14 +10326,6 @@ Object.freeze({
10336
10326
  {
10337
10327
  "path": "powerFactor",
10338
10328
  "valueType": "number"
10339
- },
10340
- {
10341
- "path": "unit",
10342
- "valueType": "string"
10343
- },
10344
- {
10345
- "path": "precision",
10346
- "valueType": "number"
10347
10329
  }
10348
10330
  ],
10349
10331
  "presence": [
@@ -10372,20 +10354,10 @@ Object.freeze({
10372
10354
  "valueType": "number"
10373
10355
  }
10374
10356
  ],
10375
- "pressure-sensor": [
10376
- {
10377
- "path": "hpa",
10378
- "valueType": "number"
10379
- },
10380
- {
10381
- "path": "unit",
10382
- "valueType": "string"
10383
- },
10384
- {
10385
- "path": "precision",
10386
- "valueType": "number"
10387
- }
10388
- ],
10357
+ "pressure-sensor": [{
10358
+ "path": "hpa",
10359
+ "valueType": "number"
10360
+ }],
10389
10361
  "privacy-mask": [{
10390
10362
  "path": "enabled",
10391
10363
  "valueType": "boolean"
@@ -10696,20 +10668,10 @@ Object.freeze({
10696
10668
  "path": "tampered",
10697
10669
  "valueType": "boolean"
10698
10670
  }],
10699
- "temperature-sensor": [
10700
- {
10701
- "path": "celsius",
10702
- "valueType": "number"
10703
- },
10704
- {
10705
- "path": "unit",
10706
- "valueType": "string"
10707
- },
10708
- {
10709
- "path": "precision",
10710
- "valueType": "number"
10711
- }
10712
- ],
10671
+ "temperature-sensor": [{
10672
+ "path": "celsius",
10673
+ "valueType": "number"
10674
+ }],
10713
10675
  "update": [
10714
10676
  {
10715
10677
  "path": "currentVersion",
@@ -10820,10 +10782,6 @@ Object.freeze({
10820
10782
  "path": "temperature",
10821
10783
  "valueType": "number"
10822
10784
  },
10823
- {
10824
- "path": "temperatureUnit",
10825
- "valueType": "string"
10826
- },
10827
10785
  {
10828
10786
  "path": "humidity",
10829
10787
  "valueType": "number"
@@ -10832,18 +10790,10 @@ Object.freeze({
10832
10790
  "path": "pressure",
10833
10791
  "valueType": "number"
10834
10792
  },
10835
- {
10836
- "path": "pressureUnit",
10837
- "valueType": "string"
10838
- },
10839
10793
  {
10840
10794
  "path": "windSpeed",
10841
10795
  "valueType": "number"
10842
10796
  },
10843
- {
10844
- "path": "windSpeedUnit",
10845
- "valueType": "string"
10846
- },
10847
10797
  {
10848
10798
  "path": "windBearing",
10849
10799
  "valueType": "number"
@@ -19391,7 +19341,8 @@ var CompositionProblemSchema = object({
19391
19341
  "item-field-reserved",
19392
19342
  "target-missing",
19393
19343
  "target-unreadable",
19394
- "not-replaceable"
19344
+ "not-replaceable",
19345
+ "native-capability-replace"
19395
19346
  ]),
19396
19347
  path: string(),
19397
19348
  message: string()
@@ -19449,9 +19400,11 @@ var CompositionFieldPlanSchema = object({
19449
19400
  reads: array(CompositionFieldReadSchema)
19450
19401
  });
19451
19402
  /**
19452
- * What a feature does to its target (D663): `new` builds a capability on a new
19453
- * device; on an existing device it `add`s a capability the device lacks, or
19454
- * `replace`s named fields of one it has natively.
19403
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19404
+ * new device; on an existing device it `add`s a capability the device lacks.
19405
+ * Replacing a capability the device already serves is refused
19406
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19407
+ * wire for a claim the hub already holds.
19455
19408
  */
19456
19409
  var CompositionFeatureModeSchema = _enum([
19457
19410
  "new",
@@ -21344,12 +21297,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
21344
21297
  * providers detect renames by matching `uniqueId` and patch `id`
21345
21298
  * in-place so dispatch keeps working.
21346
21299
  *
21347
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
21348
- * `precision`) live in the cap STATUS SLICE so they flow live with every
21349
- * state push and are the single source of truth. The `device_class` tag is
21350
- * an HA-specific input that is consumed at the discovery boundary
21351
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
21352
- * stored after that point.
21300
+ * **Display unit and precision are NOT stored here.** They are a read-time
21301
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21302
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21303
+ * neither is repeated on each sample. The `device_class` tag is an
21304
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21305
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
21353
21306
  *
21354
21307
  * The `raw` escape hatch carries provider-specific attributes that don't
21355
21308
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -21363,6 +21316,14 @@ var SourceInfoSchema = object({
21363
21316
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
21364
21317
  * Used to detect rename when `id` changes. */
21365
21318
  uniqueId: string().optional(),
21319
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21320
+ quantity: ReadingQuantitySchema.optional(),
21321
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21322
+ dateTimeShape: _enum([
21323
+ "date",
21324
+ "time",
21325
+ "datetime"
21326
+ ]).optional(),
21366
21327
  /** Provider-specific extras that don't fit the structured slots.
21367
21328
  * Whitelist what goes here per provider — do NOT dump entire HA
21368
21329
  * attribute blobs (would explode the meta JSON column). */
@@ -42390,7 +42351,8 @@ method(object({
42390
42351
  "enum"
42391
42352
  ]),
42392
42353
  enumValues: array(string()).optional(),
42393
- item: boolean().optional()
42354
+ item: boolean().optional(),
42355
+ quantity: ReadingQuantitySchema.optional()
42394
42356
  })).readonly(),
42395
42357
  itemArray: object({
42396
42358
  path: string(),
@@ -46457,8 +46419,8 @@ var MaskPolygonShapeSchema = object({
46457
46419
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
46458
46420
  var MaskGridShapeSchema = object({
46459
46421
  kind: literal("grid"),
46460
- gridWidth: number(),
46461
- gridHeight: number(),
46422
+ gridWidth: number().meta(readingMeta("none")),
46423
+ gridHeight: number().meta(readingMeta("none")),
46462
46424
  cells: array(boolean())
46463
46425
  });
46464
46426
  discriminatedUnion("kind", [
@@ -59784,27 +59746,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
59784
59746
  */
59785
59747
  var AirQualitySensorStatusSchema = object({
59786
59748
  /** Carbon dioxide concentration in ppm. */
59787
- co2Ppm: number().min(0).optional(),
59749
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
59788
59750
  /** Total volatile organic compounds in ppb. */
59789
- vocPpb: number().min(0).optional(),
59751
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
59790
59752
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
59791
- pm25: number().min(0).optional(),
59753
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
59792
59754
  /** Particulate matter ≤ 10 μm in µg/m³. */
59793
- pm10: number().min(0).optional(),
59755
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
59794
59756
  /** Composite AQI value (typically 0..500). */
59795
- aqi: number().optional(),
59757
+ aqi: number().optional().meta(readingMeta("index")),
59796
59758
  /** Ms epoch when the slice was last updated. */
59797
- lastFetchedAt: number(),
59798
- /** Live display unit of the single metric this slice carries (e.g. HA
59799
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
59800
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
59801
- * per slice is unambiguous. */
59802
- unit: string().optional(),
59803
- /** Suggested decimal places for numeric display.
59804
- * Populated live from the upstream source when provided (e.g. HA
59805
- * `attributes.suggested_display_precision`). Falls back to
59806
- * auto-formatting when absent. */
59807
- precision: number().int().min(0).max(10).optional()
59759
+ lastFetchedAt: number()
59808
59760
  });
59809
59761
  var airQualitySensorCapability = {
59810
59762
  name: "air-quality-sensor",
@@ -59841,12 +59793,12 @@ var airQualitySensorCapability = {
59841
59793
  },
59842
59794
  pm25: {
59843
59795
  kind: "gauge",
59844
- unit: "ug/m3",
59796
+ unit: "µg/m³",
59845
59797
  deadband: 1
59846
59798
  },
59847
59799
  pm10: {
59848
59800
  kind: "gauge",
59849
- unit: "ug/m3",
59801
+ unit: "µg/m³",
59850
59802
  deadband: 1
59851
59803
  },
59852
59804
  aqi: {
@@ -59864,22 +59816,13 @@ var airQualitySensorCapability = {
59864
59816
  * entries with `device_class: illuminance`.
59865
59817
  */
59866
59818
  var AmbientLightSensorStatusSchema = object({
59867
- /** Current illuminance in lux (lx). */
59868
- lux: number().min(0).meta({
59869
- unit: "lx",
59819
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
59820
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
59821
+ precision: 0,
59870
59822
  label: "Illuminance"
59871
- }),
59823
+ })),
59872
59824
  /** Ms epoch when the slice was last updated. */
59873
- lastFetchedAt: number(),
59874
- /** Live display unit from the upstream source (e.g. HA
59875
- * `attributes.unit_of_measurement`). The UI prefers this over the
59876
- * role's canonical unit. Absent → fall back to the canonical unit. */
59877
- unit: string().optional(),
59878
- /** Suggested decimal places for numeric display.
59879
- * Populated live from the upstream source when provided (e.g. HA
59880
- * `attributes.suggested_display_precision`). Falls back to
59881
- * auto-formatting when absent. */
59882
- precision: number().int().min(0).max(10).optional()
59825
+ lastFetchedAt: number()
59883
59826
  });
59884
59827
  var ambientLightSensorCapability = {
59885
59828
  name: "ambient-light-sensor",
@@ -59911,12 +59854,7 @@ var ambientLightSensorCapability = {
59911
59854
  } } },
59912
59855
  /** Clock fields: written, but excluded from the compare that decides
59913
59856
  * whether persisting is worth a SQLite commit. */
59914
- volatileStateFields: ["lastFetchedAt"],
59915
- customize: {
59916
- version: 1,
59917
- advanced: "derive",
59918
- targets: []
59919
- }
59857
+ volatileStateFields: ["lastFetchedAt"]
59920
59858
  };
59921
59859
  /**
59922
59860
  * Per-class audio metrics aggregated over a sliding window.
@@ -60143,10 +60081,7 @@ var BatteryStatusSchema = object({
60143
60081
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
60144
60082
  * declines to band a non-finite reading, which is the correct shape.
60145
60083
  */
60146
- percentage: number().min(0).max(100).nullable().meta({
60147
- unit: "%",
60148
- label: "Charge"
60149
- }),
60084
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
60150
60085
  /**
60151
60086
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
60152
60087
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -60173,7 +60108,14 @@ var BatteryStatusSchema = object({
60173
60108
  */
60174
60109
  sleeping: boolean().meta({ label: "Sleeping" }),
60175
60110
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60176
- lastUpdated: number(),
60111
+ lastUpdated: number().meta(readingMeta("none")),
60112
+ /**
60113
+ * Battery voltage in volts, for a cell the firmware reports as voltage
60114
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
60115
+ * that only report charge. A second reading on this cap, not a second
60116
+ * `numeric-sensor` on the same device.
60117
+ */
60118
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
60177
60119
  /**
60178
60120
  * Ms epoch of the last time the device PROVED it was reachable — a
60179
60121
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -60225,7 +60167,6 @@ var BATTERY_DEVICE_TYPES = [
60225
60167
  /** Generic Customize target for the percentage reading (D936). */
60226
60168
  var batteryCustomize = {
60227
60169
  version: 1,
60228
- advanced: "derive",
60229
60170
  targets: [{
60230
60171
  id: "percentage",
60231
60172
  cardinality: "scalar",
@@ -60234,7 +60175,7 @@ var batteryCustomize = {
60234
60175
  stateField: "percentage"
60235
60176
  },
60236
60177
  deviceTypes: BATTERY_DEVICE_TYPES,
60237
- apply: "add-or-replace",
60178
+ apply: "add",
60238
60179
  path: "percentage",
60239
60180
  sources: [{
60240
60181
  kind: "field",
@@ -60355,10 +60296,16 @@ onStatusChanged: { data: object({
60355
60296
  /**
60356
60297
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
60357
60298
  */
60358
- history: { fields: { percentage: {
60359
- kind: "gauge",
60360
- unit: "%"
60361
- } } },
60299
+ history: { fields: {
60300
+ percentage: {
60301
+ kind: "gauge",
60302
+ unit: "%"
60303
+ },
60304
+ volts: {
60305
+ kind: "gauge",
60306
+ unit: "V"
60307
+ }
60308
+ } },
60362
60309
  customize: batteryCustomize,
60363
60310
  /** Clock fields: written, but excluded from the compare that decides
60364
60311
  * whether persisting is worth a SQLite commit. */
@@ -60405,12 +60352,7 @@ var binaryCapability = {
60405
60352
  history: { fields: { on: { kind: "boolean" } } },
60406
60353
  /** Clock fields: written, but excluded from the compare that decides
60407
60354
  * whether persisting is worth a SQLite commit. */
60408
- volatileStateFields: ["lastChangedAt"],
60409
- customize: {
60410
- version: 1,
60411
- advanced: "derive",
60412
- targets: []
60413
- }
60355
+ volatileStateFields: ["lastChangedAt"]
60414
60356
  };
60415
60357
  /**
60416
60358
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -60426,7 +60368,7 @@ var binaryCapability = {
60426
60368
  */
60427
60369
  var BrightnessStatusSchema = object({
60428
60370
  /** Current level as 0..100 inclusive. Firmware-reported. */
60429
- percentage: number().min(0).max(100),
60371
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
60430
60372
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
60431
60373
  lastChangedAt: number()
60432
60374
  });
@@ -60907,21 +60849,21 @@ var ClimateControlStatusSchema = object({
60907
60849
  /** Single setpoint in Celsius. Used by single-target modes
60908
60850
  * (heat / cool / dry). Null when the device is in a range mode
60909
60851
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
60910
- target: number().nullable(),
60852
+ target: number().nullable().meta(readingMeta("temperature")),
60911
60853
  /** Upper bound of the dual setpoint in Celsius. Populated only
60912
60854
  * in `heat_cool` mode. */
60913
- targetHigh: number().nullable(),
60855
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
60914
60856
  /** Lower bound of the dual setpoint in Celsius. Populated only
60915
60857
  * in `heat_cool` mode. */
60916
- targetLow: number().nullable(),
60858
+ targetLow: number().nullable().meta(readingMeta("temperature")),
60917
60859
  /** Target relative humidity (0..100). Null when the device has no
60918
60860
  * humidity control. */
60919
- targetHumidity: number().min(0).max(100).nullable(),
60861
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60920
60862
  /** Current measured temperature in Celsius. Read-only — pushed by
60921
60863
  * the device's internal sensor. */
60922
- currentTemp: number().nullable(),
60864
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
60923
60865
  /** Current measured relative humidity (0..100). Read-only. */
60924
- currentHumidity: number().min(0).max(100).nullable(),
60866
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60925
60867
  /** Vertical louver swing on/off. Null when the device has no vertical
60926
60868
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
60927
60869
  swingVertical: boolean().nullable(),
@@ -60929,11 +60871,11 @@ var ClimateControlStatusSchema = object({
60929
60871
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
60930
60872
  swingHorizontal: boolean().nullable(),
60931
60873
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
60932
- minTemp: number().nullable().optional(),
60874
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
60933
60875
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
60934
- maxTemp: number().nullable().optional(),
60876
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
60935
60877
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
60936
- tempStep: number().positive().nullable().optional(),
60878
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
60937
60879
  /** Ms epoch when the slice was last updated (push or command). */
60938
60880
  lastFetchedAt: number()
60939
60881
  });
@@ -61075,17 +61017,17 @@ var climateControlCapability = {
61075
61017
  * modes to expose in the picker without a slice fetch.
61076
61018
  */
61077
61019
  var RgbTripletSchema = object({
61078
- r: number().int().min(0).max(255),
61079
- g: number().int().min(0).max(255),
61080
- b: number().int().min(0).max(255)
61020
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
61021
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
61022
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
61081
61023
  });
61082
61024
  var HsvTripletSchema = object({
61083
61025
  /** Hue in degrees, 0..360. */
61084
- h: number().min(0).max(360),
61026
+ h: number().min(0).max(360).meta(readingMeta("none")),
61085
61027
  /** Saturation as 0..100 inclusive. */
61086
- s: number().min(0).max(100),
61028
+ s: number().min(0).max(100).meta(readingMeta("none")),
61087
61029
  /** Value/brightness as 0..100 inclusive. */
61088
- v: number().min(0).max(100)
61030
+ v: number().min(0).max(100).meta(readingMeta("none"))
61089
61031
  });
61090
61032
  var ColorInputSchema = discriminatedUnion("mode", [
61091
61033
  object({
@@ -61114,7 +61056,7 @@ var ColorStatusSchema = object({
61114
61056
  /** Populated when `mode === 'hsv'`. */
61115
61057
  hsv: HsvTripletSchema.optional(),
61116
61058
  /** Populated when `mode === 'mired'`. */
61117
- mireds: number().int().optional(),
61059
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
61118
61060
  /** Ms epoch of the last operator-driven change. */
61119
61061
  lastChangedAt: number()
61120
61062
  });
@@ -61323,18 +61265,12 @@ var FORM_OF_KIND = {
61323
61265
  function consumableFormOf(item) {
61324
61266
  return item.form ?? FORM_OF_KIND[consumableKindOf(item)];
61325
61267
  }
61326
- /** Units a remaining-life counter is reported in. Closed: the panel renders
61327
- * each. */
61328
- var ConsumableRemainingUnitSchema = _enum([
61329
- "days",
61330
- "hours",
61331
- "cycles"
61332
- ]);
61333
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
61334
- * days). `value` null = unknown (D393). */
61268
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
61269
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
61270
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
61335
61271
  var ConsumableRemainingSchema = object({
61336
- value: number().min(0).nullable(),
61337
- unit: ConsumableRemainingUnitSchema
61272
+ value: number().min(0).nullable().meta(readingMeta("device")),
61273
+ kind: ConsumableRemainingKindSchema
61338
61274
  });
61339
61275
  /** A single consumable item. Either a continuous `level` (remaining
61340
61276
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -61347,7 +61283,7 @@ var ConsumableItemSchema = object({
61347
61283
  /** Display name. */
61348
61284
  label: string().min(1),
61349
61285
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
61350
- level: number().min(0).max(100).nullable(),
61286
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61351
61287
  /** The provider's discrete verdict when known (binary mode). */
61352
61288
  status: ConsumableStatusSchema.nullable(),
61353
61289
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -61429,8 +61365,8 @@ var consumablesCustomize = {
61429
61365
  transforms: [{ kind: "identity" }]
61430
61366
  }],
61431
61367
  literals: [{
61432
- path: "remaining.unit",
61433
- values: [...ConsumableRemainingUnitSchema.options]
61368
+ path: "remaining.kind",
61369
+ values: [...ConsumableRemainingKindSchema.options]
61434
61370
  }]
61435
61371
  },
61436
61372
  {
@@ -61559,7 +61495,6 @@ reset: method(object({
61559
61495
  },
61560
61496
  "remaining.value": {
61561
61497
  kind: "gauge",
61562
- unitFrom: "remaining.unit",
61563
61498
  deadband: 1,
61564
61499
  displayName: "Remaining"
61565
61500
  }
@@ -61629,10 +61564,9 @@ var contactCapability = {
61629
61564
  * (empty array for the other kinds). It lives in the slice because
61630
61565
  * HA `select.*` entities may update their options at runtime.
61631
61566
  *
61632
- * The `unit`/`min`/`max`/`step` fields are meaningful for
61633
- * `kind === 'numeric'` and are absent (undefined) for all other
61634
- * kinds. They are populated live from HA attributes on each state
61635
- * push — consistent with `options` and the sensor-unit approach.
61567
+ * Numeric `min` / `max` / `step` live in the slice and share the device
61568
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
61569
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
61636
61570
  */
61637
61571
  var ControlStatusSchema = object({
61638
61572
  kind: _enum([
@@ -61644,39 +61578,17 @@ var ControlStatusSchema = object({
61644
61578
  /** Current value. Numeric kind → number; select/text/datetime → string.
61645
61579
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
61646
61580
  * or time-only — HA accepts all three forms). */
61647
- value: union([number(), string()]),
61581
+ value: union([number().meta(readingMeta("device")), string()]),
61648
61582
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
61649
61583
  options: array(string()),
61650
61584
  /** Ms epoch when the slice was last updated. */
61651
61585
  lastChangedAt: number(),
61652
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
61653
- * Populated live from HA `attributes.unit_of_measurement` on each push.
61654
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61655
- unit: string().optional(),
61656
- /** Minimum allowed value. Populated live from HA `attributes.min`.
61657
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61658
- min: number().optional(),
61659
- /** Maximum allowed value. Populated live from HA `attributes.max`.
61660
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61661
- max: number().optional(),
61662
- /** Step increment. Populated live from HA `attributes.step`.
61663
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61664
- step: number().optional(),
61665
- /** Suggested decimal places for numeric display. Populated live from HA
61666
- * `attributes.suggested_display_precision`, or derived from `step` when the
61667
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
61668
- * other kinds. Falls back to auto-formatting when absent. */
61669
- precision: number().int().min(0).max(10).optional(),
61670
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
61671
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
61672
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
61673
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
61674
- * `has_date`/`has_time` attributes on each push. */
61675
- format: _enum([
61676
- "date",
61677
- "time",
61678
- "datetime"
61679
- ]).optional()
61586
+ /** Minimum allowed value, in the device quantity's base unit. */
61587
+ min: number().optional().meta(readingMeta("device")),
61588
+ /** Maximum allowed value, in the device quantity's base unit. */
61589
+ max: number().optional().meta(readingMeta("device")),
61590
+ /** Step increment, in the device quantity's base unit. */
61591
+ step: number().optional().meta(readingMeta("device"))
61680
61592
  });
61681
61593
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
61682
61594
  object({
@@ -61764,9 +61676,9 @@ var CoverStatusSchema = object({
61764
61676
  state: CoverStateSchema,
61765
61677
  /** 0 = fully closed, 100 = fully open. Null when the device has
61766
61678
  * no intermediate position surface. */
61767
- position: number().min(0).max(100).nullable(),
61679
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61768
61680
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
61769
- tiltPosition: number().min(0).max(100).nullable(),
61681
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61770
61682
  /** Ms epoch when the slice was last updated. */
61771
61683
  lastChangedAt: number()
61772
61684
  });
@@ -61869,9 +61781,9 @@ var NormalizedRangeSchema$1 = object({
61869
61781
  var DayNightStatusSchema = object({
61870
61782
  mode: DayNightModeSchema,
61871
61783
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
61872
- sensitivity: number().optional(),
61784
+ sensitivity: number().optional().meta(readingMeta("none")),
61873
61785
  /** Delay before the IR-cut filter flips, in seconds. */
61874
- switchDelaySec: number().optional(),
61786
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
61875
61787
  lastFetchedAt: number()
61876
61788
  });
61877
61789
  /**
@@ -62089,11 +62001,11 @@ var DishwasherStatusSchema = object({
62089
62001
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
62090
62002
  availablePrograms: array(string()),
62091
62003
  /** Seconds remaining on the active cycle, or null when not running / not known. */
62092
- remainingSeconds: number().int().nonnegative().nullable(),
62004
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
62093
62005
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
62094
- delayedStartSeconds: number().int().nonnegative().nullable(),
62006
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
62095
62007
  /** Cycle completion 0..100, or null when not applicable. */
62096
- progressPercent: number().min(0).max(100).nullable(),
62008
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62097
62009
  /** True when the door is reported open; null when the door state is not known. */
62098
62010
  doorOpen: boolean().nullable(),
62099
62011
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -62117,7 +62029,7 @@ var DishwasherStatusSchema = object({
62117
62029
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
62118
62030
  machineCareDue: boolean().nullable(),
62119
62031
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
62120
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
62032
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
62121
62033
  /** Option states: null when the appliance does not expose the option or has not said. */
62122
62034
  halfLoad: boolean().nullable(),
62123
62035
  hygienePlus: boolean().nullable(),
@@ -62132,7 +62044,7 @@ var DishwasherStatusSchema = object({
62132
62044
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
62133
62045
  activeEvents: array(string()),
62134
62046
  /** Numeric error code when the appliance reports one; null otherwise. */
62135
- errorCode: number().nullable(),
62047
+ errorCode: number().nullable().meta(readingMeta("none")),
62136
62048
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
62137
62049
  errorLabel: string().nullable(),
62138
62050
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -62363,7 +62275,7 @@ var DoorbellStatusSchema = object({
62363
62275
  /** Ms epoch of the last press. null = never observed since this provider started. */
62364
62276
  lastPressedAt: number().nullable(),
62365
62277
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
62366
- pressCountSinceStart: number()
62278
+ pressCountSinceStart: number().meta(readingMeta("count"))
62367
62279
  });
62368
62280
  var DoorbellPressEventSchema = object({
62369
62281
  deviceId: number(),
@@ -62428,17 +62340,7 @@ onPressed: {
62428
62340
  history: "none",
62429
62341
  customize: doorbellCustomize
62430
62342
  };
62431
- /**
62432
- * Enum-state sensor — a string value picked from a finite option set.
62433
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
62434
- * states, weather conditions, contact-source channels, mode strings).
62435
- * The option set is stable across observations; it is persisted in the
62436
- * device config blob (via the control slice at adoption) and is NOT
62437
- * stored in `sourceInfo`.
62438
- */
62439
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
62440
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
62441
- var EnumSensorDateTimeFormatSchema = _enum([
62343
+ _enum([
62442
62344
  "date",
62443
62345
  "time",
62444
62346
  "datetime"
@@ -62446,12 +62348,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
62446
62348
  var EnumSensorStatusSchema = object({
62447
62349
  value: string(),
62448
62350
  /**
62449
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
62450
- * as a locale date/time/datetime; absent for plain enum sensors. HA
62451
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
62452
- * a time-only sensor → `'time'`.
62351
+ * Set when `value` is a timestamp rather than an enum state, so history
62352
+ * does not learn ISO strings as states. The display shape (date / time /
62353
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
62453
62354
  */
62454
- format: EnumSensorDateTimeFormatSchema.optional(),
62355
+ timestamp: boolean().optional(),
62455
62356
  /** Ms epoch when the slice was last updated. */
62456
62357
  lastFetchedAt: number()
62457
62358
  });
@@ -62478,11 +62379,11 @@ var enumSensorCapability = {
62478
62379
  * History (D850): the value as an `enum` series (an appliance's program or
62479
62380
  * operation state, an HVAC action). The option set is per device, so the
62480
62381
  * value list is LEARNED as values are first seen (bounded). A sensor with a
62481
- * `format` carries a timestamp, not a state, and is not recorded.
62382
+ * `timestamp` carries a clock, not a state, and is not recorded.
62482
62383
  */
62483
62384
  history: { fields: { value: {
62484
62385
  kind: "enum",
62485
- skipWhenSet: "format"
62386
+ skipWhenSet: "timestamp"
62486
62387
  } } },
62487
62388
  /** Clock fields: written, but excluded from the compare that decides
62488
62389
  * whether persisting is worth a SQLite commit. */
@@ -62502,13 +62403,13 @@ var EventFireSchema = object({
62502
62403
  deviceId: number(),
62503
62404
  eventType: string(),
62504
62405
  data: record(string(), unknown()).nullable(),
62505
- timestamp: number(),
62506
- seq: number()
62406
+ timestamp: number().meta(readingMeta("none")),
62407
+ seq: number().meta(readingMeta("count"))
62507
62408
  });
62508
62409
  var EventEmitterStatusSchema = object({
62509
62410
  eventTypes: array(string()),
62510
62411
  lastEvent: EventFireSchema.nullable(),
62511
- eventCountSinceStart: number()
62412
+ eventCountSinceStart: number().meta(readingMeta("count"))
62512
62413
  });
62513
62414
  var eventEmitterCapability = {
62514
62415
  name: "event-emitter",
@@ -62961,10 +62862,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
62961
62862
  var FanControlStatusSchema = object({
62962
62863
  /** Active speed as 0..100 inclusive. Null when the device has no
62963
62864
  * speed surface (single-speed fan). */
62964
- percentage: number().min(0).max(100).nullable(),
62865
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62965
62866
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
62966
62867
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
62967
- percentageStep: number().positive().optional(),
62868
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
62968
62869
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
62969
62870
  * — empty string when no preset is active or supported. */
62970
62871
  preset: string(),
@@ -63085,7 +62986,7 @@ var FeatureProbeStatusSchema = object({
63085
62986
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
63086
62987
  model: string().nullable(),
63087
62988
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
63088
- channelCount: number().nullable(),
62989
+ channelCount: number().nullable().meta(readingMeta("count")),
63089
62990
  /**
63090
62991
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
63091
62992
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -63210,7 +63111,7 @@ var gasCapability = {
63210
63111
  };
63211
63112
  var HeatingValveStatusSchema = object({
63212
63113
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
63213
- position: number().min(0).max(100).nullable(),
63114
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63214
63115
  /** Ms epoch when the slice was last updated (push or poll). */
63215
63116
  lastFetchedAt: number()
63216
63117
  });
@@ -63253,9 +63154,9 @@ var HumidifierStatusSchema = object({
63253
63154
  /** Whether the humidifier is currently on. */
63254
63155
  on: boolean(),
63255
63156
  /** Current measured relative humidity (0..100). Null when not reported. */
63256
- currentHumidity: number().min(0).max(100).nullable(),
63157
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63257
63158
  /** Target relative humidity (0..100). Null when no setpoint surface. */
63258
- targetHumidity: number().min(0).max(100).nullable(),
63159
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63259
63160
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
63260
63161
  * device has no mode surface. */
63261
63162
  mode: string().nullable(),
@@ -63265,9 +63166,9 @@ var HumidifierStatusSchema = object({
63265
63166
  * `idle` / `off`). Null when the device doesn't report it. */
63266
63167
  action: string().nullable(),
63267
63168
  /** HA `min_humidity` attribute. Null → UI uses 0. */
63268
- minHumidity: number().nullable(),
63169
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
63269
63170
  /** HA `max_humidity` attribute. Null → UI uses 100. */
63270
- maxHumidity: number().nullable(),
63171
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
63271
63172
  /** Ms epoch when the slice was last updated. */
63272
63173
  lastChangedAt: number()
63273
63174
  });
@@ -63337,22 +63238,13 @@ var humidifierCapability = {
63337
63238
  * `sourceInfo`.
63338
63239
  */
63339
63240
  var HumiditySensorStatusSchema = object({
63340
- /** Current relative humidity, 0..100. */
63341
- percent: number().min(0).max(100).meta({
63342
- unit: "%",
63241
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
63242
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
63243
+ precision: 0,
63343
63244
  label: "Humidity"
63344
- }),
63245
+ })),
63345
63246
  /** Ms epoch when the slice was last updated. */
63346
- lastFetchedAt: number(),
63347
- /** Live display unit from the upstream source (e.g. HA
63348
- * `attributes.unit_of_measurement`). The UI prefers this over the
63349
- * role's canonical unit. Absent → fall back to the canonical unit. */
63350
- unit: string().optional(),
63351
- /** Suggested decimal places for numeric display.
63352
- * Populated live from the upstream source when provided (e.g. HA
63353
- * `attributes.suggested_display_precision`). Falls back to
63354
- * auto-formatting when absent. */
63355
- precision: number().int().min(0).max(10).optional()
63247
+ lastFetchedAt: number()
63356
63248
  });
63357
63249
  var humiditySensorCapability = {
63358
63250
  name: "humidity-sensor",
@@ -63383,12 +63275,7 @@ var humiditySensorCapability = {
63383
63275
  } } },
63384
63276
  /** Clock fields: written, but excluded from the compare that decides
63385
63277
  * whether persisting is worth a SQLite commit. */
63386
- volatileStateFields: ["lastFetchedAt"],
63387
- customize: {
63388
- version: 1,
63389
- advanced: "derive",
63390
- targets: []
63391
- }
63278
+ volatileStateFields: ["lastFetchedAt"]
63392
63279
  };
63393
63280
  /**
63394
63281
  * Image display cap. Models a single still image exposed by an integration —
@@ -63409,7 +63296,7 @@ var ImageStatusSchema = object({
63409
63296
  * entity exposes no `entity_picture` (yet). */
63410
63297
  url: string().nullable(),
63411
63298
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
63412
- lastUpdated: number().nullable()
63299
+ lastUpdated: number().nullable().meta(readingMeta("none"))
63413
63300
  });
63414
63301
  var imageCapability = {
63415
63302
  name: "image",
@@ -63497,19 +63384,19 @@ var NormalizedRangeSchema = object({
63497
63384
  */
63498
63385
  var ImageSettingsStatusSchema = object({
63499
63386
  /** Normalized 0–100. */
63500
- brightness: number().optional(),
63387
+ brightness: number().optional().meta(readingMeta("none")),
63501
63388
  /** Normalized 0–100. */
63502
- contrast: number().optional(),
63389
+ contrast: number().optional().meta(readingMeta("none")),
63503
63390
  /** Normalized 0–100. */
63504
- saturation: number().optional(),
63391
+ saturation: number().optional().meta(readingMeta("none")),
63505
63392
  /** Normalized 0–100. */
63506
- sharpness: number().optional(),
63393
+ sharpness: number().optional().meta(readingMeta("none")),
63507
63394
  mirror: boolean().optional(),
63508
63395
  flip: boolean().optional(),
63509
63396
  rotate: ImageRotateSchema.optional(),
63510
63397
  whiteBalance: WhiteBalanceModeSchema.optional(),
63511
63398
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
63512
- warmth: number().optional(),
63399
+ warmth: number().optional().meta(readingMeta("none")),
63513
63400
  exposureMode: ExposureModeSchema.optional(),
63514
63401
  backlightMode: BacklightModeSchema.optional(),
63515
63402
  lastFetchedAt: number()
@@ -63788,10 +63675,10 @@ var IntercomAbilitySchema = object({
63788
63675
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
63789
63676
  codecs: array(string()),
63790
63677
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
63791
- sampleRate: number(),
63678
+ sampleRate: number().meta(readingMeta("none")),
63792
63679
  duplex: _enum(["half", "full"]),
63793
63680
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
63794
- maxBacklogMs: number()
63681
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
63795
63682
  });
63796
63683
  var IntercomStatusSchema = object({
63797
63684
  /** True while a talk session is open. */
@@ -64018,13 +63905,13 @@ var LawnMowerControlStatusSchema = object({
64018
63905
  activity: LawnMowerActivitySchema,
64019
63906
  /** 0..100 battery percentage. Null when the device has no battery
64020
63907
  * reading. */
64021
- batteryLevel: number().min(0).max(100).nullable(),
63908
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64022
63909
  /** 0..100 mowing-completion percentage of the current task, or null when no
64023
63910
  * task is active / progress is unavailable. */
64024
- progressPercent: number().min(0).max(100).nullable(),
63911
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64025
63912
  /** Current device/event code (dynamic — mostly status, sometimes an error),
64026
63913
  * or null when unknown. */
64027
- currentCode: number().nullable(),
63914
+ currentCode: number().nullable().meta(readingMeta("none")),
64028
63915
  /** Human label for {@link currentCode}, or null when undecodable. */
64029
63916
  currentCodeLabel: string().nullable(),
64030
63917
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -64578,7 +64465,7 @@ var MediaPlayerStatusSchema = object({
64578
64465
  state: MediaPlayerStateSchema,
64579
64466
  /** Volume as 0..100 inclusive. Null when the device has no volume
64580
64467
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
64581
- volumeLevel: number().min(0).max(100).nullable(),
64468
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64582
64469
  /** Mute toggle distinct from volume=0. Null when no mute surface.
64583
64470
  * Pair with `DeviceFeature.MediaPlayerMute`. */
64584
64471
  isMuted: boolean().nullable(),
@@ -64591,10 +64478,10 @@ var MediaPlayerStatusSchema = object({
64591
64478
  currentMedia: MediaInfoSchema.nullable(),
64592
64479
  /** Current playback position in ms. Null when not seekable or
64593
64480
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
64594
- positionMs: number().int().nonnegative().nullable(),
64481
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
64595
64482
  /** Total duration of the current media in ms. Null when unknown
64596
64483
  * (live stream) or nothing is playing. */
64597
- durationMs: number().int().nonnegative().nullable(),
64484
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
64598
64485
  /** Shuffle toggle. Null when no shuffle surface.
64599
64486
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
64600
64487
  shuffle: boolean().nullable(),
@@ -64903,7 +64790,7 @@ var motionCustomize = {
64903
64790
  eventName: "onMotionChanged"
64904
64791
  },
64905
64792
  deviceTypes: [DeviceType.Camera],
64906
- apply: "replace",
64793
+ apply: "add",
64907
64794
  applyAuthority: {
64908
64795
  kind: "camera-sensors",
64909
64796
  timeoutField: "motionCooldownMs"
@@ -65000,7 +64887,7 @@ var MotionStatusSchema = object({
65000
64887
  * it, every provider seeded it at boot, and every session left it
65001
64888
  * `detected: true` after its detach.
65002
64889
  */
65003
- autoClearAfterMs: number().nullable()
64890
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
65004
64891
  });
65005
64892
  /**
65006
64893
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -65182,7 +65069,7 @@ var MotionZoneRegionSchema = object({
65182
65069
  * the grid region(s). */
65183
65070
  var MotionZoneStatusSchema = object({
65184
65071
  enabled: boolean(),
65185
- sensitivity: number(),
65072
+ sensitivity: number().meta(readingMeta("none")),
65186
65073
  /** Grid region(s). Today exactly one `grid` shape. */
65187
65074
  regions: array(MotionZoneRegionSchema),
65188
65075
  lastFetchedAt: number()
@@ -65552,7 +65439,7 @@ var NavigationStatusSchema = object({
65552
65439
  following: boolean(),
65553
65440
  flash: boolean(),
65554
65441
  lightMode: NavigationLightModeSchema,
65555
- lightLevel: number().min(40).max(100),
65442
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
65556
65443
  /** Ms epoch when the slice was last updated. */
65557
65444
  lastChangedAt: number()
65558
65445
  });
@@ -65686,13 +65573,13 @@ var NetworkLinkStatusSchema = object({
65686
65573
  * one whose reading has not landed must not be drawn at 0 %. Consumers
65687
65574
  * SKIP a null rather than coerce it.
65688
65575
  */
65689
- signalPercent: number().min(0).max(100).nullable(),
65576
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65690
65577
  /** Raw received signal strength in dBm, when the firmware reports one. */
65691
- rssiDbm: number().optional(),
65578
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
65692
65579
  /** Network name of a wireless link, when the firmware reports it. */
65693
65580
  ssid: string().optional(),
65694
65581
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
65695
- lastUpdated: number()
65582
+ lastUpdated: number().meta(readingMeta("none"))
65696
65583
  });
65697
65584
  var networkLinkCapability = {
65698
65585
  name: "network-link",
@@ -66077,7 +65964,7 @@ var NotifierStatusSchema = object({
66077
65964
  lastError: string().nullable(),
66078
65965
  /** Number of deliveries currently buffered server-side (rate-limited
66079
65966
  * notifiers may queue). 0 when the provider sends synchronously. */
66080
- queueDepth: number().int().nonnegative(),
65967
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
66081
65968
  /** Per-feature capability matrix — authoritative source for the
66082
65969
  * compose-form field gating. */
66083
65970
  supports: NotifierSupportsSchema
@@ -66169,11 +66056,9 @@ onSent: { data: object({
66169
66056
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
66170
66057
  * for: water flow, distance, weight, frequency, signal strength, …).
66171
66058
  *
66172
- * The `unit` and `precision` fields carry the live display metadata that
66173
- * the upstream source provides with each state push (HA
66174
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
66175
- * The UI reads them directly from the slice — they are the single source
66176
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
66059
+ * The quantity is not on this slice. It is chosen when the device is
66060
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
66061
+ * that quantity's base unit. `null` is a refused sample (D393).
66177
66062
  *
66178
66063
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
66179
66064
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -66182,15 +66067,8 @@ onSent: { data: object({
66182
66067
  * numeric values land here.
66183
66068
  */
66184
66069
  var NumericSensorStatusSchema = object({
66185
- value: number(),
66186
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
66187
- * Populated live from the upstream source on each state push.
66188
- * Absent for unitless sensors. */
66189
- unit: string().optional(),
66190
- /** Suggested decimal places for numeric display.
66191
- * Populated live from the upstream source when provided.
66192
- * Falls back to auto-formatting when absent. */
66193
- precision: number().int().min(0).max(10).optional(),
66070
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
66071
+ value: number().nullable().meta(readingMeta("device")),
66194
66072
  /** Ms epoch when the slice was last updated. */
66195
66073
  lastFetchedAt: number()
66196
66074
  });
@@ -66214,11 +66092,11 @@ var numericSensorCapability = {
66214
66092
  */
66215
66093
  durability: "restored",
66216
66094
  /**
66217
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
66095
+ * History (D850): recorded series. The unit is the device quantity's base,
66096
+ * resolved when the series is read — the cap has no single unit.
66218
66097
  */
66219
66098
  history: { fields: { value: {
66220
66099
  kind: "gauge",
66221
- unitFrom: "unit",
66222
66100
  minIntervalMs: 3e4
66223
66101
  } } },
66224
66102
  /** Clock fields: written, but excluded from the compare that decides
@@ -66510,23 +66388,26 @@ var PetFeederStatusSchema = object({
66510
66388
  /** Food currently in the bowl (grams). Null when the device has not
66511
66389
  * reported a reading yet. On dual-hopper models this is the combined
66512
66390
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
66513
- foodLevel: number().nullable(),
66391
+ foodLevel: number().nullable().meta(readingMeta("none")),
66514
66392
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
66515
66393
  * single-hopper models. */
66516
- food1: number().nullable(),
66394
+ food1: number().nullable().meta(readingMeta("none")),
66517
66395
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
66518
66396
  * single-hopper models. */
66519
- food2: number().nullable(),
66397
+ food2: number().nullable().meta(readingMeta("none")),
66520
66398
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
66521
66399
  * (`device_class: problem`, on = low). True when the bowl is empty /
66522
66400
  * below the feeder's low threshold. */
66523
66401
  lowFood: boolean(),
66524
66402
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
66525
66403
  * device has no battery reading. */
66526
- batteryPower: number().min(0).max(100).nullable(),
66527
- /** Days of desiccant life remaining. Null when the model has no
66528
- * desiccant sensor. */
66529
- desiccantLeftDays: number().nullable(),
66404
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66405
+ /**
66406
+ * Desiccant life remaining, stored in seconds (duration base, D944).
66407
+ * Providers convert from the days the feeder reports. Null when the
66408
+ * model has no desiccant sensor.
66409
+ */
66410
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
66530
66411
  /** True while a feed is in progress. */
66531
66412
  feeding: boolean(),
66532
66413
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -66537,7 +66418,7 @@ var PetFeederStatusSchema = object({
66537
66418
  * with `errorCode` for consumers that want the raw integer. */
66538
66419
  error: string().nullable(),
66539
66420
  /** Raw device error code (0 / null = no error). */
66540
- errorCode: number().nullable(),
66421
+ errorCode: number().nullable().meta(readingMeta("none")),
66541
66422
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
66542
66423
  isDualHopper: boolean(),
66543
66424
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -66547,7 +66428,7 @@ var PetFeederStatusSchema = object({
66547
66428
  /** Play a chime when dispensing. */
66548
66429
  feedSound: boolean(),
66549
66430
  /** Speaker / prompt volume level (device-scaled integer). */
66550
- volume: number(),
66431
+ volume: number().meta(readingMeta("none")),
66551
66432
  /** Ms epoch when the slice was last refreshed from the cloud. */
66552
66433
  lastFetchedAt: number()
66553
66434
  });
@@ -66666,7 +66547,7 @@ var petFeederCapability = {
66666
66547
  },
66667
66548
  desiccantLeftDays: {
66668
66549
  kind: "gauge",
66669
- unit: "days"
66550
+ unit: "s"
66670
66551
  }
66671
66552
  } }
66672
66553
  };
@@ -67102,36 +66983,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
67102
66983
  */
67103
66984
  var PowerMeterStatusSchema = object({
67104
66985
  /** Instantaneous power draw in watts. */
67105
- watts: number().optional().meta({
67106
- unit: "W",
67107
- label: "Power"
67108
- }),
66986
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
67109
66987
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
67110
- kwhTotal: number().optional().meta({
67111
- unit: "kWh",
67112
- label: "Energy"
67113
- }),
66988
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
67114
66989
  /** Voltage in volts. */
67115
- volts: number().optional(),
66990
+ volts: number().optional().meta(readingMeta("voltage")),
67116
66991
  /** Current in amperes. */
67117
- amps: number().optional(),
67118
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
67119
- va: number().optional(),
66992
+ amps: number().optional().meta(readingMeta("current")),
66993
+ /** Apparent power in volt-amperes. */
66994
+ va: number().optional().meta(readingMeta("apparent-power")),
67120
66995
  /** Power factor (0..1) if reported. */
67121
- powerFactor: number().min(0).max(1).optional(),
66996
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
67122
66997
  /** Ms epoch when the slice was last updated. */
67123
- lastFetchedAt: number(),
67124
- /** Live display unit of the single metric this slice carries (e.g. HA
67125
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
67126
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
67127
- * unit per slice is unambiguous. The UI prefers this over the role's
67128
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
67129
- unit: string().optional(),
67130
- /** Suggested decimal places for numeric display.
67131
- * Populated live from the upstream source when provided (e.g. HA
67132
- * `attributes.suggested_display_precision`). Falls back to
67133
- * auto-formatting when absent. */
67134
- precision: number().int().min(0).max(10).optional()
66998
+ lastFetchedAt: number()
67135
66999
  });
67136
67000
  var powerMeterCapability = {
67137
67001
  name: "power-meter",
@@ -67195,12 +67059,7 @@ var powerMeterCapability = {
67195
67059
  } },
67196
67060
  /** Clock fields: written, but excluded from the compare that decides
67197
67061
  * whether persisting is worth a SQLite commit. */
67198
- volatileStateFields: ["lastFetchedAt"],
67199
- customize: {
67200
- version: 1,
67201
- advanced: "derive",
67202
- targets: []
67203
- }
67062
+ volatileStateFields: ["lastFetchedAt"]
67204
67063
  };
67205
67064
  /**
67206
67065
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -67220,11 +67079,11 @@ var powerMeterCapability = {
67220
67079
  */
67221
67080
  var GpsLocationSchema = object({
67222
67081
  /** Latitude in decimal degrees, -90..90. */
67223
- latitude: number().min(-90).max(90),
67082
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
67224
67083
  /** Longitude in decimal degrees, -180..180. */
67225
- longitude: number().min(-180).max(180),
67084
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
67226
67085
  /** Reported accuracy in meters (lower = better). */
67227
- accuracyMeters: number().nonnegative()
67086
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
67228
67087
  });
67229
67088
  var PresenceStatusSchema = object({
67230
67089
  /** `home` / `not_home` / any user-defined zone name. */
@@ -67236,7 +67095,7 @@ var PresenceStatusSchema = object({
67236
67095
  gps: GpsLocationSchema.nullable(),
67237
67096
  /** Optional battery level of the tracking device (0..100). Null
67238
67097
  * when not reported. */
67239
- batteryPercent: number().min(0).max(100).nullable(),
67098
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67240
67099
  /** Ms epoch when the slice was last updated. */
67241
67100
  lastChangedAt: number()
67242
67101
  });
@@ -67286,19 +67145,10 @@ var presenceCapability = {
67286
67145
  * `sourceInfo`.
67287
67146
  */
67288
67147
  var PressureSensorStatusSchema = object({
67289
- /** Current pressure in hPa. */
67290
- hpa: number(),
67148
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
67149
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
67291
67150
  /** Ms epoch when the slice was last updated. */
67292
- lastFetchedAt: number(),
67293
- /** Live display unit from the upstream source (e.g. HA
67294
- * `attributes.unit_of_measurement`). The UI prefers this over the
67295
- * role's canonical unit. Absent → fall back to the canonical unit. */
67296
- unit: string().optional(),
67297
- /** Suggested decimal places for numeric display.
67298
- * Populated live from the upstream source when provided (e.g. HA
67299
- * `attributes.suggested_display_precision`). Falls back to
67300
- * auto-formatting when absent. */
67301
- precision: number().int().min(0).max(10).optional()
67151
+ lastFetchedAt: number()
67302
67152
  });
67303
67153
  var pressureSensorCapability = {
67304
67154
  name: "pressure-sensor",
@@ -67712,8 +67562,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
67712
67562
  }), _void(), { kind: "mutation" });
67713
67563
  var PtzAutotrackSettingsSchema = object({
67714
67564
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
67715
- stopDelaySeconds: number().int().min(0).max(300),
67716
- disappearDelaySeconds: number().int().min(0).max(300)
67565
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
67566
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
67717
67567
  });
67718
67568
  /**
67719
67569
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -67867,7 +67717,7 @@ var RunningStateStatusSchema = object({
67867
67717
  /** null = unknown — never coerced to false (D393). */
67868
67718
  running: boolean().nullable(),
67869
67719
  /** Epoch ms the current boolean began; null while `running` is null. */
67870
- since: number().nullable(),
67720
+ since: number().nullable().meta(readingMeta("none")),
67871
67721
  source: RunningStateSourceSchema.meta({ label: "Source" }),
67872
67722
  phase: string().nullable(),
67873
67723
  /** null until a reading has been published (D393). */
@@ -67882,7 +67732,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
67882
67732
  /** Generic Customize target for the running boolean (D936). */
67883
67733
  var runningCustomize = {
67884
67734
  version: 1,
67885
- advanced: "derive",
67886
67735
  targets: [{
67887
67736
  id: "running",
67888
67737
  cardinality: "scalar",
@@ -67902,7 +67751,7 @@ var runningCustomize = {
67902
67751
  DeviceType.Thermostat,
67903
67752
  DeviceType.PetFeeder
67904
67753
  ],
67905
- apply: "add-or-replace",
67754
+ apply: "add",
67906
67755
  path: "running",
67907
67756
  sources: [
67908
67757
  {
@@ -69198,9 +69047,9 @@ var RecordTriggerSchema = _enum([
69198
69047
  */
69199
69048
  var RecordWindowSchema = object({
69200
69049
  trigger: RecordTriggerSchema,
69201
- day: number().int().min(0).max(6),
69202
- startMinute: number().int().min(0).max(1439),
69203
- endMinute: number().int().min(1).max(1440)
69050
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
69051
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
69052
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
69204
69053
  });
69205
69054
  /** Status of one physical volume, as the camera itself describes it. */
69206
69055
  var OnboardStorageVolumeSchema = object({
@@ -69276,10 +69125,10 @@ var RecordingOnboardStatusSchema = object({
69276
69125
  /** Master "record to the card at all" switch. */
69277
69126
  enabled: boolean().nullable(),
69278
69127
  overwriteWhenFull: boolean().nullable(),
69279
- preRecordSec: number().nullable(),
69280
- postRecordSec: number().nullable(),
69128
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
69129
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
69281
69130
  /** Length of one recorded file, in minutes. */
69282
- segmentMinutes: number().nullable(),
69131
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
69283
69132
  /** The primary track's weekly windows, flattened. */
69284
69133
  windows: array(RecordWindowSchema),
69285
69134
  /**
@@ -69295,7 +69144,7 @@ var RecordingOnboardStatusSchema = object({
69295
69144
  * provider reporting a non-zero count also reports the schedule as not
69296
69145
  * writable.
69297
69146
  */
69298
- unreadableWindows: number(),
69147
+ unreadableWindows: number().meta(readingMeta("count")),
69299
69148
  /**
69300
69149
  * The camera is scheduled to record and has NO usable storage.
69301
69150
  *
@@ -70158,7 +70007,7 @@ var CamStreamDescriptorSchema = object({
70158
70007
  url: string().optional(),
70159
70008
  codec: string().optional(),
70160
70009
  resolution: CamStreamResolutionSchema.optional(),
70161
- fps: number().positive().optional(),
70010
+ fps: number().positive().optional().meta(readingMeta("rate")),
70162
70011
  label: string().optional(),
70163
70012
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
70164
70013
  deviceFeatures: array(string()).optional(),
@@ -70245,18 +70094,18 @@ var StreamProfileSchema = _enum([
70245
70094
  ]);
70246
70095
  /** The current encoding config of one stream profile. */
70247
70096
  var StreamProfileConfigSchema = object({
70248
- width: number(),
70249
- height: number(),
70097
+ width: number().meta(readingMeta("none")),
70098
+ height: number().meta(readingMeta("none")),
70250
70099
  codec: _enum(["h264", "h265"]),
70251
- framerate: number(),
70252
- bitrate: number(),
70100
+ framerate: number().meta(readingMeta("rate")),
70101
+ bitrate: number().meta(readingMeta("bitrate")),
70253
70102
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
70254
70103
  encoderProfile: _enum([
70255
70104
  "high",
70256
70105
  "main",
70257
70106
  "baseline"
70258
70107
  ]).optional(),
70259
- gop: number().optional(),
70108
+ gop: number().optional().meta(readingMeta("none")),
70260
70109
  /**
70261
70110
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
70262
70111
  *
@@ -70477,12 +70326,7 @@ var switchCapability = {
70477
70326
  label: "On",
70478
70327
  kind: "boolean"
70479
70328
  }
70480
- }] },
70481
- customize: {
70482
- version: 1,
70483
- advanced: "derive",
70484
- targets: []
70485
- }
70329
+ }] }
70486
70330
  };
70487
70331
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
70488
70332
  function hasControlCharacter(value) {
@@ -71065,35 +70909,21 @@ var tamperCapability = {
71065
70909
  * entries with `device_class: temperature` and any future native
71066
70910
  * thermometer.
71067
70911
  *
71068
- * Unit normalisation: providers convert to Celsius before storing.
71069
- * The slice value is always Celsius so cross-cap aggregators
71070
- * (climate-control's `currentTemp`, energy analytics) can compose
71071
- * without per-source unit fixups. The canonical display unit (`°C`) is
71072
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
71073
- * `sourceInfo`.
70912
+ * The number is Celsius (D944). Providers convert on ingest. A failed
70913
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
71074
70914
  *
71075
70915
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
71076
70916
  * frozen feed (provider hung) distinct from a "temperature hasn't
71077
70917
  * changed" steady state.
71078
70918
  */
71079
70919
  var TemperatureSensorStatusSchema = object({
71080
- /** Current temperature in Celsius. */
71081
- celsius: number().meta({
71082
- unit: "°C",
70920
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
70921
+ celsius: number().nullable().meta(readingMeta("temperature", {
70922
+ precision: 1,
71083
70923
  label: "Temperature"
71084
- }),
70924
+ })),
71085
70925
  /** Ms epoch when the slice was last updated (push or poll). */
71086
- lastFetchedAt: number(),
71087
- /** Live display unit from the upstream source (e.g. HA
71088
- * `attributes.unit_of_measurement`). The UI prefers this over the
71089
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
71090
- * Absent → the UI falls back to the role's canonical unit. */
71091
- unit: string().optional(),
71092
- /** Suggested decimal places for numeric display.
71093
- * Populated live from the upstream source when provided (e.g. HA
71094
- * `attributes.suggested_display_precision`). Falls back to
71095
- * auto-formatting when absent. */
71096
- precision: number().int().min(0).max(10).optional()
70926
+ lastFetchedAt: number()
71097
70927
  });
71098
70928
  var temperatureSensorCapability = {
71099
70929
  name: "temperature-sensor",
@@ -71124,12 +70954,7 @@ var temperatureSensorCapability = {
71124
70954
  } } },
71125
70955
  /** Clock fields: written, but excluded from the compare that decides
71126
70956
  * whether persisting is worth a SQLite commit. */
71127
- volatileStateFields: ["lastFetchedAt"],
71128
- customize: {
71129
- version: 1,
71130
- advanced: "derive",
71131
- targets: []
71132
- }
70957
+ volatileStateFields: ["lastFetchedAt"]
71133
70958
  };
71134
70959
  /**
71135
70960
  * toast — system-scoped singleton capability that streams toast
@@ -71412,16 +71237,16 @@ var VacuumControlStatusSchema = object({
71412
71237
  ]),
71413
71238
  /** 0..100 battery percentage. Null when the device has no battery
71414
71239
  * reading. */
71415
- batteryLevel: number().min(0).max(100).nullable(),
71240
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71416
71241
  /** Current fan-speed token (provider-verbatim). Null when unknown or
71417
71242
  * the vacuum has no speed control. */
71418
71243
  fanSpeed: string().nullable(),
71419
71244
  /** Speed tokens the hardware accepts — drives the UI selector. */
71420
71245
  availableFanSpeeds: array(string()),
71421
71246
  /** 0..100 cleaning-completion percentage of the current task, or null. */
71422
- progressPercent: number().min(0).max(100).nullable(),
71247
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71423
71248
  /** Current error code (0 / null = no error). */
71424
- errorCode: number().nullable(),
71249
+ errorCode: number().nullable().meta(readingMeta("none")),
71425
71250
  /** Human label for {@link errorCode}, or null when none / undecodable. */
71426
71251
  errorLabel: string().nullable(),
71427
71252
  /** Ms epoch when the slice was last updated. */
@@ -71499,7 +71324,7 @@ var ValveStatusSchema = object({
71499
71324
  ]),
71500
71325
  /** 0 = fully closed, 100 = fully open. Null when the device has no
71501
71326
  * intermediate position surface. */
71502
- position: number().min(0).max(100).nullable(),
71327
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71503
71328
  /** Ms epoch when the slice was last updated. */
71504
71329
  lastChangedAt: number()
71505
71330
  });
@@ -71607,9 +71432,9 @@ var vibrationCapability = {
71607
71432
  */
71608
71433
  var WaterHeaterStatusSchema = object({
71609
71434
  /** Current measured temperature. Null when not reported. */
71610
- currentTemp: number().nullable(),
71435
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
71611
71436
  /** Target temperature setpoint. Null when no setpoint surface. */
71612
- targetTemp: number().nullable(),
71437
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
71613
71438
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
71614
71439
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
71615
71440
  * device reports an unknown state. */
@@ -71620,9 +71445,9 @@ var WaterHeaterStatusSchema = object({
71620
71445
  * has no away surface. */
71621
71446
  away: boolean().nullable(),
71622
71447
  /** HA `min_temp` attribute. Null when not reported. */
71623
- minTemp: number().nullable(),
71448
+ minTemp: number().nullable().meta(readingMeta("temperature")),
71624
71449
  /** HA `max_temp` attribute. Null when not reported. */
71625
- maxTemp: number().nullable(),
71450
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
71626
71451
  /** Ms epoch when the slice was last updated. */
71627
71452
  lastChangedAt: number()
71628
71453
  });
@@ -71707,25 +71532,18 @@ var waterHeaterCapability = {
71707
71532
  * Forecast deferred — current conditions only for v1.
71708
71533
  */
71709
71534
  var WeatherStatusSchema = object({
71710
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
71711
- * when no condition has been reported yet. */
71535
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
71712
71536
  condition: string().nullable(),
71713
- /** Current temperature in the reported unit. Null when not provided. */
71714
- temperature: number().nullable(),
71715
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
71716
- temperatureUnit: string().nullable(),
71537
+ /** Current temperature in Celsius. Null when not provided or refused. */
71538
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
71717
71539
  /** Relative humidity (0..100). Null when not provided. */
71718
- humidity: number().min(0).max(100).nullable(),
71719
- /** Barometric pressure in the reported unit. Null when not provided. */
71720
- pressure: number().nullable(),
71721
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
71722
- pressureUnit: string().nullable(),
71723
- /** Wind speed in the reported unit. Null when not provided. */
71724
- windSpeed: number().nullable(),
71725
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
71726
- windSpeedUnit: string().nullable(),
71540
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
71541
+ /** Barometric pressure in hPa. Null when not provided or refused. */
71542
+ pressure: number().nullable().meta(readingMeta("pressure")),
71543
+ /** Wind speed in metres per second. Null when not provided or refused. */
71544
+ windSpeed: number().nullable().meta(readingMeta("speed")),
71727
71545
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
71728
- windBearing: number().nullable(),
71546
+ windBearing: number().nullable().meta(readingMeta("angle")),
71729
71547
  /** Ms epoch when the slice was last updated. */
71730
71548
  lastFetchedAt: number()
71731
71549
  });
@@ -71753,12 +71571,12 @@ var weatherCapability = {
71753
71571
  */
71754
71572
  durability: "session",
71755
71573
  /**
71756
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
71574
+ * History (D850): recorded series. Numbers are the quantity base (D944).
71757
71575
  */
71758
71576
  history: { fields: {
71759
71577
  temperature: {
71760
71578
  kind: "gauge",
71761
- unitFrom: "temperatureUnit",
71579
+ unit: "°C",
71762
71580
  deadband: .2
71763
71581
  },
71764
71582
  humidity: {
@@ -71768,12 +71586,12 @@ var weatherCapability = {
71768
71586
  },
71769
71587
  pressure: {
71770
71588
  kind: "gauge",
71771
- unitFrom: "pressureUnit",
71589
+ unit: "hPa",
71772
71590
  deadband: .5
71773
71591
  },
71774
71592
  windSpeed: {
71775
71593
  kind: "gauge",
71776
- unitFrom: "windSpeedUnit",
71594
+ unit: "m/s",
71777
71595
  deadband: .5
71778
71596
  }
71779
71597
  } }
@@ -86913,7 +86731,6 @@ var SENSOR_CAP_VOCABULARY = {
86913
86731
  field: "value",
86914
86732
  quantity: t("the reading", "il valore"),
86915
86733
  label: t("Reading", "Valore"),
86916
- unitField: "unit",
86917
86734
  icon: "gauge"
86918
86735
  }]
86919
86736
  },
@@ -87049,7 +86866,7 @@ var SENSOR_CAP_VOCABULARY = {
87049
86866
  field: "temperature",
87050
86867
  quantity: t("the outdoor temperature", "la temperatura esterna"),
87051
86868
  label: t("Temperature", "Temperatura"),
87052
- unitField: "temperatureUnit",
86869
+ unit: "°C",
87053
86870
  icon: "thermometer"
87054
86871
  },
87055
86872
  {
@@ -87065,7 +86882,7 @@ var SENSOR_CAP_VOCABULARY = {
87065
86882
  field: "windSpeed",
87066
86883
  quantity: t("the wind speed", "la velocità del vento"),
87067
86884
  label: t("Wind", "Vento"),
87068
- unitField: "windSpeedUnit",
86885
+ unit: "m/s",
87069
86886
  icon: "wind"
87070
86887
  },
87071
86888
  {
@@ -87073,7 +86890,7 @@ var SENSOR_CAP_VOCABULARY = {
87073
86890
  field: "pressure",
87074
86891
  quantity: t("the air pressure", "la pressione atmosferica"),
87075
86892
  label: t("Pressure", "Pressione"),
87076
- unitField: "pressureUnit",
86893
+ unit: "hPa",
87077
86894
  icon: "gauge"
87078
86895
  }
87079
86896
  ]