@camstack/addon-matter-broker 0.2.147 → 0.2.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +312 -496
  2. package/dist/addon.mjs +312 -496
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -5579,7 +5579,7 @@ var ZodIssueCode = {
5579
5579
  var ZodFirstPartyTypeKind;
5580
5580
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5581
5581
  //#endregion
5582
- //#region ../types/dist/sleep-BErAxhul.mjs
5582
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5583
5583
  /**
5584
5584
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5585
5585
  * window to float samples (D455).
@@ -8321,6 +8321,52 @@ function defineEntryKind(name, spec) {
8321
8321
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8322
8322
  return defined;
8323
8323
  }
8324
+ var READING_QUANTITIES = [
8325
+ "temperature",
8326
+ "pressure",
8327
+ "speed",
8328
+ "precipitation",
8329
+ "precipitation-rate",
8330
+ "length",
8331
+ "area",
8332
+ "illuminance",
8333
+ "irradiance",
8334
+ "power",
8335
+ "energy",
8336
+ "voltage",
8337
+ "current",
8338
+ "apparent-power",
8339
+ "humidity",
8340
+ "percentage",
8341
+ "concentration-volume",
8342
+ "concentration-mass",
8343
+ "concentration-ppb",
8344
+ "volume",
8345
+ "duration",
8346
+ "duration-ms",
8347
+ "information",
8348
+ "sound-level",
8349
+ "signal-level",
8350
+ "colour-temperature",
8351
+ "angle",
8352
+ "bitrate",
8353
+ "rate",
8354
+ "mass",
8355
+ "index",
8356
+ "ratio",
8357
+ "count",
8358
+ "none",
8359
+ "device"
8360
+ ];
8361
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8362
+ new Set(READING_QUANTITIES);
8363
+ function readingMeta(quantity, extras) {
8364
+ return {
8365
+ quantity,
8366
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8367
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8368
+ };
8369
+ }
8324
8370
  /** Shared Zod schemas used across streaming capabilities. */
8325
8371
  var CamProfileSchema = _enum([
8326
8372
  "high",
@@ -8348,8 +8394,8 @@ var CamStreamKindSchema = _enum([
8348
8394
  "derived"
8349
8395
  ]);
8350
8396
  var CamStreamResolutionSchema = object({
8351
- width: number().int().positive(),
8352
- height: number().int().positive()
8397
+ width: number().int().positive().meta(readingMeta("none")),
8398
+ height: number().int().positive().meta(readingMeta("none"))
8353
8399
  });
8354
8400
  var CameraStreamSchema = object({
8355
8401
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8996,14 +9042,6 @@ Object.freeze({
8996
9042
  {
8997
9043
  "path": "aqi",
8998
9044
  "valueType": "number"
8999
- },
9000
- {
9001
- "path": "unit",
9002
- "valueType": "string"
9003
- },
9004
- {
9005
- "path": "precision",
9006
- "valueType": "number"
9007
9045
  }
9008
9046
  ],
9009
9047
  "alarm-panel": [
@@ -9039,20 +9077,10 @@ Object.freeze({
9039
9077
  "valueType": "boolean"
9040
9078
  }
9041
9079
  ],
9042
- "ambient-light-sensor": [
9043
- {
9044
- "path": "lux",
9045
- "valueType": "number"
9046
- },
9047
- {
9048
- "path": "unit",
9049
- "valueType": "string"
9050
- },
9051
- {
9052
- "path": "precision",
9053
- "valueType": "number"
9054
- }
9055
- ],
9080
+ "ambient-light-sensor": [{
9081
+ "path": "lux",
9082
+ "valueType": "number"
9083
+ }],
9056
9084
  "audio-metrics": [
9057
9085
  {
9058
9086
  "path": "ts",
@@ -9132,6 +9160,10 @@ Object.freeze({
9132
9160
  "path": "lastUpdated",
9133
9161
  "valueType": "number"
9134
9162
  },
9163
+ {
9164
+ "path": "volts",
9165
+ "valueType": "number"
9166
+ },
9135
9167
  {
9136
9168
  "path": "lastContactAt",
9137
9169
  "valueType": "number"
@@ -9374,10 +9406,6 @@ Object.freeze({
9374
9406
  "datetime"
9375
9407
  ]
9376
9408
  },
9377
- {
9378
- "path": "unit",
9379
- "valueType": "string"
9380
- },
9381
9409
  {
9382
9410
  "path": "min",
9383
9411
  "valueType": "number"
@@ -9389,19 +9417,6 @@ Object.freeze({
9389
9417
  {
9390
9418
  "path": "step",
9391
9419
  "valueType": "number"
9392
- },
9393
- {
9394
- "path": "precision",
9395
- "valueType": "number"
9396
- },
9397
- {
9398
- "path": "format",
9399
- "valueType": "enum",
9400
- "enumValues": [
9401
- "date",
9402
- "time",
9403
- "datetime"
9404
- ]
9405
9420
  }
9406
9421
  ],
9407
9422
  "cover": [
@@ -9642,13 +9657,8 @@ Object.freeze({
9642
9657
  "path": "value",
9643
9658
  "valueType": "string"
9644
9659
  }, {
9645
- "path": "format",
9646
- "valueType": "enum",
9647
- "enumValues": [
9648
- "date",
9649
- "time",
9650
- "datetime"
9651
- ]
9660
+ "path": "timestamp",
9661
+ "valueType": "boolean"
9652
9662
  }],
9653
9663
  "event-emitter": [
9654
9664
  {
@@ -9755,20 +9765,10 @@ Object.freeze({
9755
9765
  "valueType": "number"
9756
9766
  }
9757
9767
  ],
9758
- "humidity-sensor": [
9759
- {
9760
- "path": "percent",
9761
- "valueType": "number"
9762
- },
9763
- {
9764
- "path": "unit",
9765
- "valueType": "string"
9766
- },
9767
- {
9768
- "path": "precision",
9769
- "valueType": "number"
9770
- }
9771
- ],
9768
+ "humidity-sensor": [{
9769
+ "path": "percent",
9770
+ "valueType": "number"
9771
+ }],
9772
9772
  "image": [{
9773
9773
  "path": "url",
9774
9774
  "valueType": "string"
@@ -10107,20 +10107,10 @@ Object.freeze({
10107
10107
  "valueType": "boolean"
10108
10108
  }
10109
10109
  ],
10110
- "numeric-sensor": [
10111
- {
10112
- "path": "value",
10113
- "valueType": "number"
10114
- },
10115
- {
10116
- "path": "unit",
10117
- "valueType": "string"
10118
- },
10119
- {
10120
- "path": "precision",
10121
- "valueType": "number"
10122
- }
10123
- ],
10110
+ "numeric-sensor": [{
10111
+ "path": "value",
10112
+ "valueType": "number"
10113
+ }],
10124
10114
  "pet-feeder": [
10125
10115
  {
10126
10116
  "path": "foodLevel",
@@ -10212,14 +10202,6 @@ Object.freeze({
10212
10202
  {
10213
10203
  "path": "powerFactor",
10214
10204
  "valueType": "number"
10215
- },
10216
- {
10217
- "path": "unit",
10218
- "valueType": "string"
10219
- },
10220
- {
10221
- "path": "precision",
10222
- "valueType": "number"
10223
10205
  }
10224
10206
  ],
10225
10207
  "presence": [
@@ -10248,20 +10230,10 @@ Object.freeze({
10248
10230
  "valueType": "number"
10249
10231
  }
10250
10232
  ],
10251
- "pressure-sensor": [
10252
- {
10253
- "path": "hpa",
10254
- "valueType": "number"
10255
- },
10256
- {
10257
- "path": "unit",
10258
- "valueType": "string"
10259
- },
10260
- {
10261
- "path": "precision",
10262
- "valueType": "number"
10263
- }
10264
- ],
10233
+ "pressure-sensor": [{
10234
+ "path": "hpa",
10235
+ "valueType": "number"
10236
+ }],
10265
10237
  "privacy-mask": [{
10266
10238
  "path": "enabled",
10267
10239
  "valueType": "boolean"
@@ -10572,20 +10544,10 @@ Object.freeze({
10572
10544
  "path": "tampered",
10573
10545
  "valueType": "boolean"
10574
10546
  }],
10575
- "temperature-sensor": [
10576
- {
10577
- "path": "celsius",
10578
- "valueType": "number"
10579
- },
10580
- {
10581
- "path": "unit",
10582
- "valueType": "string"
10583
- },
10584
- {
10585
- "path": "precision",
10586
- "valueType": "number"
10587
- }
10588
- ],
10547
+ "temperature-sensor": [{
10548
+ "path": "celsius",
10549
+ "valueType": "number"
10550
+ }],
10589
10551
  "update": [
10590
10552
  {
10591
10553
  "path": "currentVersion",
@@ -10696,10 +10658,6 @@ Object.freeze({
10696
10658
  "path": "temperature",
10697
10659
  "valueType": "number"
10698
10660
  },
10699
- {
10700
- "path": "temperatureUnit",
10701
- "valueType": "string"
10702
- },
10703
10661
  {
10704
10662
  "path": "humidity",
10705
10663
  "valueType": "number"
@@ -10708,18 +10666,10 @@ Object.freeze({
10708
10666
  "path": "pressure",
10709
10667
  "valueType": "number"
10710
10668
  },
10711
- {
10712
- "path": "pressureUnit",
10713
- "valueType": "string"
10714
- },
10715
10669
  {
10716
10670
  "path": "windSpeed",
10717
10671
  "valueType": "number"
10718
10672
  },
10719
- {
10720
- "path": "windSpeedUnit",
10721
- "valueType": "string"
10722
- },
10723
10673
  {
10724
10674
  "path": "windBearing",
10725
10675
  "valueType": "number"
@@ -18232,7 +18182,8 @@ var CompositionProblemSchema = object({
18232
18182
  "item-field-reserved",
18233
18183
  "target-missing",
18234
18184
  "target-unreadable",
18235
- "not-replaceable"
18185
+ "not-replaceable",
18186
+ "native-capability-replace"
18236
18187
  ]),
18237
18188
  path: string$2(),
18238
18189
  message: string$2()
@@ -18290,9 +18241,11 @@ var CompositionFieldPlanSchema = object({
18290
18241
  reads: array(CompositionFieldReadSchema)
18291
18242
  });
18292
18243
  /**
18293
- * What a feature does to its target (D663): `new` builds a capability on a new
18294
- * device; on an existing device it `add`s a capability the device lacks, or
18295
- * `replace`s named fields of one it has natively.
18244
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18245
+ * new device; on an existing device it `add`s a capability the device lacks.
18246
+ * Replacing a capability the device already serves is refused
18247
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18248
+ * wire for a claim the hub already holds.
18296
18249
  */
18297
18250
  var CompositionFeatureModeSchema = _enum([
18298
18251
  "new",
@@ -20178,12 +20131,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20178
20131
  * providers detect renames by matching `uniqueId` and patch `id`
20179
20132
  * in-place so dispatch keeps working.
20180
20133
  *
20181
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20182
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20183
- * state push and are the single source of truth. The `device_class` tag is
20184
- * an HA-specific input that is consumed at the discovery boundary
20185
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20186
- * stored after that point.
20134
+ * **Display unit and precision are NOT stored here.** They are a read-time
20135
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20136
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20137
+ * neither is repeated on each sample. The `device_class` tag is an
20138
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20139
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20187
20140
  *
20188
20141
  * The `raw` escape hatch carries provider-specific attributes that don't
20189
20142
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20197,6 +20150,14 @@ var SourceInfoSchema = object({
20197
20150
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20198
20151
  * Used to detect rename when `id` changes. */
20199
20152
  uniqueId: string$2().optional(),
20153
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20154
+ quantity: ReadingQuantitySchema.optional(),
20155
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20156
+ dateTimeShape: _enum([
20157
+ "date",
20158
+ "time",
20159
+ "datetime"
20160
+ ]).optional(),
20200
20161
  /** Provider-specific extras that don't fit the structured slots.
20201
20162
  * Whitelist what goes here per provider — do NOT dump entire HA
20202
20163
  * attribute blobs (would explode the meta JSON column). */
@@ -41318,7 +41279,8 @@ method(object({
41318
41279
  "enum"
41319
41280
  ]),
41320
41281
  enumValues: array(string$2()).optional(),
41321
- item: boolean().optional()
41282
+ item: boolean().optional(),
41283
+ quantity: ReadingQuantitySchema.optional()
41322
41284
  })).readonly(),
41323
41285
  itemArray: object({
41324
41286
  path: string$2(),
@@ -45162,8 +45124,8 @@ var MaskPolygonShapeSchema = object({
45162
45124
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45163
45125
  var MaskGridShapeSchema = object({
45164
45126
  kind: literal("grid"),
45165
- gridWidth: number(),
45166
- gridHeight: number(),
45127
+ gridWidth: number().meta(readingMeta("none")),
45128
+ gridHeight: number().meta(readingMeta("none")),
45167
45129
  cells: array(boolean())
45168
45130
  });
45169
45131
  discriminatedUnion("kind", [
@@ -56643,27 +56605,17 @@ method(object({ nodeId: string$2().optional() }).optional(), array(AddonListItem
56643
56605
  */
56644
56606
  var AirQualitySensorStatusSchema = object({
56645
56607
  /** Carbon dioxide concentration in ppm. */
56646
- co2Ppm: number().min(0).optional(),
56608
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56647
56609
  /** Total volatile organic compounds in ppb. */
56648
- vocPpb: number().min(0).optional(),
56610
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56649
56611
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56650
- pm25: number().min(0).optional(),
56612
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56651
56613
  /** Particulate matter ≤ 10 μm in µg/m³. */
56652
- pm10: number().min(0).optional(),
56614
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56653
56615
  /** Composite AQI value (typically 0..500). */
56654
- aqi: number().optional(),
56616
+ aqi: number().optional().meta(readingMeta("index")),
56655
56617
  /** Ms epoch when the slice was last updated. */
56656
- lastFetchedAt: number(),
56657
- /** Live display unit of the single metric this slice carries (e.g. HA
56658
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56659
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56660
- * per slice is unambiguous. */
56661
- unit: string$2().optional(),
56662
- /** Suggested decimal places for numeric display.
56663
- * Populated live from the upstream source when provided (e.g. HA
56664
- * `attributes.suggested_display_precision`). Falls back to
56665
- * auto-formatting when absent. */
56666
- precision: number().int().min(0).max(10).optional()
56618
+ lastFetchedAt: number()
56667
56619
  });
56668
56620
  var airQualitySensorCapability = {
56669
56621
  name: "air-quality-sensor",
@@ -56700,12 +56652,12 @@ var airQualitySensorCapability = {
56700
56652
  },
56701
56653
  pm25: {
56702
56654
  kind: "gauge",
56703
- unit: "ug/m3",
56655
+ unit: "µg/m³",
56704
56656
  deadband: 1
56705
56657
  },
56706
56658
  pm10: {
56707
56659
  kind: "gauge",
56708
- unit: "ug/m3",
56660
+ unit: "µg/m³",
56709
56661
  deadband: 1
56710
56662
  },
56711
56663
  aqi: {
@@ -56723,22 +56675,13 @@ var airQualitySensorCapability = {
56723
56675
  * entries with `device_class: illuminance`.
56724
56676
  */
56725
56677
  var AmbientLightSensorStatusSchema = object({
56726
- /** Current illuminance in lux (lx). */
56727
- lux: number().min(0).meta({
56728
- unit: "lx",
56678
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56679
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56680
+ precision: 0,
56729
56681
  label: "Illuminance"
56730
- }),
56682
+ })),
56731
56683
  /** Ms epoch when the slice was last updated. */
56732
- lastFetchedAt: number(),
56733
- /** Live display unit from the upstream source (e.g. HA
56734
- * `attributes.unit_of_measurement`). The UI prefers this over the
56735
- * role's canonical unit. Absent → fall back to the canonical unit. */
56736
- unit: string$2().optional(),
56737
- /** Suggested decimal places for numeric display.
56738
- * Populated live from the upstream source when provided (e.g. HA
56739
- * `attributes.suggested_display_precision`). Falls back to
56740
- * auto-formatting when absent. */
56741
- precision: number().int().min(0).max(10).optional()
56684
+ lastFetchedAt: number()
56742
56685
  });
56743
56686
  var ambientLightSensorCapability = {
56744
56687
  name: "ambient-light-sensor",
@@ -56770,12 +56713,7 @@ var ambientLightSensorCapability = {
56770
56713
  } } },
56771
56714
  /** Clock fields: written, but excluded from the compare that decides
56772
56715
  * whether persisting is worth a SQLite commit. */
56773
- volatileStateFields: ["lastFetchedAt"],
56774
- customize: {
56775
- version: 1,
56776
- advanced: "derive",
56777
- targets: []
56778
- }
56716
+ volatileStateFields: ["lastFetchedAt"]
56779
56717
  };
56780
56718
  /**
56781
56719
  * Per-class audio metrics aggregated over a sliding window.
@@ -57002,10 +56940,7 @@ var BatteryStatusSchema = object({
57002
56940
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57003
56941
  * declines to band a non-finite reading, which is the correct shape.
57004
56942
  */
57005
- percentage: number().min(0).max(100).nullable().meta({
57006
- unit: "%",
57007
- label: "Charge"
57008
- }),
56943
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57009
56944
  /**
57010
56945
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57011
56946
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57032,7 +56967,14 @@ var BatteryStatusSchema = object({
57032
56967
  */
57033
56968
  sleeping: boolean().meta({ label: "Sleeping" }),
57034
56969
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57035
- lastUpdated: number(),
56970
+ lastUpdated: number().meta(readingMeta("none")),
56971
+ /**
56972
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56973
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56974
+ * that only report charge. A second reading on this cap, not a second
56975
+ * `numeric-sensor` on the same device.
56976
+ */
56977
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57036
56978
  /**
57037
56979
  * Ms epoch of the last time the device PROVED it was reachable — a
57038
56980
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57084,7 +57026,6 @@ var BATTERY_DEVICE_TYPES = [
57084
57026
  /** Generic Customize target for the percentage reading (D936). */
57085
57027
  var batteryCustomize = {
57086
57028
  version: 1,
57087
- advanced: "derive",
57088
57029
  targets: [{
57089
57030
  id: "percentage",
57090
57031
  cardinality: "scalar",
@@ -57093,7 +57034,7 @@ var batteryCustomize = {
57093
57034
  stateField: "percentage"
57094
57035
  },
57095
57036
  deviceTypes: BATTERY_DEVICE_TYPES,
57096
- apply: "add-or-replace",
57037
+ apply: "add",
57097
57038
  path: "percentage",
57098
57039
  sources: [{
57099
57040
  kind: "field",
@@ -57214,10 +57155,16 @@ onStatusChanged: { data: object({
57214
57155
  /**
57215
57156
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57216
57157
  */
57217
- history: { fields: { percentage: {
57218
- kind: "gauge",
57219
- unit: "%"
57220
- } } },
57158
+ history: { fields: {
57159
+ percentage: {
57160
+ kind: "gauge",
57161
+ unit: "%"
57162
+ },
57163
+ volts: {
57164
+ kind: "gauge",
57165
+ unit: "V"
57166
+ }
57167
+ } },
57221
57168
  customize: batteryCustomize,
57222
57169
  /** Clock fields: written, but excluded from the compare that decides
57223
57170
  * whether persisting is worth a SQLite commit. */
@@ -57264,12 +57211,7 @@ var binaryCapability = {
57264
57211
  history: { fields: { on: { kind: "boolean" } } },
57265
57212
  /** Clock fields: written, but excluded from the compare that decides
57266
57213
  * whether persisting is worth a SQLite commit. */
57267
- volatileStateFields: ["lastChangedAt"],
57268
- customize: {
57269
- version: 1,
57270
- advanced: "derive",
57271
- targets: []
57272
- }
57214
+ volatileStateFields: ["lastChangedAt"]
57273
57215
  };
57274
57216
  /**
57275
57217
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57285,7 +57227,7 @@ var binaryCapability = {
57285
57227
  */
57286
57228
  var BrightnessStatusSchema = object({
57287
57229
  /** Current level as 0..100 inclusive. Firmware-reported. */
57288
- percentage: number().min(0).max(100),
57230
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57289
57231
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57290
57232
  lastChangedAt: number()
57291
57233
  });
@@ -57756,21 +57698,21 @@ var ClimateControlStatusSchema = object({
57756
57698
  /** Single setpoint in Celsius. Used by single-target modes
57757
57699
  * (heat / cool / dry). Null when the device is in a range mode
57758
57700
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57759
- target: number().nullable(),
57701
+ target: number().nullable().meta(readingMeta("temperature")),
57760
57702
  /** Upper bound of the dual setpoint in Celsius. Populated only
57761
57703
  * in `heat_cool` mode. */
57762
- targetHigh: number().nullable(),
57704
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57763
57705
  /** Lower bound of the dual setpoint in Celsius. Populated only
57764
57706
  * in `heat_cool` mode. */
57765
- targetLow: number().nullable(),
57707
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57766
57708
  /** Target relative humidity (0..100). Null when the device has no
57767
57709
  * humidity control. */
57768
- targetHumidity: number().min(0).max(100).nullable(),
57710
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57769
57711
  /** Current measured temperature in Celsius. Read-only — pushed by
57770
57712
  * the device's internal sensor. */
57771
- currentTemp: number().nullable(),
57713
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57772
57714
  /** Current measured relative humidity (0..100). Read-only. */
57773
- currentHumidity: number().min(0).max(100).nullable(),
57715
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57774
57716
  /** Vertical louver swing on/off. Null when the device has no vertical
57775
57717
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57776
57718
  swingVertical: boolean().nullable(),
@@ -57778,11 +57720,11 @@ var ClimateControlStatusSchema = object({
57778
57720
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57779
57721
  swingHorizontal: boolean().nullable(),
57780
57722
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57781
- minTemp: number().nullable().optional(),
57723
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57782
57724
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57783
- maxTemp: number().nullable().optional(),
57725
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57784
57726
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57785
- tempStep: number().positive().nullable().optional(),
57727
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57786
57728
  /** Ms epoch when the slice was last updated (push or command). */
57787
57729
  lastFetchedAt: number()
57788
57730
  });
@@ -57924,17 +57866,17 @@ var climateControlCapability = {
57924
57866
  * modes to expose in the picker without a slice fetch.
57925
57867
  */
57926
57868
  var RgbTripletSchema = object({
57927
- r: number().int().min(0).max(255),
57928
- g: number().int().min(0).max(255),
57929
- b: number().int().min(0).max(255)
57869
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57870
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57871
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57930
57872
  });
57931
57873
  var HsvTripletSchema = object({
57932
57874
  /** Hue in degrees, 0..360. */
57933
- h: number().min(0).max(360),
57875
+ h: number().min(0).max(360).meta(readingMeta("none")),
57934
57876
  /** Saturation as 0..100 inclusive. */
57935
- s: number().min(0).max(100),
57877
+ s: number().min(0).max(100).meta(readingMeta("none")),
57936
57878
  /** Value/brightness as 0..100 inclusive. */
57937
- v: number().min(0).max(100)
57879
+ v: number().min(0).max(100).meta(readingMeta("none"))
57938
57880
  });
57939
57881
  var ColorInputSchema = discriminatedUnion("mode", [
57940
57882
  object({
@@ -57963,7 +57905,7 @@ var ColorStatusSchema = object({
57963
57905
  /** Populated when `mode === 'hsv'`. */
57964
57906
  hsv: HsvTripletSchema.optional(),
57965
57907
  /** Populated when `mode === 'mired'`. */
57966
- mireds: number().int().optional(),
57908
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57967
57909
  /** Ms epoch of the last operator-driven change. */
57968
57910
  lastChangedAt: number()
57969
57911
  });
@@ -58158,18 +58100,12 @@ _enum([
58158
58100
  "empty",
58159
58101
  "full"
58160
58102
  ]);
58161
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58162
- * each. */
58163
- var ConsumableRemainingUnitSchema = _enum([
58164
- "days",
58165
- "hours",
58166
- "cycles"
58167
- ]);
58168
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58169
- * days). `value` null = unknown (D393). */
58103
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58104
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58105
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58170
58106
  var ConsumableRemainingSchema = object({
58171
- value: number().min(0).nullable(),
58172
- unit: ConsumableRemainingUnitSchema
58107
+ value: number().min(0).nullable().meta(readingMeta("device")),
58108
+ kind: ConsumableRemainingKindSchema
58173
58109
  });
58174
58110
  /** A single consumable item. Either a continuous `level` (remaining
58175
58111
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58182,7 +58118,7 @@ var ConsumableItemSchema = object({
58182
58118
  /** Display name. */
58183
58119
  label: string$2().min(1),
58184
58120
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58185
- level: number().min(0).max(100).nullable(),
58121
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58186
58122
  /** The provider's discrete verdict when known (binary mode). */
58187
58123
  status: ConsumableStatusSchema.nullable(),
58188
58124
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58264,8 +58200,8 @@ var consumablesCustomize = {
58264
58200
  transforms: [{ kind: "identity" }]
58265
58201
  }],
58266
58202
  literals: [{
58267
- path: "remaining.unit",
58268
- values: [...ConsumableRemainingUnitSchema.options]
58203
+ path: "remaining.kind",
58204
+ values: [...ConsumableRemainingKindSchema.options]
58269
58205
  }]
58270
58206
  },
58271
58207
  {
@@ -58394,7 +58330,6 @@ reset: method(object({
58394
58330
  },
58395
58331
  "remaining.value": {
58396
58332
  kind: "gauge",
58397
- unitFrom: "remaining.unit",
58398
58333
  deadband: 1,
58399
58334
  displayName: "Remaining"
58400
58335
  }
@@ -58464,10 +58399,9 @@ var contactCapability = {
58464
58399
  * (empty array for the other kinds). It lives in the slice because
58465
58400
  * HA `select.*` entities may update their options at runtime.
58466
58401
  *
58467
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58468
- * `kind === 'numeric'` and are absent (undefined) for all other
58469
- * kinds. They are populated live from HA attributes on each state
58470
- * push — consistent with `options` and the sensor-unit approach.
58402
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58403
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58404
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58471
58405
  */
58472
58406
  var ControlStatusSchema = object({
58473
58407
  kind: _enum([
@@ -58479,39 +58413,17 @@ var ControlStatusSchema = object({
58479
58413
  /** Current value. Numeric kind → number; select/text/datetime → string.
58480
58414
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58481
58415
  * or time-only — HA accepts all three forms). */
58482
- value: union([number(), string$2()]),
58416
+ value: union([number().meta(readingMeta("device")), string$2()]),
58483
58417
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58484
58418
  options: array(string$2()),
58485
58419
  /** Ms epoch when the slice was last updated. */
58486
58420
  lastChangedAt: number(),
58487
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58488
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58489
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58490
- unit: string$2().optional(),
58491
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58492
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58493
- min: number().optional(),
58494
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58495
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58496
- max: number().optional(),
58497
- /** Step increment. Populated live from HA `attributes.step`.
58498
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58499
- step: number().optional(),
58500
- /** Suggested decimal places for numeric display. Populated live from HA
58501
- * `attributes.suggested_display_precision`, or derived from `step` when the
58502
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58503
- * other kinds. Falls back to auto-formatting when absent. */
58504
- precision: number().int().min(0).max(10).optional(),
58505
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58506
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58507
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58508
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58509
- * `has_date`/`has_time` attributes on each push. */
58510
- format: _enum([
58511
- "date",
58512
- "time",
58513
- "datetime"
58514
- ]).optional()
58421
+ /** Minimum allowed value, in the device quantity's base unit. */
58422
+ min: number().optional().meta(readingMeta("device")),
58423
+ /** Maximum allowed value, in the device quantity's base unit. */
58424
+ max: number().optional().meta(readingMeta("device")),
58425
+ /** Step increment, in the device quantity's base unit. */
58426
+ step: number().optional().meta(readingMeta("device"))
58515
58427
  });
58516
58428
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58517
58429
  object({
@@ -58577,9 +58489,9 @@ var CoverStatusSchema = object({
58577
58489
  ]),
58578
58490
  /** 0 = fully closed, 100 = fully open. Null when the device has
58579
58491
  * no intermediate position surface. */
58580
- position: number().min(0).max(100).nullable(),
58492
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58581
58493
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58582
- tiltPosition: number().min(0).max(100).nullable(),
58494
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58583
58495
  /** Ms epoch when the slice was last updated. */
58584
58496
  lastChangedAt: number()
58585
58497
  });
@@ -58682,9 +58594,9 @@ var NormalizedRangeSchema$1 = object({
58682
58594
  var DayNightStatusSchema = object({
58683
58595
  mode: DayNightModeSchema,
58684
58596
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58685
- sensitivity: number().optional(),
58597
+ sensitivity: number().optional().meta(readingMeta("none")),
58686
58598
  /** Delay before the IR-cut filter flips, in seconds. */
58687
- switchDelaySec: number().optional(),
58599
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58688
58600
  lastFetchedAt: number()
58689
58601
  });
58690
58602
  /**
@@ -58902,11 +58814,11 @@ var DishwasherStatusSchema = object({
58902
58814
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58903
58815
  availablePrograms: array(string$2()),
58904
58816
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58905
- remainingSeconds: number().int().nonnegative().nullable(),
58817
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58906
58818
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58907
- delayedStartSeconds: number().int().nonnegative().nullable(),
58819
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58908
58820
  /** Cycle completion 0..100, or null when not applicable. */
58909
- progressPercent: number().min(0).max(100).nullable(),
58821
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58910
58822
  /** True when the door is reported open; null when the door state is not known. */
58911
58823
  doorOpen: boolean().nullable(),
58912
58824
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58930,7 +58842,7 @@ var DishwasherStatusSchema = object({
58930
58842
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58931
58843
  machineCareDue: boolean().nullable(),
58932
58844
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58933
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58845
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58934
58846
  /** Option states: null when the appliance does not expose the option or has not said. */
58935
58847
  halfLoad: boolean().nullable(),
58936
58848
  hygienePlus: boolean().nullable(),
@@ -58945,7 +58857,7 @@ var DishwasherStatusSchema = object({
58945
58857
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58946
58858
  activeEvents: array(string$2()),
58947
58859
  /** Numeric error code when the appliance reports one; null otherwise. */
58948
- errorCode: number().nullable(),
58860
+ errorCode: number().nullable().meta(readingMeta("none")),
58949
58861
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58950
58862
  errorLabel: string$2().nullable(),
58951
58863
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59176,7 +59088,7 @@ var DoorbellStatusSchema = object({
59176
59088
  /** Ms epoch of the last press. null = never observed since this provider started. */
59177
59089
  lastPressedAt: number().nullable(),
59178
59090
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59179
- pressCountSinceStart: number()
59091
+ pressCountSinceStart: number().meta(readingMeta("count"))
59180
59092
  });
59181
59093
  var DoorbellPressEventSchema = object({
59182
59094
  deviceId: number(),
@@ -59241,17 +59153,7 @@ onPressed: {
59241
59153
  history: "none",
59242
59154
  customize: doorbellCustomize
59243
59155
  };
59244
- /**
59245
- * Enum-state sensor — a string value picked from a finite option set.
59246
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59247
- * states, weather conditions, contact-source channels, mode strings).
59248
- * The option set is stable across observations; it is persisted in the
59249
- * device config blob (via the control slice at adoption) and is NOT
59250
- * stored in `sourceInfo`.
59251
- */
59252
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59253
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59254
- var EnumSensorDateTimeFormatSchema = _enum([
59156
+ _enum([
59255
59157
  "date",
59256
59158
  "time",
59257
59159
  "datetime"
@@ -59259,12 +59161,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59259
59161
  var EnumSensorStatusSchema = object({
59260
59162
  value: string$2(),
59261
59163
  /**
59262
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59263
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59264
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59265
- * a time-only sensor → `'time'`.
59164
+ * Set when `value` is a timestamp rather than an enum state, so history
59165
+ * does not learn ISO strings as states. The display shape (date / time /
59166
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59266
59167
  */
59267
- format: EnumSensorDateTimeFormatSchema.optional(),
59168
+ timestamp: boolean().optional(),
59268
59169
  /** Ms epoch when the slice was last updated. */
59269
59170
  lastFetchedAt: number()
59270
59171
  });
@@ -59291,11 +59192,11 @@ var enumSensorCapability = {
59291
59192
  * History (D850): the value as an `enum` series (an appliance's program or
59292
59193
  * operation state, an HVAC action). The option set is per device, so the
59293
59194
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59294
- * `format` carries a timestamp, not a state, and is not recorded.
59195
+ * `timestamp` carries a clock, not a state, and is not recorded.
59295
59196
  */
59296
59197
  history: { fields: { value: {
59297
59198
  kind: "enum",
59298
- skipWhenSet: "format"
59199
+ skipWhenSet: "timestamp"
59299
59200
  } } },
59300
59201
  /** Clock fields: written, but excluded from the compare that decides
59301
59202
  * whether persisting is worth a SQLite commit. */
@@ -59315,13 +59216,13 @@ var EventFireSchema = object({
59315
59216
  deviceId: number(),
59316
59217
  eventType: string$2(),
59317
59218
  data: record(string$2(), unknown()).nullable(),
59318
- timestamp: number(),
59319
- seq: number()
59219
+ timestamp: number().meta(readingMeta("none")),
59220
+ seq: number().meta(readingMeta("count"))
59320
59221
  });
59321
59222
  var EventEmitterStatusSchema = object({
59322
59223
  eventTypes: array(string$2()),
59323
59224
  lastEvent: EventFireSchema.nullable(),
59324
- eventCountSinceStart: number()
59225
+ eventCountSinceStart: number().meta(readingMeta("count"))
59325
59226
  });
59326
59227
  var eventEmitterCapability = {
59327
59228
  name: "event-emitter",
@@ -59734,10 +59635,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59734
59635
  var FanControlStatusSchema = object({
59735
59636
  /** Active speed as 0..100 inclusive. Null when the device has no
59736
59637
  * speed surface (single-speed fan). */
59737
- percentage: number().min(0).max(100).nullable(),
59638
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59738
59639
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59739
59640
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59740
- percentageStep: number().positive().optional(),
59641
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59741
59642
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59742
59643
  * — empty string when no preset is active or supported. */
59743
59644
  preset: string$2(),
@@ -59858,7 +59759,7 @@ var FeatureProbeStatusSchema = object({
59858
59759
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59859
59760
  model: string$2().nullable(),
59860
59761
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59861
- channelCount: number().nullable(),
59762
+ channelCount: number().nullable().meta(readingMeta("count")),
59862
59763
  /**
59863
59764
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59864
59765
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59983,7 +59884,7 @@ var gasCapability = {
59983
59884
  };
59984
59885
  var HeatingValveStatusSchema = object({
59985
59886
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59986
- position: number().min(0).max(100).nullable(),
59887
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59987
59888
  /** Ms epoch when the slice was last updated (push or poll). */
59988
59889
  lastFetchedAt: number()
59989
59890
  });
@@ -60026,9 +59927,9 @@ var HumidifierStatusSchema = object({
60026
59927
  /** Whether the humidifier is currently on. */
60027
59928
  on: boolean(),
60028
59929
  /** Current measured relative humidity (0..100). Null when not reported. */
60029
- currentHumidity: number().min(0).max(100).nullable(),
59930
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60030
59931
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60031
- targetHumidity: number().min(0).max(100).nullable(),
59932
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60032
59933
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60033
59934
  * device has no mode surface. */
60034
59935
  mode: string$2().nullable(),
@@ -60038,9 +59939,9 @@ var HumidifierStatusSchema = object({
60038
59939
  * `idle` / `off`). Null when the device doesn't report it. */
60039
59940
  action: string$2().nullable(),
60040
59941
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60041
- minHumidity: number().nullable(),
59942
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60042
59943
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60043
- maxHumidity: number().nullable(),
59944
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60044
59945
  /** Ms epoch when the slice was last updated. */
60045
59946
  lastChangedAt: number()
60046
59947
  });
@@ -60110,22 +60011,13 @@ var humidifierCapability = {
60110
60011
  * `sourceInfo`.
60111
60012
  */
60112
60013
  var HumiditySensorStatusSchema = object({
60113
- /** Current relative humidity, 0..100. */
60114
- percent: number().min(0).max(100).meta({
60115
- unit: "%",
60014
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60015
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60016
+ precision: 0,
60116
60017
  label: "Humidity"
60117
- }),
60018
+ })),
60118
60019
  /** Ms epoch when the slice was last updated. */
60119
- lastFetchedAt: number(),
60120
- /** Live display unit from the upstream source (e.g. HA
60121
- * `attributes.unit_of_measurement`). The UI prefers this over the
60122
- * role's canonical unit. Absent → fall back to the canonical unit. */
60123
- unit: string$2().optional(),
60124
- /** Suggested decimal places for numeric display.
60125
- * Populated live from the upstream source when provided (e.g. HA
60126
- * `attributes.suggested_display_precision`). Falls back to
60127
- * auto-formatting when absent. */
60128
- precision: number().int().min(0).max(10).optional()
60020
+ lastFetchedAt: number()
60129
60021
  });
60130
60022
  var humiditySensorCapability = {
60131
60023
  name: "humidity-sensor",
@@ -60156,12 +60048,7 @@ var humiditySensorCapability = {
60156
60048
  } } },
60157
60049
  /** Clock fields: written, but excluded from the compare that decides
60158
60050
  * whether persisting is worth a SQLite commit. */
60159
- volatileStateFields: ["lastFetchedAt"],
60160
- customize: {
60161
- version: 1,
60162
- advanced: "derive",
60163
- targets: []
60164
- }
60051
+ volatileStateFields: ["lastFetchedAt"]
60165
60052
  };
60166
60053
  /**
60167
60054
  * Image display cap. Models a single still image exposed by an integration —
@@ -60182,7 +60069,7 @@ var ImageStatusSchema = object({
60182
60069
  * entity exposes no `entity_picture` (yet). */
60183
60070
  url: string$2().nullable(),
60184
60071
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60185
- lastUpdated: number().nullable()
60072
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60186
60073
  });
60187
60074
  var imageCapability = {
60188
60075
  name: "image",
@@ -60270,19 +60157,19 @@ var NormalizedRangeSchema = object({
60270
60157
  */
60271
60158
  var ImageSettingsStatusSchema = object({
60272
60159
  /** Normalized 0–100. */
60273
- brightness: number().optional(),
60160
+ brightness: number().optional().meta(readingMeta("none")),
60274
60161
  /** Normalized 0–100. */
60275
- contrast: number().optional(),
60162
+ contrast: number().optional().meta(readingMeta("none")),
60276
60163
  /** Normalized 0–100. */
60277
- saturation: number().optional(),
60164
+ saturation: number().optional().meta(readingMeta("none")),
60278
60165
  /** Normalized 0–100. */
60279
- sharpness: number().optional(),
60166
+ sharpness: number().optional().meta(readingMeta("none")),
60280
60167
  mirror: boolean().optional(),
60281
60168
  flip: boolean().optional(),
60282
60169
  rotate: ImageRotateSchema.optional(),
60283
60170
  whiteBalance: WhiteBalanceModeSchema.optional(),
60284
60171
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60285
- warmth: number().optional(),
60172
+ warmth: number().optional().meta(readingMeta("none")),
60286
60173
  exposureMode: ExposureModeSchema.optional(),
60287
60174
  backlightMode: BacklightModeSchema.optional(),
60288
60175
  lastFetchedAt: number()
@@ -60561,10 +60448,10 @@ var IntercomAbilitySchema = object({
60561
60448
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60562
60449
  codecs: array(string$2()),
60563
60450
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60564
- sampleRate: number(),
60451
+ sampleRate: number().meta(readingMeta("none")),
60565
60452
  duplex: _enum(["half", "full"]),
60566
60453
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60567
- maxBacklogMs: number()
60454
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60568
60455
  });
60569
60456
  var IntercomStatusSchema = object({
60570
60457
  /** True while a talk session is open. */
@@ -60791,13 +60678,13 @@ var LawnMowerControlStatusSchema = object({
60791
60678
  activity: LawnMowerActivitySchema,
60792
60679
  /** 0..100 battery percentage. Null when the device has no battery
60793
60680
  * reading. */
60794
- batteryLevel: number().min(0).max(100).nullable(),
60681
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60795
60682
  /** 0..100 mowing-completion percentage of the current task, or null when no
60796
60683
  * task is active / progress is unavailable. */
60797
- progressPercent: number().min(0).max(100).nullable(),
60684
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60798
60685
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60799
60686
  * or null when unknown. */
60800
- currentCode: number().nullable(),
60687
+ currentCode: number().nullable().meta(readingMeta("none")),
60801
60688
  /** Human label for {@link currentCode}, or null when undecodable. */
60802
60689
  currentCodeLabel: string$2().nullable(),
60803
60690
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61330,7 +61217,7 @@ var MediaPlayerStatusSchema = object({
61330
61217
  state: MediaPlayerStateSchema,
61331
61218
  /** Volume as 0..100 inclusive. Null when the device has no volume
61332
61219
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61333
- volumeLevel: number().min(0).max(100).nullable(),
61220
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61334
61221
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61335
61222
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61336
61223
  isMuted: boolean().nullable(),
@@ -61343,10 +61230,10 @@ var MediaPlayerStatusSchema = object({
61343
61230
  currentMedia: MediaInfoSchema.nullable(),
61344
61231
  /** Current playback position in ms. Null when not seekable or
61345
61232
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61346
- positionMs: number().int().nonnegative().nullable(),
61233
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61347
61234
  /** Total duration of the current media in ms. Null when unknown
61348
61235
  * (live stream) or nothing is playing. */
61349
- durationMs: number().int().nonnegative().nullable(),
61236
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61350
61237
  /** Shuffle toggle. Null when no shuffle surface.
61351
61238
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61352
61239
  shuffle: boolean().nullable(),
@@ -61655,7 +61542,7 @@ var motionCustomize = {
61655
61542
  eventName: "onMotionChanged"
61656
61543
  },
61657
61544
  deviceTypes: [DeviceType.Camera],
61658
- apply: "replace",
61545
+ apply: "add",
61659
61546
  applyAuthority: {
61660
61547
  kind: "camera-sensors",
61661
61548
  timeoutField: "motionCooldownMs"
@@ -61727,7 +61614,7 @@ var MotionStatusSchema = object({
61727
61614
  * it, every provider seeded it at boot, and every session left it
61728
61615
  * `detected: true` after its detach.
61729
61616
  */
61730
- autoClearAfterMs: number().nullable()
61617
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61731
61618
  });
61732
61619
  /**
61733
61620
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61909,7 +61796,7 @@ var MotionZoneRegionSchema = object({
61909
61796
  * the grid region(s). */
61910
61797
  var MotionZoneStatusSchema = object({
61911
61798
  enabled: boolean(),
61912
- sensitivity: number(),
61799
+ sensitivity: number().meta(readingMeta("none")),
61913
61800
  /** Grid region(s). Today exactly one `grid` shape. */
61914
61801
  regions: array(MotionZoneRegionSchema),
61915
61802
  lastFetchedAt: number()
@@ -62279,7 +62166,7 @@ var NavigationStatusSchema = object({
62279
62166
  following: boolean(),
62280
62167
  flash: boolean(),
62281
62168
  lightMode: NavigationLightModeSchema,
62282
- lightLevel: number().min(40).max(100),
62169
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62283
62170
  /** Ms epoch when the slice was last updated. */
62284
62171
  lastChangedAt: number()
62285
62172
  });
@@ -62413,13 +62300,13 @@ var NetworkLinkStatusSchema = object({
62413
62300
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62414
62301
  * SKIP a null rather than coerce it.
62415
62302
  */
62416
- signalPercent: number().min(0).max(100).nullable(),
62303
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62417
62304
  /** Raw received signal strength in dBm, when the firmware reports one. */
62418
- rssiDbm: number().optional(),
62305
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62419
62306
  /** Network name of a wireless link, when the firmware reports it. */
62420
62307
  ssid: string$2().optional(),
62421
62308
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62422
- lastUpdated: number()
62309
+ lastUpdated: number().meta(readingMeta("none"))
62423
62310
  });
62424
62311
  var networkLinkCapability = {
62425
62312
  name: "network-link",
@@ -62804,7 +62691,7 @@ var NotifierStatusSchema = object({
62804
62691
  lastError: string$2().nullable(),
62805
62692
  /** Number of deliveries currently buffered server-side (rate-limited
62806
62693
  * notifiers may queue). 0 when the provider sends synchronously. */
62807
- queueDepth: number().int().nonnegative(),
62694
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62808
62695
  /** Per-feature capability matrix — authoritative source for the
62809
62696
  * compose-form field gating. */
62810
62697
  supports: NotifierSupportsSchema
@@ -62896,11 +62783,9 @@ onSent: { data: object({
62896
62783
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62897
62784
  * for: water flow, distance, weight, frequency, signal strength, …).
62898
62785
  *
62899
- * The `unit` and `precision` fields carry the live display metadata that
62900
- * the upstream source provides with each state push (HA
62901
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62902
- * The UI reads them directly from the slice — they are the single source
62903
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
62786
+ * The quantity is not on this slice. It is chosen when the device is
62787
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
62788
+ * that quantity's base unit. `null` is a refused sample (D393).
62904
62789
  *
62905
62790
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62906
62791
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62909,15 +62794,8 @@ onSent: { data: object({
62909
62794
  * numeric values land here.
62910
62795
  */
62911
62796
  var NumericSensorStatusSchema = object({
62912
- value: number(),
62913
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62914
- * Populated live from the upstream source on each state push.
62915
- * Absent for unitless sensors. */
62916
- unit: string$2().optional(),
62917
- /** Suggested decimal places for numeric display.
62918
- * Populated live from the upstream source when provided.
62919
- * Falls back to auto-formatting when absent. */
62920
- precision: number().int().min(0).max(10).optional(),
62797
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
62798
+ value: number().nullable().meta(readingMeta("device")),
62921
62799
  /** Ms epoch when the slice was last updated. */
62922
62800
  lastFetchedAt: number()
62923
62801
  });
@@ -62941,11 +62819,11 @@ var numericSensorCapability = {
62941
62819
  */
62942
62820
  durability: "restored",
62943
62821
  /**
62944
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
62822
+ * History (D850): recorded series. The unit is the device quantity's base,
62823
+ * resolved when the series is read — the cap has no single unit.
62945
62824
  */
62946
62825
  history: { fields: { value: {
62947
62826
  kind: "gauge",
62948
- unitFrom: "unit",
62949
62827
  minIntervalMs: 3e4
62950
62828
  } } },
62951
62829
  /** Clock fields: written, but excluded from the compare that decides
@@ -63237,23 +63115,26 @@ var PetFeederStatusSchema = object({
63237
63115
  /** Food currently in the bowl (grams). Null when the device has not
63238
63116
  * reported a reading yet. On dual-hopper models this is the combined
63239
63117
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63240
- foodLevel: number().nullable(),
63118
+ foodLevel: number().nullable().meta(readingMeta("none")),
63241
63119
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63242
63120
  * single-hopper models. */
63243
- food1: number().nullable(),
63121
+ food1: number().nullable().meta(readingMeta("none")),
63244
63122
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63245
63123
  * single-hopper models. */
63246
- food2: number().nullable(),
63124
+ food2: number().nullable().meta(readingMeta("none")),
63247
63125
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63248
63126
  * (`device_class: problem`, on = low). True when the bowl is empty /
63249
63127
  * below the feeder's low threshold. */
63250
63128
  lowFood: boolean(),
63251
63129
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63252
63130
  * device has no battery reading. */
63253
- batteryPower: number().min(0).max(100).nullable(),
63254
- /** Days of desiccant life remaining. Null when the model has no
63255
- * desiccant sensor. */
63256
- desiccantLeftDays: number().nullable(),
63131
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63132
+ /**
63133
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63134
+ * Providers convert from the days the feeder reports. Null when the
63135
+ * model has no desiccant sensor.
63136
+ */
63137
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63257
63138
  /** True while a feed is in progress. */
63258
63139
  feeding: boolean(),
63259
63140
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63264,7 +63145,7 @@ var PetFeederStatusSchema = object({
63264
63145
  * with `errorCode` for consumers that want the raw integer. */
63265
63146
  error: string$2().nullable(),
63266
63147
  /** Raw device error code (0 / null = no error). */
63267
- errorCode: number().nullable(),
63148
+ errorCode: number().nullable().meta(readingMeta("none")),
63268
63149
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63269
63150
  isDualHopper: boolean(),
63270
63151
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63274,7 +63155,7 @@ var PetFeederStatusSchema = object({
63274
63155
  /** Play a chime when dispensing. */
63275
63156
  feedSound: boolean(),
63276
63157
  /** Speaker / prompt volume level (device-scaled integer). */
63277
- volume: number(),
63158
+ volume: number().meta(readingMeta("none")),
63278
63159
  /** Ms epoch when the slice was last refreshed from the cloud. */
63279
63160
  lastFetchedAt: number()
63280
63161
  });
@@ -63393,7 +63274,7 @@ var petFeederCapability = {
63393
63274
  },
63394
63275
  desiccantLeftDays: {
63395
63276
  kind: "gauge",
63396
- unit: "days"
63277
+ unit: "s"
63397
63278
  }
63398
63279
  } }
63399
63280
  };
@@ -63794,36 +63675,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63794
63675
  */
63795
63676
  var PowerMeterStatusSchema = object({
63796
63677
  /** Instantaneous power draw in watts. */
63797
- watts: number().optional().meta({
63798
- unit: "W",
63799
- label: "Power"
63800
- }),
63678
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63801
63679
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63802
- kwhTotal: number().optional().meta({
63803
- unit: "kWh",
63804
- label: "Energy"
63805
- }),
63680
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63806
63681
  /** Voltage in volts. */
63807
- volts: number().optional(),
63682
+ volts: number().optional().meta(readingMeta("voltage")),
63808
63683
  /** Current in amperes. */
63809
- amps: number().optional(),
63810
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63811
- va: number().optional(),
63684
+ amps: number().optional().meta(readingMeta("current")),
63685
+ /** Apparent power in volt-amperes. */
63686
+ va: number().optional().meta(readingMeta("apparent-power")),
63812
63687
  /** Power factor (0..1) if reported. */
63813
- powerFactor: number().min(0).max(1).optional(),
63688
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63814
63689
  /** Ms epoch when the slice was last updated. */
63815
- lastFetchedAt: number(),
63816
- /** Live display unit of the single metric this slice carries (e.g. HA
63817
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63818
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63819
- * unit per slice is unambiguous. The UI prefers this over the role's
63820
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63821
- unit: string$2().optional(),
63822
- /** Suggested decimal places for numeric display.
63823
- * Populated live from the upstream source when provided (e.g. HA
63824
- * `attributes.suggested_display_precision`). Falls back to
63825
- * auto-formatting when absent. */
63826
- precision: number().int().min(0).max(10).optional()
63690
+ lastFetchedAt: number()
63827
63691
  });
63828
63692
  var powerMeterCapability = {
63829
63693
  name: "power-meter",
@@ -63887,12 +63751,7 @@ var powerMeterCapability = {
63887
63751
  } },
63888
63752
  /** Clock fields: written, but excluded from the compare that decides
63889
63753
  * whether persisting is worth a SQLite commit. */
63890
- volatileStateFields: ["lastFetchedAt"],
63891
- customize: {
63892
- version: 1,
63893
- advanced: "derive",
63894
- targets: []
63895
- }
63754
+ volatileStateFields: ["lastFetchedAt"]
63896
63755
  };
63897
63756
  /**
63898
63757
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63912,11 +63771,11 @@ var powerMeterCapability = {
63912
63771
  */
63913
63772
  var GpsLocationSchema = object({
63914
63773
  /** Latitude in decimal degrees, -90..90. */
63915
- latitude: number().min(-90).max(90),
63774
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63916
63775
  /** Longitude in decimal degrees, -180..180. */
63917
- longitude: number().min(-180).max(180),
63776
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63918
63777
  /** Reported accuracy in meters (lower = better). */
63919
- accuracyMeters: number().nonnegative()
63778
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63920
63779
  });
63921
63780
  var PresenceStatusSchema = object({
63922
63781
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63928,7 +63787,7 @@ var PresenceStatusSchema = object({
63928
63787
  gps: GpsLocationSchema.nullable(),
63929
63788
  /** Optional battery level of the tracking device (0..100). Null
63930
63789
  * when not reported. */
63931
- batteryPercent: number().min(0).max(100).nullable(),
63790
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63932
63791
  /** Ms epoch when the slice was last updated. */
63933
63792
  lastChangedAt: number()
63934
63793
  });
@@ -63978,19 +63837,10 @@ var presenceCapability = {
63978
63837
  * `sourceInfo`.
63979
63838
  */
63980
63839
  var PressureSensorStatusSchema = object({
63981
- /** Current pressure in hPa. */
63982
- hpa: number(),
63840
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
63841
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63983
63842
  /** Ms epoch when the slice was last updated. */
63984
- lastFetchedAt: number(),
63985
- /** Live display unit from the upstream source (e.g. HA
63986
- * `attributes.unit_of_measurement`). The UI prefers this over the
63987
- * role's canonical unit. Absent → fall back to the canonical unit. */
63988
- unit: string$2().optional(),
63989
- /** Suggested decimal places for numeric display.
63990
- * Populated live from the upstream source when provided (e.g. HA
63991
- * `attributes.suggested_display_precision`). Falls back to
63992
- * auto-formatting when absent. */
63993
- precision: number().int().min(0).max(10).optional()
63843
+ lastFetchedAt: number()
63994
63844
  });
63995
63845
  var pressureSensorCapability = {
63996
63846
  name: "pressure-sensor",
@@ -64404,8 +64254,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64404
64254
  }), _void(), { kind: "mutation" });
64405
64255
  var PtzAutotrackSettingsSchema = object({
64406
64256
  targetType: string$2().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64407
- stopDelaySeconds: number().int().min(0).max(300),
64408
- disappearDelaySeconds: number().int().min(0).max(300)
64257
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64258
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64409
64259
  });
64410
64260
  /**
64411
64261
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64559,7 +64409,7 @@ var RunningStateStatusSchema = object({
64559
64409
  /** null = unknown — never coerced to false (D393). */
64560
64410
  running: boolean().nullable(),
64561
64411
  /** Epoch ms the current boolean began; null while `running` is null. */
64562
- since: number().nullable(),
64412
+ since: number().nullable().meta(readingMeta("none")),
64563
64413
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64564
64414
  phase: string$2().nullable(),
64565
64415
  /** null until a reading has been published (D393). */
@@ -64574,7 +64424,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64574
64424
  /** Generic Customize target for the running boolean (D936). */
64575
64425
  var runningCustomize = {
64576
64426
  version: 1,
64577
- advanced: "derive",
64578
64427
  targets: [{
64579
64428
  id: "running",
64580
64429
  cardinality: "scalar",
@@ -64594,7 +64443,7 @@ var runningCustomize = {
64594
64443
  DeviceType.Thermostat,
64595
64444
  DeviceType.PetFeeder
64596
64445
  ],
64597
- apply: "add-or-replace",
64446
+ apply: "add",
64598
64447
  path: "running",
64599
64448
  sources: [
64600
64449
  {
@@ -65858,9 +65707,9 @@ var RecordTriggerSchema = _enum([
65858
65707
  */
65859
65708
  var RecordWindowSchema = object({
65860
65709
  trigger: RecordTriggerSchema,
65861
- day: number().int().min(0).max(6),
65862
- startMinute: number().int().min(0).max(1439),
65863
- endMinute: number().int().min(1).max(1440)
65710
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65711
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65712
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65864
65713
  });
65865
65714
  /** Status of one physical volume, as the camera itself describes it. */
65866
65715
  var OnboardStorageVolumeSchema = object({
@@ -65936,10 +65785,10 @@ var RecordingOnboardStatusSchema = object({
65936
65785
  /** Master "record to the card at all" switch. */
65937
65786
  enabled: boolean().nullable(),
65938
65787
  overwriteWhenFull: boolean().nullable(),
65939
- preRecordSec: number().nullable(),
65940
- postRecordSec: number().nullable(),
65788
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
65789
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65941
65790
  /** Length of one recorded file, in minutes. */
65942
- segmentMinutes: number().nullable(),
65791
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65943
65792
  /** The primary track's weekly windows, flattened. */
65944
65793
  windows: array(RecordWindowSchema),
65945
65794
  /**
@@ -65955,7 +65804,7 @@ var RecordingOnboardStatusSchema = object({
65955
65804
  * provider reporting a non-zero count also reports the schedule as not
65956
65805
  * writable.
65957
65806
  */
65958
- unreadableWindows: number(),
65807
+ unreadableWindows: number().meta(readingMeta("count")),
65959
65808
  /**
65960
65809
  * The camera is scheduled to record and has NO usable storage.
65961
65810
  *
@@ -66800,7 +66649,7 @@ var CamStreamDescriptorSchema = object({
66800
66649
  url: string$2().optional(),
66801
66650
  codec: string$2().optional(),
66802
66651
  resolution: CamStreamResolutionSchema.optional(),
66803
- fps: number().positive().optional(),
66652
+ fps: number().positive().optional().meta(readingMeta("rate")),
66804
66653
  label: string$2().optional(),
66805
66654
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66806
66655
  deviceFeatures: array(string$2()).optional(),
@@ -66887,18 +66736,18 @@ var StreamProfileSchema = _enum([
66887
66736
  ]);
66888
66737
  /** The current encoding config of one stream profile. */
66889
66738
  var StreamProfileConfigSchema = object({
66890
- width: number(),
66891
- height: number(),
66739
+ width: number().meta(readingMeta("none")),
66740
+ height: number().meta(readingMeta("none")),
66892
66741
  codec: _enum(["h264", "h265"]),
66893
- framerate: number(),
66894
- bitrate: number(),
66742
+ framerate: number().meta(readingMeta("rate")),
66743
+ bitrate: number().meta(readingMeta("bitrate")),
66895
66744
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66896
66745
  encoderProfile: _enum([
66897
66746
  "high",
66898
66747
  "main",
66899
66748
  "baseline"
66900
66749
  ]).optional(),
66901
- gop: number().optional(),
66750
+ gop: number().optional().meta(readingMeta("none")),
66902
66751
  /**
66903
66752
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66904
66753
  *
@@ -67119,12 +66968,7 @@ var switchCapability = {
67119
66968
  label: "On",
67120
66969
  kind: "boolean"
67121
66970
  }
67122
- }] },
67123
- customize: {
67124
- version: 1,
67125
- advanced: "derive",
67126
- targets: []
67127
- }
66971
+ }] }
67128
66972
  };
67129
66973
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67130
66974
  function hasControlCharacter(value) {
@@ -67707,35 +67551,21 @@ var tamperCapability = {
67707
67551
  * entries with `device_class: temperature` and any future native
67708
67552
  * thermometer.
67709
67553
  *
67710
- * Unit normalisation: providers convert to Celsius before storing.
67711
- * The slice value is always Celsius so cross-cap aggregators
67712
- * (climate-control's `currentTemp`, energy analytics) can compose
67713
- * without per-source unit fixups. The canonical display unit (`°C`) is
67714
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67715
- * `sourceInfo`.
67554
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67555
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67716
67556
  *
67717
67557
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67718
67558
  * frozen feed (provider hung) distinct from a "temperature hasn't
67719
67559
  * changed" steady state.
67720
67560
  */
67721
67561
  var TemperatureSensorStatusSchema = object({
67722
- /** Current temperature in Celsius. */
67723
- celsius: number().meta({
67724
- unit: "°C",
67562
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67563
+ celsius: number().nullable().meta(readingMeta("temperature", {
67564
+ precision: 1,
67725
67565
  label: "Temperature"
67726
- }),
67566
+ })),
67727
67567
  /** Ms epoch when the slice was last updated (push or poll). */
67728
- lastFetchedAt: number(),
67729
- /** Live display unit from the upstream source (e.g. HA
67730
- * `attributes.unit_of_measurement`). The UI prefers this over the
67731
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67732
- * Absent → the UI falls back to the role's canonical unit. */
67733
- unit: string$2().optional(),
67734
- /** Suggested decimal places for numeric display.
67735
- * Populated live from the upstream source when provided (e.g. HA
67736
- * `attributes.suggested_display_precision`). Falls back to
67737
- * auto-formatting when absent. */
67738
- precision: number().int().min(0).max(10).optional()
67568
+ lastFetchedAt: number()
67739
67569
  });
67740
67570
  var temperatureSensorCapability = {
67741
67571
  name: "temperature-sensor",
@@ -67766,12 +67596,7 @@ var temperatureSensorCapability = {
67766
67596
  } } },
67767
67597
  /** Clock fields: written, but excluded from the compare that decides
67768
67598
  * whether persisting is worth a SQLite commit. */
67769
- volatileStateFields: ["lastFetchedAt"],
67770
- customize: {
67771
- version: 1,
67772
- advanced: "derive",
67773
- targets: []
67774
- }
67599
+ volatileStateFields: ["lastFetchedAt"]
67775
67600
  };
67776
67601
  /**
67777
67602
  * toast — system-scoped singleton capability that streams toast
@@ -68054,16 +67879,16 @@ var VacuumControlStatusSchema = object({
68054
67879
  ]),
68055
67880
  /** 0..100 battery percentage. Null when the device has no battery
68056
67881
  * reading. */
68057
- batteryLevel: number().min(0).max(100).nullable(),
67882
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68058
67883
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68059
67884
  * the vacuum has no speed control. */
68060
67885
  fanSpeed: string$2().nullable(),
68061
67886
  /** Speed tokens the hardware accepts — drives the UI selector. */
68062
67887
  availableFanSpeeds: array(string$2()),
68063
67888
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68064
- progressPercent: number().min(0).max(100).nullable(),
67889
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68065
67890
  /** Current error code (0 / null = no error). */
68066
- errorCode: number().nullable(),
67891
+ errorCode: number().nullable().meta(readingMeta("none")),
68067
67892
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68068
67893
  errorLabel: string$2().nullable(),
68069
67894
  /** Ms epoch when the slice was last updated. */
@@ -68141,7 +67966,7 @@ var ValveStatusSchema = object({
68141
67966
  ]),
68142
67967
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68143
67968
  * intermediate position surface. */
68144
- position: number().min(0).max(100).nullable(),
67969
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68145
67970
  /** Ms epoch when the slice was last updated. */
68146
67971
  lastChangedAt: number()
68147
67972
  });
@@ -68249,9 +68074,9 @@ var vibrationCapability = {
68249
68074
  */
68250
68075
  var WaterHeaterStatusSchema = object({
68251
68076
  /** Current measured temperature. Null when not reported. */
68252
- currentTemp: number().nullable(),
68077
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68253
68078
  /** Target temperature setpoint. Null when no setpoint surface. */
68254
- targetTemp: number().nullable(),
68079
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68255
68080
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68256
68081
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68257
68082
  * device reports an unknown state. */
@@ -68262,9 +68087,9 @@ var WaterHeaterStatusSchema = object({
68262
68087
  * has no away surface. */
68263
68088
  away: boolean().nullable(),
68264
68089
  /** HA `min_temp` attribute. Null when not reported. */
68265
- minTemp: number().nullable(),
68090
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68266
68091
  /** HA `max_temp` attribute. Null when not reported. */
68267
- maxTemp: number().nullable(),
68092
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68268
68093
  /** Ms epoch when the slice was last updated. */
68269
68094
  lastChangedAt: number()
68270
68095
  });
@@ -68349,25 +68174,18 @@ var waterHeaterCapability = {
68349
68174
  * Forecast deferred — current conditions only for v1.
68350
68175
  */
68351
68176
  var WeatherStatusSchema = object({
68352
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68353
- * when no condition has been reported yet. */
68177
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68354
68178
  condition: string$2().nullable(),
68355
- /** Current temperature in the reported unit. Null when not provided. */
68356
- temperature: number().nullable(),
68357
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68358
- temperatureUnit: string$2().nullable(),
68179
+ /** Current temperature in Celsius. Null when not provided or refused. */
68180
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68359
68181
  /** Relative humidity (0..100). Null when not provided. */
68360
- humidity: number().min(0).max(100).nullable(),
68361
- /** Barometric pressure in the reported unit. Null when not provided. */
68362
- pressure: number().nullable(),
68363
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68364
- pressureUnit: string$2().nullable(),
68365
- /** Wind speed in the reported unit. Null when not provided. */
68366
- windSpeed: number().nullable(),
68367
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68368
- windSpeedUnit: string$2().nullable(),
68182
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68183
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68184
+ pressure: number().nullable().meta(readingMeta("pressure")),
68185
+ /** Wind speed in metres per second. Null when not provided or refused. */
68186
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68369
68187
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68370
- windBearing: number().nullable(),
68188
+ windBearing: number().nullable().meta(readingMeta("angle")),
68371
68189
  /** Ms epoch when the slice was last updated. */
68372
68190
  lastFetchedAt: number()
68373
68191
  });
@@ -68395,12 +68213,12 @@ var weatherCapability = {
68395
68213
  */
68396
68214
  durability: "session",
68397
68215
  /**
68398
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68216
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68399
68217
  */
68400
68218
  history: { fields: {
68401
68219
  temperature: {
68402
68220
  kind: "gauge",
68403
- unitFrom: "temperatureUnit",
68221
+ unit: "°C",
68404
68222
  deadband: .2
68405
68223
  },
68406
68224
  humidity: {
@@ -68410,12 +68228,12 @@ var weatherCapability = {
68410
68228
  },
68411
68229
  pressure: {
68412
68230
  kind: "gauge",
68413
- unitFrom: "pressureUnit",
68231
+ unit: "hPa",
68414
68232
  deadband: .5
68415
68233
  },
68416
68234
  windSpeed: {
68417
68235
  kind: "gauge",
68418
- unitFrom: "windSpeedUnit",
68236
+ unit: "m/s",
68419
68237
  deadband: .5
68420
68238
  }
68421
68239
  } }
@@ -83985,7 +83803,6 @@ var SENSOR_CAP_VOCABULARY = {
83985
83803
  field: "value",
83986
83804
  quantity: t("the reading", "il valore"),
83987
83805
  label: t("Reading", "Valore"),
83988
- unitField: "unit",
83989
83806
  icon: "gauge"
83990
83807
  }]
83991
83808
  },
@@ -84121,7 +83938,7 @@ var SENSOR_CAP_VOCABULARY = {
84121
83938
  field: "temperature",
84122
83939
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84123
83940
  label: t("Temperature", "Temperatura"),
84124
- unitField: "temperatureUnit",
83941
+ unit: "°C",
84125
83942
  icon: "thermometer"
84126
83943
  },
84127
83944
  {
@@ -84137,7 +83954,7 @@ var SENSOR_CAP_VOCABULARY = {
84137
83954
  field: "windSpeed",
84138
83955
  quantity: t("the wind speed", "la velocità del vento"),
84139
83956
  label: t("Wind", "Vento"),
84140
- unitField: "windSpeedUnit",
83957
+ unit: "m/s",
84141
83958
  icon: "wind"
84142
83959
  },
84143
83960
  {
@@ -84145,7 +83962,7 @@ var SENSOR_CAP_VOCABULARY = {
84145
83962
  field: "pressure",
84146
83963
  quantity: t("the air pressure", "la pressione atmosferica"),
84147
83964
  label: t("Pressure", "Pressione"),
84148
- unitField: "pressureUnit",
83965
+ unit: "hPa",
84149
83966
  icon: "gauge"
84150
83967
  }
84151
83968
  ]
@@ -197437,7 +197254,6 @@ function decodeElectricalPower(attributeId, value) {
197437
197254
  cap: "power-meter",
197438
197255
  patch: {
197439
197256
  watts: mw / 1e3,
197440
- unit: "W",
197441
197257
  lastFetchedAt: Date.now()
197442
197258
  }
197443
197259
  };