@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.
@@ -5632,7 +5632,7 @@ var ZodIssueCode = {
5632
5632
  var ZodFirstPartyTypeKind;
5633
5633
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5634
5634
  //#endregion
5635
- //#region ../types/dist/sleep-BErAxhul.mjs
5635
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5636
5636
  /**
5637
5637
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5638
5638
  * window to float samples (D455).
@@ -8332,6 +8332,52 @@ function defineRecordKinds(specs) {
8332
8332
  }
8333
8333
  return out;
8334
8334
  }
8335
+ var READING_QUANTITIES = [
8336
+ "temperature",
8337
+ "pressure",
8338
+ "speed",
8339
+ "precipitation",
8340
+ "precipitation-rate",
8341
+ "length",
8342
+ "area",
8343
+ "illuminance",
8344
+ "irradiance",
8345
+ "power",
8346
+ "energy",
8347
+ "voltage",
8348
+ "current",
8349
+ "apparent-power",
8350
+ "humidity",
8351
+ "percentage",
8352
+ "concentration-volume",
8353
+ "concentration-mass",
8354
+ "concentration-ppb",
8355
+ "volume",
8356
+ "duration",
8357
+ "duration-ms",
8358
+ "information",
8359
+ "sound-level",
8360
+ "signal-level",
8361
+ "colour-temperature",
8362
+ "angle",
8363
+ "bitrate",
8364
+ "rate",
8365
+ "mass",
8366
+ "index",
8367
+ "ratio",
8368
+ "count",
8369
+ "none",
8370
+ "device"
8371
+ ];
8372
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8373
+ new Set(READING_QUANTITIES);
8374
+ function readingMeta(quantity, extras) {
8375
+ return {
8376
+ quantity,
8377
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8378
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8379
+ };
8380
+ }
8335
8381
  /** Shared Zod schemas used across streaming capabilities. */
8336
8382
  var CamProfileSchema = _enum([
8337
8383
  "high",
@@ -8359,8 +8405,8 @@ var CamStreamKindSchema = _enum([
8359
8405
  "derived"
8360
8406
  ]);
8361
8407
  var CamStreamResolutionSchema = object({
8362
- width: number().int().positive(),
8363
- height: number().int().positive()
8408
+ width: number().int().positive().meta(readingMeta("none")),
8409
+ height: number().int().positive().meta(readingMeta("none"))
8364
8410
  });
8365
8411
  var CameraStreamSchema = object({
8366
8412
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9089,14 +9135,6 @@ Object.freeze({
9089
9135
  {
9090
9136
  "path": "aqi",
9091
9137
  "valueType": "number"
9092
- },
9093
- {
9094
- "path": "unit",
9095
- "valueType": "string"
9096
- },
9097
- {
9098
- "path": "precision",
9099
- "valueType": "number"
9100
9138
  }
9101
9139
  ],
9102
9140
  "alarm-panel": [
@@ -9132,20 +9170,10 @@ Object.freeze({
9132
9170
  "valueType": "boolean"
9133
9171
  }
9134
9172
  ],
9135
- "ambient-light-sensor": [
9136
- {
9137
- "path": "lux",
9138
- "valueType": "number"
9139
- },
9140
- {
9141
- "path": "unit",
9142
- "valueType": "string"
9143
- },
9144
- {
9145
- "path": "precision",
9146
- "valueType": "number"
9147
- }
9148
- ],
9173
+ "ambient-light-sensor": [{
9174
+ "path": "lux",
9175
+ "valueType": "number"
9176
+ }],
9149
9177
  "audio-metrics": [
9150
9178
  {
9151
9179
  "path": "ts",
@@ -9225,6 +9253,10 @@ Object.freeze({
9225
9253
  "path": "lastUpdated",
9226
9254
  "valueType": "number"
9227
9255
  },
9256
+ {
9257
+ "path": "volts",
9258
+ "valueType": "number"
9259
+ },
9228
9260
  {
9229
9261
  "path": "lastContactAt",
9230
9262
  "valueType": "number"
@@ -9467,10 +9499,6 @@ Object.freeze({
9467
9499
  "datetime"
9468
9500
  ]
9469
9501
  },
9470
- {
9471
- "path": "unit",
9472
- "valueType": "string"
9473
- },
9474
9502
  {
9475
9503
  "path": "min",
9476
9504
  "valueType": "number"
@@ -9482,19 +9510,6 @@ Object.freeze({
9482
9510
  {
9483
9511
  "path": "step",
9484
9512
  "valueType": "number"
9485
- },
9486
- {
9487
- "path": "precision",
9488
- "valueType": "number"
9489
- },
9490
- {
9491
- "path": "format",
9492
- "valueType": "enum",
9493
- "enumValues": [
9494
- "date",
9495
- "time",
9496
- "datetime"
9497
- ]
9498
9513
  }
9499
9514
  ],
9500
9515
  "cover": [
@@ -9735,13 +9750,8 @@ Object.freeze({
9735
9750
  "path": "value",
9736
9751
  "valueType": "string"
9737
9752
  }, {
9738
- "path": "format",
9739
- "valueType": "enum",
9740
- "enumValues": [
9741
- "date",
9742
- "time",
9743
- "datetime"
9744
- ]
9753
+ "path": "timestamp",
9754
+ "valueType": "boolean"
9745
9755
  }],
9746
9756
  "event-emitter": [
9747
9757
  {
@@ -9848,20 +9858,10 @@ Object.freeze({
9848
9858
  "valueType": "number"
9849
9859
  }
9850
9860
  ],
9851
- "humidity-sensor": [
9852
- {
9853
- "path": "percent",
9854
- "valueType": "number"
9855
- },
9856
- {
9857
- "path": "unit",
9858
- "valueType": "string"
9859
- },
9860
- {
9861
- "path": "precision",
9862
- "valueType": "number"
9863
- }
9864
- ],
9861
+ "humidity-sensor": [{
9862
+ "path": "percent",
9863
+ "valueType": "number"
9864
+ }],
9865
9865
  "image": [{
9866
9866
  "path": "url",
9867
9867
  "valueType": "string"
@@ -10200,20 +10200,10 @@ Object.freeze({
10200
10200
  "valueType": "boolean"
10201
10201
  }
10202
10202
  ],
10203
- "numeric-sensor": [
10204
- {
10205
- "path": "value",
10206
- "valueType": "number"
10207
- },
10208
- {
10209
- "path": "unit",
10210
- "valueType": "string"
10211
- },
10212
- {
10213
- "path": "precision",
10214
- "valueType": "number"
10215
- }
10216
- ],
10203
+ "numeric-sensor": [{
10204
+ "path": "value",
10205
+ "valueType": "number"
10206
+ }],
10217
10207
  "pet-feeder": [
10218
10208
  {
10219
10209
  "path": "foodLevel",
@@ -10305,14 +10295,6 @@ Object.freeze({
10305
10295
  {
10306
10296
  "path": "powerFactor",
10307
10297
  "valueType": "number"
10308
- },
10309
- {
10310
- "path": "unit",
10311
- "valueType": "string"
10312
- },
10313
- {
10314
- "path": "precision",
10315
- "valueType": "number"
10316
10298
  }
10317
10299
  ],
10318
10300
  "presence": [
@@ -10341,20 +10323,10 @@ Object.freeze({
10341
10323
  "valueType": "number"
10342
10324
  }
10343
10325
  ],
10344
- "pressure-sensor": [
10345
- {
10346
- "path": "hpa",
10347
- "valueType": "number"
10348
- },
10349
- {
10350
- "path": "unit",
10351
- "valueType": "string"
10352
- },
10353
- {
10354
- "path": "precision",
10355
- "valueType": "number"
10356
- }
10357
- ],
10326
+ "pressure-sensor": [{
10327
+ "path": "hpa",
10328
+ "valueType": "number"
10329
+ }],
10358
10330
  "privacy-mask": [{
10359
10331
  "path": "enabled",
10360
10332
  "valueType": "boolean"
@@ -10665,20 +10637,10 @@ Object.freeze({
10665
10637
  "path": "tampered",
10666
10638
  "valueType": "boolean"
10667
10639
  }],
10668
- "temperature-sensor": [
10669
- {
10670
- "path": "celsius",
10671
- "valueType": "number"
10672
- },
10673
- {
10674
- "path": "unit",
10675
- "valueType": "string"
10676
- },
10677
- {
10678
- "path": "precision",
10679
- "valueType": "number"
10680
- }
10681
- ],
10640
+ "temperature-sensor": [{
10641
+ "path": "celsius",
10642
+ "valueType": "number"
10643
+ }],
10682
10644
  "update": [
10683
10645
  {
10684
10646
  "path": "currentVersion",
@@ -10789,10 +10751,6 @@ Object.freeze({
10789
10751
  "path": "temperature",
10790
10752
  "valueType": "number"
10791
10753
  },
10792
- {
10793
- "path": "temperatureUnit",
10794
- "valueType": "string"
10795
- },
10796
10754
  {
10797
10755
  "path": "humidity",
10798
10756
  "valueType": "number"
@@ -10801,18 +10759,10 @@ Object.freeze({
10801
10759
  "path": "pressure",
10802
10760
  "valueType": "number"
10803
10761
  },
10804
- {
10805
- "path": "pressureUnit",
10806
- "valueType": "string"
10807
- },
10808
10762
  {
10809
10763
  "path": "windSpeed",
10810
10764
  "valueType": "number"
10811
10765
  },
10812
- {
10813
- "path": "windSpeedUnit",
10814
- "valueType": "string"
10815
- },
10816
10766
  {
10817
10767
  "path": "windBearing",
10818
10768
  "valueType": "number"
@@ -19360,7 +19310,8 @@ var CompositionProblemSchema = object({
19360
19310
  "item-field-reserved",
19361
19311
  "target-missing",
19362
19312
  "target-unreadable",
19363
- "not-replaceable"
19313
+ "not-replaceable",
19314
+ "native-capability-replace"
19364
19315
  ]),
19365
19316
  path: string(),
19366
19317
  message: string()
@@ -19418,9 +19369,11 @@ var CompositionFieldPlanSchema = object({
19418
19369
  reads: array(CompositionFieldReadSchema)
19419
19370
  });
19420
19371
  /**
19421
- * What a feature does to its target (D663): `new` builds a capability on a new
19422
- * device; on an existing device it `add`s a capability the device lacks, or
19423
- * `replace`s named fields of one it has natively.
19372
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19373
+ * new device; on an existing device it `add`s a capability the device lacks.
19374
+ * Replacing a capability the device already serves is refused
19375
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19376
+ * wire for a claim the hub already holds.
19424
19377
  */
19425
19378
  var CompositionFeatureModeSchema = _enum([
19426
19379
  "new",
@@ -21313,12 +21266,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
21313
21266
  * providers detect renames by matching `uniqueId` and patch `id`
21314
21267
  * in-place so dispatch keeps working.
21315
21268
  *
21316
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
21317
- * `precision`) live in the cap STATUS SLICE so they flow live with every
21318
- * state push and are the single source of truth. The `device_class` tag is
21319
- * an HA-specific input that is consumed at the discovery boundary
21320
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
21321
- * stored after that point.
21269
+ * **Display unit and precision are NOT stored here.** They are a read-time
21270
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21271
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21272
+ * neither is repeated on each sample. The `device_class` tag is an
21273
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21274
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
21322
21275
  *
21323
21276
  * The `raw` escape hatch carries provider-specific attributes that don't
21324
21277
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -21332,6 +21285,14 @@ var SourceInfoSchema = object({
21332
21285
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
21333
21286
  * Used to detect rename when `id` changes. */
21334
21287
  uniqueId: string().optional(),
21288
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21289
+ quantity: ReadingQuantitySchema.optional(),
21290
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21291
+ dateTimeShape: _enum([
21292
+ "date",
21293
+ "time",
21294
+ "datetime"
21295
+ ]).optional(),
21335
21296
  /** Provider-specific extras that don't fit the structured slots.
21336
21297
  * Whitelist what goes here per provider — do NOT dump entire HA
21337
21298
  * attribute blobs (would explode the meta JSON column). */
@@ -42359,7 +42320,8 @@ method(object({
42359
42320
  "enum"
42360
42321
  ]),
42361
42322
  enumValues: array(string()).optional(),
42362
- item: boolean().optional()
42323
+ item: boolean().optional(),
42324
+ quantity: ReadingQuantitySchema.optional()
42363
42325
  })).readonly(),
42364
42326
  itemArray: object({
42365
42327
  path: string(),
@@ -46426,8 +46388,8 @@ var MaskPolygonShapeSchema = object({
46426
46388
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
46427
46389
  var MaskGridShapeSchema = object({
46428
46390
  kind: literal("grid"),
46429
- gridWidth: number(),
46430
- gridHeight: number(),
46391
+ gridWidth: number().meta(readingMeta("none")),
46392
+ gridHeight: number().meta(readingMeta("none")),
46431
46393
  cells: array(boolean())
46432
46394
  });
46433
46395
  discriminatedUnion("kind", [
@@ -59753,27 +59715,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
59753
59715
  */
59754
59716
  var AirQualitySensorStatusSchema = object({
59755
59717
  /** Carbon dioxide concentration in ppm. */
59756
- co2Ppm: number().min(0).optional(),
59718
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
59757
59719
  /** Total volatile organic compounds in ppb. */
59758
- vocPpb: number().min(0).optional(),
59720
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
59759
59721
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
59760
- pm25: number().min(0).optional(),
59722
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
59761
59723
  /** Particulate matter ≤ 10 μm in µg/m³. */
59762
- pm10: number().min(0).optional(),
59724
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
59763
59725
  /** Composite AQI value (typically 0..500). */
59764
- aqi: number().optional(),
59726
+ aqi: number().optional().meta(readingMeta("index")),
59765
59727
  /** Ms epoch when the slice was last updated. */
59766
- lastFetchedAt: number(),
59767
- /** Live display unit of the single metric this slice carries (e.g. HA
59768
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
59769
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
59770
- * per slice is unambiguous. */
59771
- unit: string().optional(),
59772
- /** Suggested decimal places for numeric display.
59773
- * Populated live from the upstream source when provided (e.g. HA
59774
- * `attributes.suggested_display_precision`). Falls back to
59775
- * auto-formatting when absent. */
59776
- precision: number().int().min(0).max(10).optional()
59728
+ lastFetchedAt: number()
59777
59729
  });
59778
59730
  var airQualitySensorCapability = {
59779
59731
  name: "air-quality-sensor",
@@ -59810,12 +59762,12 @@ var airQualitySensorCapability = {
59810
59762
  },
59811
59763
  pm25: {
59812
59764
  kind: "gauge",
59813
- unit: "ug/m3",
59765
+ unit: "µg/m³",
59814
59766
  deadband: 1
59815
59767
  },
59816
59768
  pm10: {
59817
59769
  kind: "gauge",
59818
- unit: "ug/m3",
59770
+ unit: "µg/m³",
59819
59771
  deadband: 1
59820
59772
  },
59821
59773
  aqi: {
@@ -59833,22 +59785,13 @@ var airQualitySensorCapability = {
59833
59785
  * entries with `device_class: illuminance`.
59834
59786
  */
59835
59787
  var AmbientLightSensorStatusSchema = object({
59836
- /** Current illuminance in lux (lx). */
59837
- lux: number().min(0).meta({
59838
- unit: "lx",
59788
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
59789
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
59790
+ precision: 0,
59839
59791
  label: "Illuminance"
59840
- }),
59792
+ })),
59841
59793
  /** Ms epoch when the slice was last updated. */
59842
- lastFetchedAt: number(),
59843
- /** Live display unit from the upstream source (e.g. HA
59844
- * `attributes.unit_of_measurement`). The UI prefers this over the
59845
- * role's canonical unit. Absent → fall back to the canonical unit. */
59846
- unit: string().optional(),
59847
- /** Suggested decimal places for numeric display.
59848
- * Populated live from the upstream source when provided (e.g. HA
59849
- * `attributes.suggested_display_precision`). Falls back to
59850
- * auto-formatting when absent. */
59851
- precision: number().int().min(0).max(10).optional()
59794
+ lastFetchedAt: number()
59852
59795
  });
59853
59796
  var ambientLightSensorCapability = {
59854
59797
  name: "ambient-light-sensor",
@@ -59880,12 +59823,7 @@ var ambientLightSensorCapability = {
59880
59823
  } } },
59881
59824
  /** Clock fields: written, but excluded from the compare that decides
59882
59825
  * whether persisting is worth a SQLite commit. */
59883
- volatileStateFields: ["lastFetchedAt"],
59884
- customize: {
59885
- version: 1,
59886
- advanced: "derive",
59887
- targets: []
59888
- }
59826
+ volatileStateFields: ["lastFetchedAt"]
59889
59827
  };
59890
59828
  /**
59891
59829
  * Per-class audio metrics aggregated over a sliding window.
@@ -60112,10 +60050,7 @@ var BatteryStatusSchema = object({
60112
60050
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
60113
60051
  * declines to band a non-finite reading, which is the correct shape.
60114
60052
  */
60115
- percentage: number().min(0).max(100).nullable().meta({
60116
- unit: "%",
60117
- label: "Charge"
60118
- }),
60053
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
60119
60054
  /**
60120
60055
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
60121
60056
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -60142,7 +60077,14 @@ var BatteryStatusSchema = object({
60142
60077
  */
60143
60078
  sleeping: boolean().meta({ label: "Sleeping" }),
60144
60079
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60145
- lastUpdated: number(),
60080
+ lastUpdated: number().meta(readingMeta("none")),
60081
+ /**
60082
+ * Battery voltage in volts, for a cell the firmware reports as voltage
60083
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
60084
+ * that only report charge. A second reading on this cap, not a second
60085
+ * `numeric-sensor` on the same device.
60086
+ */
60087
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
60146
60088
  /**
60147
60089
  * Ms epoch of the last time the device PROVED it was reachable — a
60148
60090
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -60194,7 +60136,6 @@ var BATTERY_DEVICE_TYPES = [
60194
60136
  /** Generic Customize target for the percentage reading (D936). */
60195
60137
  var batteryCustomize = {
60196
60138
  version: 1,
60197
- advanced: "derive",
60198
60139
  targets: [{
60199
60140
  id: "percentage",
60200
60141
  cardinality: "scalar",
@@ -60203,7 +60144,7 @@ var batteryCustomize = {
60203
60144
  stateField: "percentage"
60204
60145
  },
60205
60146
  deviceTypes: BATTERY_DEVICE_TYPES,
60206
- apply: "add-or-replace",
60147
+ apply: "add",
60207
60148
  path: "percentage",
60208
60149
  sources: [{
60209
60150
  kind: "field",
@@ -60324,10 +60265,16 @@ onStatusChanged: { data: object({
60324
60265
  /**
60325
60266
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
60326
60267
  */
60327
- history: { fields: { percentage: {
60328
- kind: "gauge",
60329
- unit: "%"
60330
- } } },
60268
+ history: { fields: {
60269
+ percentage: {
60270
+ kind: "gauge",
60271
+ unit: "%"
60272
+ },
60273
+ volts: {
60274
+ kind: "gauge",
60275
+ unit: "V"
60276
+ }
60277
+ } },
60331
60278
  customize: batteryCustomize,
60332
60279
  /** Clock fields: written, but excluded from the compare that decides
60333
60280
  * whether persisting is worth a SQLite commit. */
@@ -60374,12 +60321,7 @@ var binaryCapability = {
60374
60321
  history: { fields: { on: { kind: "boolean" } } },
60375
60322
  /** Clock fields: written, but excluded from the compare that decides
60376
60323
  * whether persisting is worth a SQLite commit. */
60377
- volatileStateFields: ["lastChangedAt"],
60378
- customize: {
60379
- version: 1,
60380
- advanced: "derive",
60381
- targets: []
60382
- }
60324
+ volatileStateFields: ["lastChangedAt"]
60383
60325
  };
60384
60326
  /**
60385
60327
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -60395,7 +60337,7 @@ var binaryCapability = {
60395
60337
  */
60396
60338
  var BrightnessStatusSchema = object({
60397
60339
  /** Current level as 0..100 inclusive. Firmware-reported. */
60398
- percentage: number().min(0).max(100),
60340
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
60399
60341
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
60400
60342
  lastChangedAt: number()
60401
60343
  });
@@ -60876,21 +60818,21 @@ var ClimateControlStatusSchema = object({
60876
60818
  /** Single setpoint in Celsius. Used by single-target modes
60877
60819
  * (heat / cool / dry). Null when the device is in a range mode
60878
60820
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
60879
- target: number().nullable(),
60821
+ target: number().nullable().meta(readingMeta("temperature")),
60880
60822
  /** Upper bound of the dual setpoint in Celsius. Populated only
60881
60823
  * in `heat_cool` mode. */
60882
- targetHigh: number().nullable(),
60824
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
60883
60825
  /** Lower bound of the dual setpoint in Celsius. Populated only
60884
60826
  * in `heat_cool` mode. */
60885
- targetLow: number().nullable(),
60827
+ targetLow: number().nullable().meta(readingMeta("temperature")),
60886
60828
  /** Target relative humidity (0..100). Null when the device has no
60887
60829
  * humidity control. */
60888
- targetHumidity: number().min(0).max(100).nullable(),
60830
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60889
60831
  /** Current measured temperature in Celsius. Read-only — pushed by
60890
60832
  * the device's internal sensor. */
60891
- currentTemp: number().nullable(),
60833
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
60892
60834
  /** Current measured relative humidity (0..100). Read-only. */
60893
- currentHumidity: number().min(0).max(100).nullable(),
60835
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60894
60836
  /** Vertical louver swing on/off. Null when the device has no vertical
60895
60837
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
60896
60838
  swingVertical: boolean().nullable(),
@@ -60898,11 +60840,11 @@ var ClimateControlStatusSchema = object({
60898
60840
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
60899
60841
  swingHorizontal: boolean().nullable(),
60900
60842
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
60901
- minTemp: number().nullable().optional(),
60843
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
60902
60844
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
60903
- maxTemp: number().nullable().optional(),
60845
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
60904
60846
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
60905
- tempStep: number().positive().nullable().optional(),
60847
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
60906
60848
  /** Ms epoch when the slice was last updated (push or command). */
60907
60849
  lastFetchedAt: number()
60908
60850
  });
@@ -61044,17 +60986,17 @@ var climateControlCapability = {
61044
60986
  * modes to expose in the picker without a slice fetch.
61045
60987
  */
61046
60988
  var RgbTripletSchema = object({
61047
- r: number().int().min(0).max(255),
61048
- g: number().int().min(0).max(255),
61049
- b: number().int().min(0).max(255)
60989
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
60990
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
60991
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
61050
60992
  });
61051
60993
  var HsvTripletSchema = object({
61052
60994
  /** Hue in degrees, 0..360. */
61053
- h: number().min(0).max(360),
60995
+ h: number().min(0).max(360).meta(readingMeta("none")),
61054
60996
  /** Saturation as 0..100 inclusive. */
61055
- s: number().min(0).max(100),
60997
+ s: number().min(0).max(100).meta(readingMeta("none")),
61056
60998
  /** Value/brightness as 0..100 inclusive. */
61057
- v: number().min(0).max(100)
60999
+ v: number().min(0).max(100).meta(readingMeta("none"))
61058
61000
  });
61059
61001
  var ColorInputSchema = discriminatedUnion("mode", [
61060
61002
  object({
@@ -61083,7 +61025,7 @@ var ColorStatusSchema = object({
61083
61025
  /** Populated when `mode === 'hsv'`. */
61084
61026
  hsv: HsvTripletSchema.optional(),
61085
61027
  /** Populated when `mode === 'mired'`. */
61086
- mireds: number().int().optional(),
61028
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
61087
61029
  /** Ms epoch of the last operator-driven change. */
61088
61030
  lastChangedAt: number()
61089
61031
  });
@@ -61292,18 +61234,12 @@ var FORM_OF_KIND = {
61292
61234
  function consumableFormOf(item) {
61293
61235
  return item.form ?? FORM_OF_KIND[consumableKindOf(item)];
61294
61236
  }
61295
- /** Units a remaining-life counter is reported in. Closed: the panel renders
61296
- * each. */
61297
- var ConsumableRemainingUnitSchema = _enum([
61298
- "days",
61299
- "hours",
61300
- "cycles"
61301
- ]);
61302
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
61303
- * days). `value` null = unknown (D393). */
61237
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
61238
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
61239
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
61304
61240
  var ConsumableRemainingSchema = object({
61305
- value: number().min(0).nullable(),
61306
- unit: ConsumableRemainingUnitSchema
61241
+ value: number().min(0).nullable().meta(readingMeta("device")),
61242
+ kind: ConsumableRemainingKindSchema
61307
61243
  });
61308
61244
  /** A single consumable item. Either a continuous `level` (remaining
61309
61245
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -61316,7 +61252,7 @@ var ConsumableItemSchema = object({
61316
61252
  /** Display name. */
61317
61253
  label: string().min(1),
61318
61254
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
61319
- level: number().min(0).max(100).nullable(),
61255
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61320
61256
  /** The provider's discrete verdict when known (binary mode). */
61321
61257
  status: ConsumableStatusSchema.nullable(),
61322
61258
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -61398,8 +61334,8 @@ var consumablesCustomize = {
61398
61334
  transforms: [{ kind: "identity" }]
61399
61335
  }],
61400
61336
  literals: [{
61401
- path: "remaining.unit",
61402
- values: [...ConsumableRemainingUnitSchema.options]
61337
+ path: "remaining.kind",
61338
+ values: [...ConsumableRemainingKindSchema.options]
61403
61339
  }]
61404
61340
  },
61405
61341
  {
@@ -61528,7 +61464,6 @@ reset: method(object({
61528
61464
  },
61529
61465
  "remaining.value": {
61530
61466
  kind: "gauge",
61531
- unitFrom: "remaining.unit",
61532
61467
  deadband: 1,
61533
61468
  displayName: "Remaining"
61534
61469
  }
@@ -61598,10 +61533,9 @@ var contactCapability = {
61598
61533
  * (empty array for the other kinds). It lives in the slice because
61599
61534
  * HA `select.*` entities may update their options at runtime.
61600
61535
  *
61601
- * The `unit`/`min`/`max`/`step` fields are meaningful for
61602
- * `kind === 'numeric'` and are absent (undefined) for all other
61603
- * kinds. They are populated live from HA attributes on each state
61604
- * push — consistent with `options` and the sensor-unit approach.
61536
+ * Numeric `min` / `max` / `step` live in the slice and share the device
61537
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
61538
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
61605
61539
  */
61606
61540
  var ControlStatusSchema = object({
61607
61541
  kind: _enum([
@@ -61613,39 +61547,17 @@ var ControlStatusSchema = object({
61613
61547
  /** Current value. Numeric kind → number; select/text/datetime → string.
61614
61548
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
61615
61549
  * or time-only — HA accepts all three forms). */
61616
- value: union([number(), string()]),
61550
+ value: union([number().meta(readingMeta("device")), string()]),
61617
61551
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
61618
61552
  options: array(string()),
61619
61553
  /** Ms epoch when the slice was last updated. */
61620
61554
  lastChangedAt: number(),
61621
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
61622
- * Populated live from HA `attributes.unit_of_measurement` on each push.
61623
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61624
- unit: string().optional(),
61625
- /** Minimum allowed value. Populated live from HA `attributes.min`.
61626
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61627
- min: number().optional(),
61628
- /** Maximum allowed value. Populated live from HA `attributes.max`.
61629
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61630
- max: number().optional(),
61631
- /** Step increment. Populated live from HA `attributes.step`.
61632
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
61633
- step: number().optional(),
61634
- /** Suggested decimal places for numeric display. Populated live from HA
61635
- * `attributes.suggested_display_precision`, or derived from `step` when the
61636
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
61637
- * other kinds. Falls back to auto-formatting when absent. */
61638
- precision: number().int().min(0).max(10).optional(),
61639
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
61640
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
61641
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
61642
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
61643
- * `has_date`/`has_time` attributes on each push. */
61644
- format: _enum([
61645
- "date",
61646
- "time",
61647
- "datetime"
61648
- ]).optional()
61555
+ /** Minimum allowed value, in the device quantity's base unit. */
61556
+ min: number().optional().meta(readingMeta("device")),
61557
+ /** Maximum allowed value, in the device quantity's base unit. */
61558
+ max: number().optional().meta(readingMeta("device")),
61559
+ /** Step increment, in the device quantity's base unit. */
61560
+ step: number().optional().meta(readingMeta("device"))
61649
61561
  });
61650
61562
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
61651
61563
  object({
@@ -61733,9 +61645,9 @@ var CoverStatusSchema = object({
61733
61645
  state: CoverStateSchema,
61734
61646
  /** 0 = fully closed, 100 = fully open. Null when the device has
61735
61647
  * no intermediate position surface. */
61736
- position: number().min(0).max(100).nullable(),
61648
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61737
61649
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
61738
- tiltPosition: number().min(0).max(100).nullable(),
61650
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61739
61651
  /** Ms epoch when the slice was last updated. */
61740
61652
  lastChangedAt: number()
61741
61653
  });
@@ -61838,9 +61750,9 @@ var NormalizedRangeSchema$1 = object({
61838
61750
  var DayNightStatusSchema = object({
61839
61751
  mode: DayNightModeSchema,
61840
61752
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
61841
- sensitivity: number().optional(),
61753
+ sensitivity: number().optional().meta(readingMeta("none")),
61842
61754
  /** Delay before the IR-cut filter flips, in seconds. */
61843
- switchDelaySec: number().optional(),
61755
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
61844
61756
  lastFetchedAt: number()
61845
61757
  });
61846
61758
  /**
@@ -62058,11 +61970,11 @@ var DishwasherStatusSchema = object({
62058
61970
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
62059
61971
  availablePrograms: array(string()),
62060
61972
  /** Seconds remaining on the active cycle, or null when not running / not known. */
62061
- remainingSeconds: number().int().nonnegative().nullable(),
61973
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
62062
61974
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
62063
- delayedStartSeconds: number().int().nonnegative().nullable(),
61975
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
62064
61976
  /** Cycle completion 0..100, or null when not applicable. */
62065
- progressPercent: number().min(0).max(100).nullable(),
61977
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62066
61978
  /** True when the door is reported open; null when the door state is not known. */
62067
61979
  doorOpen: boolean().nullable(),
62068
61980
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -62086,7 +61998,7 @@ var DishwasherStatusSchema = object({
62086
61998
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
62087
61999
  machineCareDue: boolean().nullable(),
62088
62000
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
62089
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
62001
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
62090
62002
  /** Option states: null when the appliance does not expose the option or has not said. */
62091
62003
  halfLoad: boolean().nullable(),
62092
62004
  hygienePlus: boolean().nullable(),
@@ -62101,7 +62013,7 @@ var DishwasherStatusSchema = object({
62101
62013
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
62102
62014
  activeEvents: array(string()),
62103
62015
  /** Numeric error code when the appliance reports one; null otherwise. */
62104
- errorCode: number().nullable(),
62016
+ errorCode: number().nullable().meta(readingMeta("none")),
62105
62017
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
62106
62018
  errorLabel: string().nullable(),
62107
62019
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -62332,7 +62244,7 @@ var DoorbellStatusSchema = object({
62332
62244
  /** Ms epoch of the last press. null = never observed since this provider started. */
62333
62245
  lastPressedAt: number().nullable(),
62334
62246
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
62335
- pressCountSinceStart: number()
62247
+ pressCountSinceStart: number().meta(readingMeta("count"))
62336
62248
  });
62337
62249
  var DoorbellPressEventSchema = object({
62338
62250
  deviceId: number(),
@@ -62397,17 +62309,7 @@ onPressed: {
62397
62309
  history: "none",
62398
62310
  customize: doorbellCustomize
62399
62311
  };
62400
- /**
62401
- * Enum-state sensor — a string value picked from a finite option set.
62402
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
62403
- * states, weather conditions, contact-source channels, mode strings).
62404
- * The option set is stable across observations; it is persisted in the
62405
- * device config blob (via the control slice at adoption) and is NOT
62406
- * stored in `sourceInfo`.
62407
- */
62408
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
62409
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
62410
- var EnumSensorDateTimeFormatSchema = _enum([
62312
+ _enum([
62411
62313
  "date",
62412
62314
  "time",
62413
62315
  "datetime"
@@ -62415,12 +62317,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
62415
62317
  var EnumSensorStatusSchema = object({
62416
62318
  value: string(),
62417
62319
  /**
62418
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
62419
- * as a locale date/time/datetime; absent for plain enum sensors. HA
62420
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
62421
- * a time-only sensor → `'time'`.
62320
+ * Set when `value` is a timestamp rather than an enum state, so history
62321
+ * does not learn ISO strings as states. The display shape (date / time /
62322
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
62422
62323
  */
62423
- format: EnumSensorDateTimeFormatSchema.optional(),
62324
+ timestamp: boolean().optional(),
62424
62325
  /** Ms epoch when the slice was last updated. */
62425
62326
  lastFetchedAt: number()
62426
62327
  });
@@ -62447,11 +62348,11 @@ var enumSensorCapability = {
62447
62348
  * History (D850): the value as an `enum` series (an appliance's program or
62448
62349
  * operation state, an HVAC action). The option set is per device, so the
62449
62350
  * value list is LEARNED as values are first seen (bounded). A sensor with a
62450
- * `format` carries a timestamp, not a state, and is not recorded.
62351
+ * `timestamp` carries a clock, not a state, and is not recorded.
62451
62352
  */
62452
62353
  history: { fields: { value: {
62453
62354
  kind: "enum",
62454
- skipWhenSet: "format"
62355
+ skipWhenSet: "timestamp"
62455
62356
  } } },
62456
62357
  /** Clock fields: written, but excluded from the compare that decides
62457
62358
  * whether persisting is worth a SQLite commit. */
@@ -62471,13 +62372,13 @@ var EventFireSchema = object({
62471
62372
  deviceId: number(),
62472
62373
  eventType: string(),
62473
62374
  data: record(string(), unknown()).nullable(),
62474
- timestamp: number(),
62475
- seq: number()
62375
+ timestamp: number().meta(readingMeta("none")),
62376
+ seq: number().meta(readingMeta("count"))
62476
62377
  });
62477
62378
  var EventEmitterStatusSchema = object({
62478
62379
  eventTypes: array(string()),
62479
62380
  lastEvent: EventFireSchema.nullable(),
62480
- eventCountSinceStart: number()
62381
+ eventCountSinceStart: number().meta(readingMeta("count"))
62481
62382
  });
62482
62383
  var eventEmitterCapability = {
62483
62384
  name: "event-emitter",
@@ -62930,10 +62831,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
62930
62831
  var FanControlStatusSchema = object({
62931
62832
  /** Active speed as 0..100 inclusive. Null when the device has no
62932
62833
  * speed surface (single-speed fan). */
62933
- percentage: number().min(0).max(100).nullable(),
62834
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62934
62835
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
62935
62836
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
62936
- percentageStep: number().positive().optional(),
62837
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
62937
62838
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
62938
62839
  * — empty string when no preset is active or supported. */
62939
62840
  preset: string(),
@@ -63054,7 +62955,7 @@ var FeatureProbeStatusSchema = object({
63054
62955
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
63055
62956
  model: string().nullable(),
63056
62957
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
63057
- channelCount: number().nullable(),
62958
+ channelCount: number().nullable().meta(readingMeta("count")),
63058
62959
  /**
63059
62960
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
63060
62961
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -63179,7 +63080,7 @@ var gasCapability = {
63179
63080
  };
63180
63081
  var HeatingValveStatusSchema = object({
63181
63082
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
63182
- position: number().min(0).max(100).nullable(),
63083
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63183
63084
  /** Ms epoch when the slice was last updated (push or poll). */
63184
63085
  lastFetchedAt: number()
63185
63086
  });
@@ -63222,9 +63123,9 @@ var HumidifierStatusSchema = object({
63222
63123
  /** Whether the humidifier is currently on. */
63223
63124
  on: boolean(),
63224
63125
  /** Current measured relative humidity (0..100). Null when not reported. */
63225
- currentHumidity: number().min(0).max(100).nullable(),
63126
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63226
63127
  /** Target relative humidity (0..100). Null when no setpoint surface. */
63227
- targetHumidity: number().min(0).max(100).nullable(),
63128
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
63228
63129
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
63229
63130
  * device has no mode surface. */
63230
63131
  mode: string().nullable(),
@@ -63234,9 +63135,9 @@ var HumidifierStatusSchema = object({
63234
63135
  * `idle` / `off`). Null when the device doesn't report it. */
63235
63136
  action: string().nullable(),
63236
63137
  /** HA `min_humidity` attribute. Null → UI uses 0. */
63237
- minHumidity: number().nullable(),
63138
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
63238
63139
  /** HA `max_humidity` attribute. Null → UI uses 100. */
63239
- maxHumidity: number().nullable(),
63140
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
63240
63141
  /** Ms epoch when the slice was last updated. */
63241
63142
  lastChangedAt: number()
63242
63143
  });
@@ -63306,22 +63207,13 @@ var humidifierCapability = {
63306
63207
  * `sourceInfo`.
63307
63208
  */
63308
63209
  var HumiditySensorStatusSchema = object({
63309
- /** Current relative humidity, 0..100. */
63310
- percent: number().min(0).max(100).meta({
63311
- unit: "%",
63210
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
63211
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
63212
+ precision: 0,
63312
63213
  label: "Humidity"
63313
- }),
63214
+ })),
63314
63215
  /** Ms epoch when the slice was last updated. */
63315
- lastFetchedAt: number(),
63316
- /** Live display unit from the upstream source (e.g. HA
63317
- * `attributes.unit_of_measurement`). The UI prefers this over the
63318
- * role's canonical unit. Absent → fall back to the canonical unit. */
63319
- unit: string().optional(),
63320
- /** Suggested decimal places for numeric display.
63321
- * Populated live from the upstream source when provided (e.g. HA
63322
- * `attributes.suggested_display_precision`). Falls back to
63323
- * auto-formatting when absent. */
63324
- precision: number().int().min(0).max(10).optional()
63216
+ lastFetchedAt: number()
63325
63217
  });
63326
63218
  var humiditySensorCapability = {
63327
63219
  name: "humidity-sensor",
@@ -63352,12 +63244,7 @@ var humiditySensorCapability = {
63352
63244
  } } },
63353
63245
  /** Clock fields: written, but excluded from the compare that decides
63354
63246
  * whether persisting is worth a SQLite commit. */
63355
- volatileStateFields: ["lastFetchedAt"],
63356
- customize: {
63357
- version: 1,
63358
- advanced: "derive",
63359
- targets: []
63360
- }
63247
+ volatileStateFields: ["lastFetchedAt"]
63361
63248
  };
63362
63249
  /**
63363
63250
  * Image display cap. Models a single still image exposed by an integration —
@@ -63378,7 +63265,7 @@ var ImageStatusSchema = object({
63378
63265
  * entity exposes no `entity_picture` (yet). */
63379
63266
  url: string().nullable(),
63380
63267
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
63381
- lastUpdated: number().nullable()
63268
+ lastUpdated: number().nullable().meta(readingMeta("none"))
63382
63269
  });
63383
63270
  var imageCapability = {
63384
63271
  name: "image",
@@ -63466,19 +63353,19 @@ var NormalizedRangeSchema = object({
63466
63353
  */
63467
63354
  var ImageSettingsStatusSchema = object({
63468
63355
  /** Normalized 0–100. */
63469
- brightness: number().optional(),
63356
+ brightness: number().optional().meta(readingMeta("none")),
63470
63357
  /** Normalized 0–100. */
63471
- contrast: number().optional(),
63358
+ contrast: number().optional().meta(readingMeta("none")),
63472
63359
  /** Normalized 0–100. */
63473
- saturation: number().optional(),
63360
+ saturation: number().optional().meta(readingMeta("none")),
63474
63361
  /** Normalized 0–100. */
63475
- sharpness: number().optional(),
63362
+ sharpness: number().optional().meta(readingMeta("none")),
63476
63363
  mirror: boolean().optional(),
63477
63364
  flip: boolean().optional(),
63478
63365
  rotate: ImageRotateSchema.optional(),
63479
63366
  whiteBalance: WhiteBalanceModeSchema.optional(),
63480
63367
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
63481
- warmth: number().optional(),
63368
+ warmth: number().optional().meta(readingMeta("none")),
63482
63369
  exposureMode: ExposureModeSchema.optional(),
63483
63370
  backlightMode: BacklightModeSchema.optional(),
63484
63371
  lastFetchedAt: number()
@@ -63757,10 +63644,10 @@ var IntercomAbilitySchema = object({
63757
63644
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
63758
63645
  codecs: array(string()),
63759
63646
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
63760
- sampleRate: number(),
63647
+ sampleRate: number().meta(readingMeta("none")),
63761
63648
  duplex: _enum(["half", "full"]),
63762
63649
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
63763
- maxBacklogMs: number()
63650
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
63764
63651
  });
63765
63652
  var IntercomStatusSchema = object({
63766
63653
  /** True while a talk session is open. */
@@ -63987,13 +63874,13 @@ var LawnMowerControlStatusSchema = object({
63987
63874
  activity: LawnMowerActivitySchema,
63988
63875
  /** 0..100 battery percentage. Null when the device has no battery
63989
63876
  * reading. */
63990
- batteryLevel: number().min(0).max(100).nullable(),
63877
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63991
63878
  /** 0..100 mowing-completion percentage of the current task, or null when no
63992
63879
  * task is active / progress is unavailable. */
63993
- progressPercent: number().min(0).max(100).nullable(),
63880
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63994
63881
  /** Current device/event code (dynamic — mostly status, sometimes an error),
63995
63882
  * or null when unknown. */
63996
- currentCode: number().nullable(),
63883
+ currentCode: number().nullable().meta(readingMeta("none")),
63997
63884
  /** Human label for {@link currentCode}, or null when undecodable. */
63998
63885
  currentCodeLabel: string().nullable(),
63999
63886
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -64547,7 +64434,7 @@ var MediaPlayerStatusSchema = object({
64547
64434
  state: MediaPlayerStateSchema,
64548
64435
  /** Volume as 0..100 inclusive. Null when the device has no volume
64549
64436
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
64550
- volumeLevel: number().min(0).max(100).nullable(),
64437
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64551
64438
  /** Mute toggle distinct from volume=0. Null when no mute surface.
64552
64439
  * Pair with `DeviceFeature.MediaPlayerMute`. */
64553
64440
  isMuted: boolean().nullable(),
@@ -64560,10 +64447,10 @@ var MediaPlayerStatusSchema = object({
64560
64447
  currentMedia: MediaInfoSchema.nullable(),
64561
64448
  /** Current playback position in ms. Null when not seekable or
64562
64449
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
64563
- positionMs: number().int().nonnegative().nullable(),
64450
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
64564
64451
  /** Total duration of the current media in ms. Null when unknown
64565
64452
  * (live stream) or nothing is playing. */
64566
- durationMs: number().int().nonnegative().nullable(),
64453
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
64567
64454
  /** Shuffle toggle. Null when no shuffle surface.
64568
64455
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
64569
64456
  shuffle: boolean().nullable(),
@@ -64872,7 +64759,7 @@ var motionCustomize = {
64872
64759
  eventName: "onMotionChanged"
64873
64760
  },
64874
64761
  deviceTypes: [DeviceType.Camera],
64875
- apply: "replace",
64762
+ apply: "add",
64876
64763
  applyAuthority: {
64877
64764
  kind: "camera-sensors",
64878
64765
  timeoutField: "motionCooldownMs"
@@ -64969,7 +64856,7 @@ var MotionStatusSchema = object({
64969
64856
  * it, every provider seeded it at boot, and every session left it
64970
64857
  * `detected: true` after its detach.
64971
64858
  */
64972
- autoClearAfterMs: number().nullable()
64859
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
64973
64860
  });
64974
64861
  /**
64975
64862
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -65151,7 +65038,7 @@ var MotionZoneRegionSchema = object({
65151
65038
  * the grid region(s). */
65152
65039
  var MotionZoneStatusSchema = object({
65153
65040
  enabled: boolean(),
65154
- sensitivity: number(),
65041
+ sensitivity: number().meta(readingMeta("none")),
65155
65042
  /** Grid region(s). Today exactly one `grid` shape. */
65156
65043
  regions: array(MotionZoneRegionSchema),
65157
65044
  lastFetchedAt: number()
@@ -65521,7 +65408,7 @@ var NavigationStatusSchema = object({
65521
65408
  following: boolean(),
65522
65409
  flash: boolean(),
65523
65410
  lightMode: NavigationLightModeSchema,
65524
- lightLevel: number().min(40).max(100),
65411
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
65525
65412
  /** Ms epoch when the slice was last updated. */
65526
65413
  lastChangedAt: number()
65527
65414
  });
@@ -65655,13 +65542,13 @@ var NetworkLinkStatusSchema = object({
65655
65542
  * one whose reading has not landed must not be drawn at 0 %. Consumers
65656
65543
  * SKIP a null rather than coerce it.
65657
65544
  */
65658
- signalPercent: number().min(0).max(100).nullable(),
65545
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65659
65546
  /** Raw received signal strength in dBm, when the firmware reports one. */
65660
- rssiDbm: number().optional(),
65547
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
65661
65548
  /** Network name of a wireless link, when the firmware reports it. */
65662
65549
  ssid: string().optional(),
65663
65550
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
65664
- lastUpdated: number()
65551
+ lastUpdated: number().meta(readingMeta("none"))
65665
65552
  });
65666
65553
  var networkLinkCapability = {
65667
65554
  name: "network-link",
@@ -66046,7 +65933,7 @@ var NotifierStatusSchema = object({
66046
65933
  lastError: string().nullable(),
66047
65934
  /** Number of deliveries currently buffered server-side (rate-limited
66048
65935
  * notifiers may queue). 0 when the provider sends synchronously. */
66049
- queueDepth: number().int().nonnegative(),
65936
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
66050
65937
  /** Per-feature capability matrix — authoritative source for the
66051
65938
  * compose-form field gating. */
66052
65939
  supports: NotifierSupportsSchema
@@ -66138,11 +66025,9 @@ onSent: { data: object({
66138
66025
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
66139
66026
  * for: water flow, distance, weight, frequency, signal strength, …).
66140
66027
  *
66141
- * The `unit` and `precision` fields carry the live display metadata that
66142
- * the upstream source provides with each state push (HA
66143
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
66144
- * The UI reads them directly from the slice — they are the single source
66145
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
66028
+ * The quantity is not on this slice. It is chosen when the device is
66029
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
66030
+ * that quantity's base unit. `null` is a refused sample (D393).
66146
66031
  *
66147
66032
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
66148
66033
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -66151,15 +66036,8 @@ onSent: { data: object({
66151
66036
  * numeric values land here.
66152
66037
  */
66153
66038
  var NumericSensorStatusSchema = object({
66154
- value: number(),
66155
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
66156
- * Populated live from the upstream source on each state push.
66157
- * Absent for unitless sensors. */
66158
- unit: string().optional(),
66159
- /** Suggested decimal places for numeric display.
66160
- * Populated live from the upstream source when provided.
66161
- * Falls back to auto-formatting when absent. */
66162
- precision: number().int().min(0).max(10).optional(),
66039
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
66040
+ value: number().nullable().meta(readingMeta("device")),
66163
66041
  /** Ms epoch when the slice was last updated. */
66164
66042
  lastFetchedAt: number()
66165
66043
  });
@@ -66183,11 +66061,11 @@ var numericSensorCapability = {
66183
66061
  */
66184
66062
  durability: "restored",
66185
66063
  /**
66186
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
66064
+ * History (D850): recorded series. The unit is the device quantity's base,
66065
+ * resolved when the series is read — the cap has no single unit.
66187
66066
  */
66188
66067
  history: { fields: { value: {
66189
66068
  kind: "gauge",
66190
- unitFrom: "unit",
66191
66069
  minIntervalMs: 3e4
66192
66070
  } } },
66193
66071
  /** Clock fields: written, but excluded from the compare that decides
@@ -66479,23 +66357,26 @@ var PetFeederStatusSchema = object({
66479
66357
  /** Food currently in the bowl (grams). Null when the device has not
66480
66358
  * reported a reading yet. On dual-hopper models this is the combined
66481
66359
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
66482
- foodLevel: number().nullable(),
66360
+ foodLevel: number().nullable().meta(readingMeta("none")),
66483
66361
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
66484
66362
  * single-hopper models. */
66485
- food1: number().nullable(),
66363
+ food1: number().nullable().meta(readingMeta("none")),
66486
66364
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
66487
66365
  * single-hopper models. */
66488
- food2: number().nullable(),
66366
+ food2: number().nullable().meta(readingMeta("none")),
66489
66367
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
66490
66368
  * (`device_class: problem`, on = low). True when the bowl is empty /
66491
66369
  * below the feeder's low threshold. */
66492
66370
  lowFood: boolean(),
66493
66371
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
66494
66372
  * device has no battery reading. */
66495
- batteryPower: number().min(0).max(100).nullable(),
66496
- /** Days of desiccant life remaining. Null when the model has no
66497
- * desiccant sensor. */
66498
- desiccantLeftDays: number().nullable(),
66373
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
66374
+ /**
66375
+ * Desiccant life remaining, stored in seconds (duration base, D944).
66376
+ * Providers convert from the days the feeder reports. Null when the
66377
+ * model has no desiccant sensor.
66378
+ */
66379
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
66499
66380
  /** True while a feed is in progress. */
66500
66381
  feeding: boolean(),
66501
66382
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -66506,7 +66387,7 @@ var PetFeederStatusSchema = object({
66506
66387
  * with `errorCode` for consumers that want the raw integer. */
66507
66388
  error: string().nullable(),
66508
66389
  /** Raw device error code (0 / null = no error). */
66509
- errorCode: number().nullable(),
66390
+ errorCode: number().nullable().meta(readingMeta("none")),
66510
66391
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
66511
66392
  isDualHopper: boolean(),
66512
66393
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -66516,7 +66397,7 @@ var PetFeederStatusSchema = object({
66516
66397
  /** Play a chime when dispensing. */
66517
66398
  feedSound: boolean(),
66518
66399
  /** Speaker / prompt volume level (device-scaled integer). */
66519
- volume: number(),
66400
+ volume: number().meta(readingMeta("none")),
66520
66401
  /** Ms epoch when the slice was last refreshed from the cloud. */
66521
66402
  lastFetchedAt: number()
66522
66403
  });
@@ -66635,7 +66516,7 @@ var petFeederCapability = {
66635
66516
  },
66636
66517
  desiccantLeftDays: {
66637
66518
  kind: "gauge",
66638
- unit: "days"
66519
+ unit: "s"
66639
66520
  }
66640
66521
  } }
66641
66522
  };
@@ -67071,36 +66952,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
67071
66952
  */
67072
66953
  var PowerMeterStatusSchema = object({
67073
66954
  /** Instantaneous power draw in watts. */
67074
- watts: number().optional().meta({
67075
- unit: "W",
67076
- label: "Power"
67077
- }),
66955
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
67078
66956
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
67079
- kwhTotal: number().optional().meta({
67080
- unit: "kWh",
67081
- label: "Energy"
67082
- }),
66957
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
67083
66958
  /** Voltage in volts. */
67084
- volts: number().optional(),
66959
+ volts: number().optional().meta(readingMeta("voltage")),
67085
66960
  /** Current in amperes. */
67086
- amps: number().optional(),
67087
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
67088
- va: number().optional(),
66961
+ amps: number().optional().meta(readingMeta("current")),
66962
+ /** Apparent power in volt-amperes. */
66963
+ va: number().optional().meta(readingMeta("apparent-power")),
67089
66964
  /** Power factor (0..1) if reported. */
67090
- powerFactor: number().min(0).max(1).optional(),
66965
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
67091
66966
  /** Ms epoch when the slice was last updated. */
67092
- lastFetchedAt: number(),
67093
- /** Live display unit of the single metric this slice carries (e.g. HA
67094
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
67095
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
67096
- * unit per slice is unambiguous. The UI prefers this over the role's
67097
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
67098
- unit: string().optional(),
67099
- /** Suggested decimal places for numeric display.
67100
- * Populated live from the upstream source when provided (e.g. HA
67101
- * `attributes.suggested_display_precision`). Falls back to
67102
- * auto-formatting when absent. */
67103
- precision: number().int().min(0).max(10).optional()
66967
+ lastFetchedAt: number()
67104
66968
  });
67105
66969
  var powerMeterCapability = {
67106
66970
  name: "power-meter",
@@ -67164,12 +67028,7 @@ var powerMeterCapability = {
67164
67028
  } },
67165
67029
  /** Clock fields: written, but excluded from the compare that decides
67166
67030
  * whether persisting is worth a SQLite commit. */
67167
- volatileStateFields: ["lastFetchedAt"],
67168
- customize: {
67169
- version: 1,
67170
- advanced: "derive",
67171
- targets: []
67172
- }
67031
+ volatileStateFields: ["lastFetchedAt"]
67173
67032
  };
67174
67033
  /**
67175
67034
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -67189,11 +67048,11 @@ var powerMeterCapability = {
67189
67048
  */
67190
67049
  var GpsLocationSchema = object({
67191
67050
  /** Latitude in decimal degrees, -90..90. */
67192
- latitude: number().min(-90).max(90),
67051
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
67193
67052
  /** Longitude in decimal degrees, -180..180. */
67194
- longitude: number().min(-180).max(180),
67053
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
67195
67054
  /** Reported accuracy in meters (lower = better). */
67196
- accuracyMeters: number().nonnegative()
67055
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
67197
67056
  });
67198
67057
  var PresenceStatusSchema = object({
67199
67058
  /** `home` / `not_home` / any user-defined zone name. */
@@ -67205,7 +67064,7 @@ var PresenceStatusSchema = object({
67205
67064
  gps: GpsLocationSchema.nullable(),
67206
67065
  /** Optional battery level of the tracking device (0..100). Null
67207
67066
  * when not reported. */
67208
- batteryPercent: number().min(0).max(100).nullable(),
67067
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67209
67068
  /** Ms epoch when the slice was last updated. */
67210
67069
  lastChangedAt: number()
67211
67070
  });
@@ -67255,19 +67114,10 @@ var presenceCapability = {
67255
67114
  * `sourceInfo`.
67256
67115
  */
67257
67116
  var PressureSensorStatusSchema = object({
67258
- /** Current pressure in hPa. */
67259
- hpa: number(),
67117
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
67118
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
67260
67119
  /** Ms epoch when the slice was last updated. */
67261
- lastFetchedAt: number(),
67262
- /** Live display unit from the upstream source (e.g. HA
67263
- * `attributes.unit_of_measurement`). The UI prefers this over the
67264
- * role's canonical unit. Absent → fall back to the canonical unit. */
67265
- unit: string().optional(),
67266
- /** Suggested decimal places for numeric display.
67267
- * Populated live from the upstream source when provided (e.g. HA
67268
- * `attributes.suggested_display_precision`). Falls back to
67269
- * auto-formatting when absent. */
67270
- precision: number().int().min(0).max(10).optional()
67120
+ lastFetchedAt: number()
67271
67121
  });
67272
67122
  var pressureSensorCapability = {
67273
67123
  name: "pressure-sensor",
@@ -67681,8 +67531,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
67681
67531
  }), _void(), { kind: "mutation" });
67682
67532
  var PtzAutotrackSettingsSchema = object({
67683
67533
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
67684
- stopDelaySeconds: number().int().min(0).max(300),
67685
- disappearDelaySeconds: number().int().min(0).max(300)
67534
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
67535
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
67686
67536
  });
67687
67537
  /**
67688
67538
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -67836,7 +67686,7 @@ var RunningStateStatusSchema = object({
67836
67686
  /** null = unknown — never coerced to false (D393). */
67837
67687
  running: boolean().nullable(),
67838
67688
  /** Epoch ms the current boolean began; null while `running` is null. */
67839
- since: number().nullable(),
67689
+ since: number().nullable().meta(readingMeta("none")),
67840
67690
  source: RunningStateSourceSchema.meta({ label: "Source" }),
67841
67691
  phase: string().nullable(),
67842
67692
  /** null until a reading has been published (D393). */
@@ -67851,7 +67701,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
67851
67701
  /** Generic Customize target for the running boolean (D936). */
67852
67702
  var runningCustomize = {
67853
67703
  version: 1,
67854
- advanced: "derive",
67855
67704
  targets: [{
67856
67705
  id: "running",
67857
67706
  cardinality: "scalar",
@@ -67871,7 +67720,7 @@ var runningCustomize = {
67871
67720
  DeviceType.Thermostat,
67872
67721
  DeviceType.PetFeeder
67873
67722
  ],
67874
- apply: "add-or-replace",
67723
+ apply: "add",
67875
67724
  path: "running",
67876
67725
  sources: [
67877
67726
  {
@@ -69167,9 +69016,9 @@ var RecordTriggerSchema = _enum([
69167
69016
  */
69168
69017
  var RecordWindowSchema = object({
69169
69018
  trigger: RecordTriggerSchema,
69170
- day: number().int().min(0).max(6),
69171
- startMinute: number().int().min(0).max(1439),
69172
- endMinute: number().int().min(1).max(1440)
69019
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
69020
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
69021
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
69173
69022
  });
69174
69023
  /** Status of one physical volume, as the camera itself describes it. */
69175
69024
  var OnboardStorageVolumeSchema = object({
@@ -69245,10 +69094,10 @@ var RecordingOnboardStatusSchema = object({
69245
69094
  /** Master "record to the card at all" switch. */
69246
69095
  enabled: boolean().nullable(),
69247
69096
  overwriteWhenFull: boolean().nullable(),
69248
- preRecordSec: number().nullable(),
69249
- postRecordSec: number().nullable(),
69097
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
69098
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
69250
69099
  /** Length of one recorded file, in minutes. */
69251
- segmentMinutes: number().nullable(),
69100
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
69252
69101
  /** The primary track's weekly windows, flattened. */
69253
69102
  windows: array(RecordWindowSchema),
69254
69103
  /**
@@ -69264,7 +69113,7 @@ var RecordingOnboardStatusSchema = object({
69264
69113
  * provider reporting a non-zero count also reports the schedule as not
69265
69114
  * writable.
69266
69115
  */
69267
- unreadableWindows: number(),
69116
+ unreadableWindows: number().meta(readingMeta("count")),
69268
69117
  /**
69269
69118
  * The camera is scheduled to record and has NO usable storage.
69270
69119
  *
@@ -70127,7 +69976,7 @@ var CamStreamDescriptorSchema = object({
70127
69976
  url: string().optional(),
70128
69977
  codec: string().optional(),
70129
69978
  resolution: CamStreamResolutionSchema.optional(),
70130
- fps: number().positive().optional(),
69979
+ fps: number().positive().optional().meta(readingMeta("rate")),
70131
69980
  label: string().optional(),
70132
69981
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
70133
69982
  deviceFeatures: array(string()).optional(),
@@ -70214,18 +70063,18 @@ var StreamProfileSchema = _enum([
70214
70063
  ]);
70215
70064
  /** The current encoding config of one stream profile. */
70216
70065
  var StreamProfileConfigSchema = object({
70217
- width: number(),
70218
- height: number(),
70066
+ width: number().meta(readingMeta("none")),
70067
+ height: number().meta(readingMeta("none")),
70219
70068
  codec: _enum(["h264", "h265"]),
70220
- framerate: number(),
70221
- bitrate: number(),
70069
+ framerate: number().meta(readingMeta("rate")),
70070
+ bitrate: number().meta(readingMeta("bitrate")),
70222
70071
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
70223
70072
  encoderProfile: _enum([
70224
70073
  "high",
70225
70074
  "main",
70226
70075
  "baseline"
70227
70076
  ]).optional(),
70228
- gop: number().optional(),
70077
+ gop: number().optional().meta(readingMeta("none")),
70229
70078
  /**
70230
70079
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
70231
70080
  *
@@ -70446,12 +70295,7 @@ var switchCapability = {
70446
70295
  label: "On",
70447
70296
  kind: "boolean"
70448
70297
  }
70449
- }] },
70450
- customize: {
70451
- version: 1,
70452
- advanced: "derive",
70453
- targets: []
70454
- }
70298
+ }] }
70455
70299
  };
70456
70300
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
70457
70301
  function hasControlCharacter(value) {
@@ -71034,35 +70878,21 @@ var tamperCapability = {
71034
70878
  * entries with `device_class: temperature` and any future native
71035
70879
  * thermometer.
71036
70880
  *
71037
- * Unit normalisation: providers convert to Celsius before storing.
71038
- * The slice value is always Celsius so cross-cap aggregators
71039
- * (climate-control's `currentTemp`, energy analytics) can compose
71040
- * without per-source unit fixups. The canonical display unit (`°C`) is
71041
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
71042
- * `sourceInfo`.
70881
+ * The number is Celsius (D944). Providers convert on ingest. A failed
70882
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
71043
70883
  *
71044
70884
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
71045
70885
  * frozen feed (provider hung) distinct from a "temperature hasn't
71046
70886
  * changed" steady state.
71047
70887
  */
71048
70888
  var TemperatureSensorStatusSchema = object({
71049
- /** Current temperature in Celsius. */
71050
- celsius: number().meta({
71051
- unit: "°C",
70889
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
70890
+ celsius: number().nullable().meta(readingMeta("temperature", {
70891
+ precision: 1,
71052
70892
  label: "Temperature"
71053
- }),
70893
+ })),
71054
70894
  /** Ms epoch when the slice was last updated (push or poll). */
71055
- lastFetchedAt: number(),
71056
- /** Live display unit from the upstream source (e.g. HA
71057
- * `attributes.unit_of_measurement`). The UI prefers this over the
71058
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
71059
- * Absent → the UI falls back to the role's canonical unit. */
71060
- unit: string().optional(),
71061
- /** Suggested decimal places for numeric display.
71062
- * Populated live from the upstream source when provided (e.g. HA
71063
- * `attributes.suggested_display_precision`). Falls back to
71064
- * auto-formatting when absent. */
71065
- precision: number().int().min(0).max(10).optional()
70895
+ lastFetchedAt: number()
71066
70896
  });
71067
70897
  var temperatureSensorCapability = {
71068
70898
  name: "temperature-sensor",
@@ -71093,12 +70923,7 @@ var temperatureSensorCapability = {
71093
70923
  } } },
71094
70924
  /** Clock fields: written, but excluded from the compare that decides
71095
70925
  * whether persisting is worth a SQLite commit. */
71096
- volatileStateFields: ["lastFetchedAt"],
71097
- customize: {
71098
- version: 1,
71099
- advanced: "derive",
71100
- targets: []
71101
- }
70926
+ volatileStateFields: ["lastFetchedAt"]
71102
70927
  };
71103
70928
  /**
71104
70929
  * toast — system-scoped singleton capability that streams toast
@@ -71381,16 +71206,16 @@ var VacuumControlStatusSchema = object({
71381
71206
  ]),
71382
71207
  /** 0..100 battery percentage. Null when the device has no battery
71383
71208
  * reading. */
71384
- batteryLevel: number().min(0).max(100).nullable(),
71209
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71385
71210
  /** Current fan-speed token (provider-verbatim). Null when unknown or
71386
71211
  * the vacuum has no speed control. */
71387
71212
  fanSpeed: string().nullable(),
71388
71213
  /** Speed tokens the hardware accepts — drives the UI selector. */
71389
71214
  availableFanSpeeds: array(string()),
71390
71215
  /** 0..100 cleaning-completion percentage of the current task, or null. */
71391
- progressPercent: number().min(0).max(100).nullable(),
71216
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71392
71217
  /** Current error code (0 / null = no error). */
71393
- errorCode: number().nullable(),
71218
+ errorCode: number().nullable().meta(readingMeta("none")),
71394
71219
  /** Human label for {@link errorCode}, or null when none / undecodable. */
71395
71220
  errorLabel: string().nullable(),
71396
71221
  /** Ms epoch when the slice was last updated. */
@@ -71468,7 +71293,7 @@ var ValveStatusSchema = object({
71468
71293
  ]),
71469
71294
  /** 0 = fully closed, 100 = fully open. Null when the device has no
71470
71295
  * intermediate position surface. */
71471
- position: number().min(0).max(100).nullable(),
71296
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
71472
71297
  /** Ms epoch when the slice was last updated. */
71473
71298
  lastChangedAt: number()
71474
71299
  });
@@ -71576,9 +71401,9 @@ var vibrationCapability = {
71576
71401
  */
71577
71402
  var WaterHeaterStatusSchema = object({
71578
71403
  /** Current measured temperature. Null when not reported. */
71579
- currentTemp: number().nullable(),
71404
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
71580
71405
  /** Target temperature setpoint. Null when no setpoint surface. */
71581
- targetTemp: number().nullable(),
71406
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
71582
71407
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
71583
71408
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
71584
71409
  * device reports an unknown state. */
@@ -71589,9 +71414,9 @@ var WaterHeaterStatusSchema = object({
71589
71414
  * has no away surface. */
71590
71415
  away: boolean().nullable(),
71591
71416
  /** HA `min_temp` attribute. Null when not reported. */
71592
- minTemp: number().nullable(),
71417
+ minTemp: number().nullable().meta(readingMeta("temperature")),
71593
71418
  /** HA `max_temp` attribute. Null when not reported. */
71594
- maxTemp: number().nullable(),
71419
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
71595
71420
  /** Ms epoch when the slice was last updated. */
71596
71421
  lastChangedAt: number()
71597
71422
  });
@@ -71676,25 +71501,18 @@ var waterHeaterCapability = {
71676
71501
  * Forecast deferred — current conditions only for v1.
71677
71502
  */
71678
71503
  var WeatherStatusSchema = object({
71679
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
71680
- * when no condition has been reported yet. */
71504
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
71681
71505
  condition: string().nullable(),
71682
- /** Current temperature in the reported unit. Null when not provided. */
71683
- temperature: number().nullable(),
71684
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
71685
- temperatureUnit: string().nullable(),
71506
+ /** Current temperature in Celsius. Null when not provided or refused. */
71507
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
71686
71508
  /** Relative humidity (0..100). Null when not provided. */
71687
- humidity: number().min(0).max(100).nullable(),
71688
- /** Barometric pressure in the reported unit. Null when not provided. */
71689
- pressure: number().nullable(),
71690
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
71691
- pressureUnit: string().nullable(),
71692
- /** Wind speed in the reported unit. Null when not provided. */
71693
- windSpeed: number().nullable(),
71694
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
71695
- windSpeedUnit: string().nullable(),
71509
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
71510
+ /** Barometric pressure in hPa. Null when not provided or refused. */
71511
+ pressure: number().nullable().meta(readingMeta("pressure")),
71512
+ /** Wind speed in metres per second. Null when not provided or refused. */
71513
+ windSpeed: number().nullable().meta(readingMeta("speed")),
71696
71514
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
71697
- windBearing: number().nullable(),
71515
+ windBearing: number().nullable().meta(readingMeta("angle")),
71698
71516
  /** Ms epoch when the slice was last updated. */
71699
71517
  lastFetchedAt: number()
71700
71518
  });
@@ -71722,12 +71540,12 @@ var weatherCapability = {
71722
71540
  */
71723
71541
  durability: "session",
71724
71542
  /**
71725
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
71543
+ * History (D850): recorded series. Numbers are the quantity base (D944).
71726
71544
  */
71727
71545
  history: { fields: {
71728
71546
  temperature: {
71729
71547
  kind: "gauge",
71730
- unitFrom: "temperatureUnit",
71548
+ unit: "°C",
71731
71549
  deadband: .2
71732
71550
  },
71733
71551
  humidity: {
@@ -71737,12 +71555,12 @@ var weatherCapability = {
71737
71555
  },
71738
71556
  pressure: {
71739
71557
  kind: "gauge",
71740
- unitFrom: "pressureUnit",
71558
+ unit: "hPa",
71741
71559
  deadband: .5
71742
71560
  },
71743
71561
  windSpeed: {
71744
71562
  kind: "gauge",
71745
- unitFrom: "windSpeedUnit",
71563
+ unit: "m/s",
71746
71564
  deadband: .5
71747
71565
  }
71748
71566
  } }
@@ -86882,7 +86700,6 @@ var SENSOR_CAP_VOCABULARY = {
86882
86700
  field: "value",
86883
86701
  quantity: t("the reading", "il valore"),
86884
86702
  label: t("Reading", "Valore"),
86885
- unitField: "unit",
86886
86703
  icon: "gauge"
86887
86704
  }]
86888
86705
  },
@@ -87018,7 +86835,7 @@ var SENSOR_CAP_VOCABULARY = {
87018
86835
  field: "temperature",
87019
86836
  quantity: t("the outdoor temperature", "la temperatura esterna"),
87020
86837
  label: t("Temperature", "Temperatura"),
87021
- unitField: "temperatureUnit",
86838
+ unit: "°C",
87022
86839
  icon: "thermometer"
87023
86840
  },
87024
86841
  {
@@ -87034,7 +86851,7 @@ var SENSOR_CAP_VOCABULARY = {
87034
86851
  field: "windSpeed",
87035
86852
  quantity: t("the wind speed", "la velocità del vento"),
87036
86853
  label: t("Wind", "Vento"),
87037
- unitField: "windSpeedUnit",
86854
+ unit: "m/s",
87038
86855
  icon: "wind"
87039
86856
  },
87040
86857
  {
@@ -87042,7 +86859,7 @@ var SENSOR_CAP_VOCABULARY = {
87042
86859
  field: "pressure",
87043
86860
  quantity: t("the air pressure", "la pressione atmosferica"),
87044
86861
  label: t("Pressure", "Pressione"),
87045
- unitField: "pressureUnit",
86862
+ unit: "hPa",
87046
86863
  icon: "gauge"
87047
86864
  }
87048
86865
  ]