@camstack/addon-provider-homematic 1.2.157 → 1.2.158

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 +949 -509
  2. package/dist/addon.mjs +949 -509
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5576,7 +5576,7 @@ var ZodIssueCode = {
5576
5576
  var ZodFirstPartyTypeKind;
5577
5577
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5578
5578
  //#endregion
5579
- //#region ../types/dist/sleep-BErAxhul.mjs
5579
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5580
5580
  /**
5581
5581
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5582
5582
  * window to float samples (D455).
@@ -8318,6 +8318,99 @@ function defineEntryKind(name, spec) {
8318
8318
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8319
8319
  return defined;
8320
8320
  }
8321
+ var READING_QUANTITIES = [
8322
+ "temperature",
8323
+ "pressure",
8324
+ "speed",
8325
+ "precipitation",
8326
+ "precipitation-rate",
8327
+ "length",
8328
+ "area",
8329
+ "illuminance",
8330
+ "irradiance",
8331
+ "power",
8332
+ "energy",
8333
+ "voltage",
8334
+ "current",
8335
+ "apparent-power",
8336
+ "humidity",
8337
+ "percentage",
8338
+ "concentration-volume",
8339
+ "concentration-mass",
8340
+ "concentration-ppb",
8341
+ "volume",
8342
+ "duration",
8343
+ "duration-ms",
8344
+ "information",
8345
+ "sound-level",
8346
+ "signal-level",
8347
+ "colour-temperature",
8348
+ "angle",
8349
+ "bitrate",
8350
+ "rate",
8351
+ "mass",
8352
+ "index",
8353
+ "ratio",
8354
+ "count",
8355
+ "none",
8356
+ "device"
8357
+ ];
8358
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8359
+ new Set(READING_QUANTITIES);
8360
+ /**
8361
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8362
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8363
+ */
8364
+ var QUANTITY_BASE = {
8365
+ temperature: "°C",
8366
+ pressure: "hPa",
8367
+ speed: "m/s",
8368
+ precipitation: "mm",
8369
+ "precipitation-rate": "mm/h",
8370
+ length: "m",
8371
+ area: "m²",
8372
+ illuminance: "lx",
8373
+ irradiance: "W/m²",
8374
+ power: "W",
8375
+ energy: "kWh",
8376
+ voltage: "V",
8377
+ current: "A",
8378
+ "apparent-power": "VA",
8379
+ humidity: "%",
8380
+ percentage: "%",
8381
+ "concentration-volume": "ppm",
8382
+ "concentration-mass": "µg/m³",
8383
+ "concentration-ppb": "ppb",
8384
+ volume: "L",
8385
+ duration: "s",
8386
+ "duration-ms": "ms",
8387
+ information: "B",
8388
+ "sound-level": "dBFS",
8389
+ "signal-level": "dBm",
8390
+ "colour-temperature": "mired",
8391
+ angle: "°",
8392
+ bitrate: "kbps",
8393
+ rate: "fps",
8394
+ mass: "kg",
8395
+ index: "none",
8396
+ ratio: "none",
8397
+ count: "none",
8398
+ none: "none",
8399
+ device: "device"
8400
+ };
8401
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8402
+ function baseUnitOf(quantity) {
8403
+ const symbol = QUANTITY_BASE[quantity];
8404
+ if (symbol === "none" || symbol === "device") return void 0;
8405
+ return symbol;
8406
+ }
8407
+ function readingMeta(quantity, extras) {
8408
+ return {
8409
+ quantity,
8410
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8411
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8412
+ };
8413
+ }
8321
8414
  /** Shared Zod schemas used across streaming capabilities. */
8322
8415
  var CamProfileSchema = _enum([
8323
8416
  "high",
@@ -8345,8 +8438,8 @@ var CamStreamKindSchema = _enum([
8345
8438
  "derived"
8346
8439
  ]);
8347
8440
  var CamStreamResolutionSchema = object({
8348
- width: number().int().positive(),
8349
- height: number().int().positive()
8441
+ width: number().int().positive().meta(readingMeta("none")),
8442
+ height: number().int().positive().meta(readingMeta("none"))
8350
8443
  });
8351
8444
  var CameraStreamSchema = object({
8352
8445
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8993,14 +9086,6 @@ Object.freeze({
8993
9086
  {
8994
9087
  "path": "aqi",
8995
9088
  "valueType": "number"
8996
- },
8997
- {
8998
- "path": "unit",
8999
- "valueType": "string"
9000
- },
9001
- {
9002
- "path": "precision",
9003
- "valueType": "number"
9004
9089
  }
9005
9090
  ],
9006
9091
  "alarm-panel": [
@@ -9036,20 +9121,10 @@ Object.freeze({
9036
9121
  "valueType": "boolean"
9037
9122
  }
9038
9123
  ],
9039
- "ambient-light-sensor": [
9040
- {
9041
- "path": "lux",
9042
- "valueType": "number"
9043
- },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
- {
9049
- "path": "precision",
9050
- "valueType": "number"
9051
- }
9052
- ],
9124
+ "ambient-light-sensor": [{
9125
+ "path": "lux",
9126
+ "valueType": "number"
9127
+ }],
9053
9128
  "audio-metrics": [
9054
9129
  {
9055
9130
  "path": "ts",
@@ -9129,6 +9204,10 @@ Object.freeze({
9129
9204
  "path": "lastUpdated",
9130
9205
  "valueType": "number"
9131
9206
  },
9207
+ {
9208
+ "path": "volts",
9209
+ "valueType": "number"
9210
+ },
9132
9211
  {
9133
9212
  "path": "lastContactAt",
9134
9213
  "valueType": "number"
@@ -9371,10 +9450,6 @@ Object.freeze({
9371
9450
  "datetime"
9372
9451
  ]
9373
9452
  },
9374
- {
9375
- "path": "unit",
9376
- "valueType": "string"
9377
- },
9378
9453
  {
9379
9454
  "path": "min",
9380
9455
  "valueType": "number"
@@ -9386,19 +9461,6 @@ Object.freeze({
9386
9461
  {
9387
9462
  "path": "step",
9388
9463
  "valueType": "number"
9389
- },
9390
- {
9391
- "path": "precision",
9392
- "valueType": "number"
9393
- },
9394
- {
9395
- "path": "format",
9396
- "valueType": "enum",
9397
- "enumValues": [
9398
- "date",
9399
- "time",
9400
- "datetime"
9401
- ]
9402
9464
  }
9403
9465
  ],
9404
9466
  "cover": [
@@ -9639,13 +9701,8 @@ Object.freeze({
9639
9701
  "path": "value",
9640
9702
  "valueType": "string"
9641
9703
  }, {
9642
- "path": "format",
9643
- "valueType": "enum",
9644
- "enumValues": [
9645
- "date",
9646
- "time",
9647
- "datetime"
9648
- ]
9704
+ "path": "timestamp",
9705
+ "valueType": "boolean"
9649
9706
  }],
9650
9707
  "event-emitter": [
9651
9708
  {
@@ -9752,20 +9809,10 @@ Object.freeze({
9752
9809
  "valueType": "number"
9753
9810
  }
9754
9811
  ],
9755
- "humidity-sensor": [
9756
- {
9757
- "path": "percent",
9758
- "valueType": "number"
9759
- },
9760
- {
9761
- "path": "unit",
9762
- "valueType": "string"
9763
- },
9764
- {
9765
- "path": "precision",
9766
- "valueType": "number"
9767
- }
9768
- ],
9812
+ "humidity-sensor": [{
9813
+ "path": "percent",
9814
+ "valueType": "number"
9815
+ }],
9769
9816
  "image": [{
9770
9817
  "path": "url",
9771
9818
  "valueType": "string"
@@ -10104,20 +10151,10 @@ Object.freeze({
10104
10151
  "valueType": "boolean"
10105
10152
  }
10106
10153
  ],
10107
- "numeric-sensor": [
10108
- {
10109
- "path": "value",
10110
- "valueType": "number"
10111
- },
10112
- {
10113
- "path": "unit",
10114
- "valueType": "string"
10115
- },
10116
- {
10117
- "path": "precision",
10118
- "valueType": "number"
10119
- }
10120
- ],
10154
+ "numeric-sensor": [{
10155
+ "path": "value",
10156
+ "valueType": "number"
10157
+ }],
10121
10158
  "pet-feeder": [
10122
10159
  {
10123
10160
  "path": "foodLevel",
@@ -10209,14 +10246,6 @@ Object.freeze({
10209
10246
  {
10210
10247
  "path": "powerFactor",
10211
10248
  "valueType": "number"
10212
- },
10213
- {
10214
- "path": "unit",
10215
- "valueType": "string"
10216
- },
10217
- {
10218
- "path": "precision",
10219
- "valueType": "number"
10220
10249
  }
10221
10250
  ],
10222
10251
  "presence": [
@@ -10245,20 +10274,10 @@ Object.freeze({
10245
10274
  "valueType": "number"
10246
10275
  }
10247
10276
  ],
10248
- "pressure-sensor": [
10249
- {
10250
- "path": "hpa",
10251
- "valueType": "number"
10252
- },
10253
- {
10254
- "path": "unit",
10255
- "valueType": "string"
10256
- },
10257
- {
10258
- "path": "precision",
10259
- "valueType": "number"
10260
- }
10261
- ],
10277
+ "pressure-sensor": [{
10278
+ "path": "hpa",
10279
+ "valueType": "number"
10280
+ }],
10262
10281
  "privacy-mask": [{
10263
10282
  "path": "enabled",
10264
10283
  "valueType": "boolean"
@@ -10569,20 +10588,10 @@ Object.freeze({
10569
10588
  "path": "tampered",
10570
10589
  "valueType": "boolean"
10571
10590
  }],
10572
- "temperature-sensor": [
10573
- {
10574
- "path": "celsius",
10575
- "valueType": "number"
10576
- },
10577
- {
10578
- "path": "unit",
10579
- "valueType": "string"
10580
- },
10581
- {
10582
- "path": "precision",
10583
- "valueType": "number"
10584
- }
10585
- ],
10591
+ "temperature-sensor": [{
10592
+ "path": "celsius",
10593
+ "valueType": "number"
10594
+ }],
10586
10595
  "update": [
10587
10596
  {
10588
10597
  "path": "currentVersion",
@@ -10693,10 +10702,6 @@ Object.freeze({
10693
10702
  "path": "temperature",
10694
10703
  "valueType": "number"
10695
10704
  },
10696
- {
10697
- "path": "temperatureUnit",
10698
- "valueType": "string"
10699
- },
10700
10705
  {
10701
10706
  "path": "humidity",
10702
10707
  "valueType": "number"
@@ -10705,18 +10710,10 @@ Object.freeze({
10705
10710
  "path": "pressure",
10706
10711
  "valueType": "number"
10707
10712
  },
10708
- {
10709
- "path": "pressureUnit",
10710
- "valueType": "string"
10711
- },
10712
10713
  {
10713
10714
  "path": "windSpeed",
10714
10715
  "valueType": "number"
10715
10716
  },
10716
- {
10717
- "path": "windSpeedUnit",
10718
- "valueType": "string"
10719
- },
10720
10717
  {
10721
10718
  "path": "windBearing",
10722
10719
  "valueType": "number"
@@ -18229,7 +18226,8 @@ var CompositionProblemSchema = object({
18229
18226
  "item-field-reserved",
18230
18227
  "target-missing",
18231
18228
  "target-unreadable",
18232
- "not-replaceable"
18229
+ "not-replaceable",
18230
+ "native-capability-replace"
18233
18231
  ]),
18234
18232
  path: string(),
18235
18233
  message: string()
@@ -18287,9 +18285,11 @@ var CompositionFieldPlanSchema = object({
18287
18285
  reads: array(CompositionFieldReadSchema)
18288
18286
  });
18289
18287
  /**
18290
- * What a feature does to its target (D663): `new` builds a capability on a new
18291
- * device; on an existing device it `add`s a capability the device lacks, or
18292
- * `replace`s named fields of one it has natively.
18288
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18289
+ * new device; on an existing device it `add`s a capability the device lacks.
18290
+ * Replacing a capability the device already serves is refused
18291
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18292
+ * wire for a claim the hub already holds.
18293
18293
  */
18294
18294
  var CompositionFeatureModeSchema = _enum([
18295
18295
  "new",
@@ -20288,12 +20288,12 @@ function buildDeviceAdminUrl(parts) {
20288
20288
  * providers detect renames by matching `uniqueId` and patch `id`
20289
20289
  * in-place so dispatch keeps working.
20290
20290
  *
20291
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20292
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20293
- * state push and are the single source of truth. The `device_class` tag is
20294
- * an HA-specific input that is consumed at the discovery boundary
20295
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20296
- * stored after that point.
20291
+ * **Display unit and precision are NOT stored here.** They are a read-time
20292
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20293
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20294
+ * neither is repeated on each sample. The `device_class` tag is an
20295
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20296
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20297
20297
  *
20298
20298
  * The `raw` escape hatch carries provider-specific attributes that don't
20299
20299
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20307,6 +20307,14 @@ var SourceInfoSchema = object({
20307
20307
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20308
20308
  * Used to detect rename when `id` changes. */
20309
20309
  uniqueId: string().optional(),
20310
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20311
+ quantity: ReadingQuantitySchema.optional(),
20312
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20313
+ dateTimeShape: _enum([
20314
+ "date",
20315
+ "time",
20316
+ "datetime"
20317
+ ]).optional(),
20310
20318
  /** Provider-specific extras that don't fit the structured slots.
20311
20319
  * Whitelist what goes here per provider — do NOT dump entire HA
20312
20320
  * attribute blobs (would explode the meta JSON column). */
@@ -20352,6 +20360,80 @@ function mergeSourceInfo(prev, patch) {
20352
20360
  return next;
20353
20361
  }
20354
20362
  /**
20363
+ * Coerce common unit-of-measurement string variants to a canonical form
20364
+ * so the UI's display templates can rely on stable strings. Pure
20365
+ * passthrough for unknown units — kept conservative on purpose, the goal
20366
+ * is to normalise the long tail of HA `unit_of_measurement` values that
20367
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
20368
+ *
20369
+ * Extend the map as we encounter real-world data. Returning the raw
20370
+ * input for unknown values is intentional: providers should never
20371
+ * lose information silently here.
20372
+ */
20373
+ function normalizeUnit(raw) {
20374
+ if (raw == null) return void 0;
20375
+ const trimmed = raw.trim();
20376
+ if (trimmed === "") return void 0;
20377
+ const lower = trimmed.toLowerCase();
20378
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
20379
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
20380
+ if (lower === "k" || lower === "kelvin") return "K";
20381
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
20382
+ if (lower === "lx" || lower === "lux") return "lx";
20383
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
20384
+ if (lower === "kpa") return "kPa";
20385
+ if (lower === "pa") return "Pa";
20386
+ if (lower === "mbar" || lower === "millibar") return "mbar";
20387
+ if (lower === "w" || lower === "watt") return "W";
20388
+ if (lower === "kw" || lower === "kilowatt") return "kW";
20389
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
20390
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
20391
+ if (lower === "v" || lower === "volt") return "V";
20392
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
20393
+ if (lower === "ma" || lower === "milliampere") return "mA";
20394
+ if (lower === "va") return "VA";
20395
+ if (lower === "ppm") return "ppm";
20396
+ if (lower === "ppb") return "ppb";
20397
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
20398
+ if (lower === "km/h" || lower === "kph") return "km/h";
20399
+ if (lower === "mph") return "mph";
20400
+ if (lower === "m/s") return "m/s";
20401
+ if (lower === "m") return "m";
20402
+ if (lower === "m²" || lower === "m2") return "m²";
20403
+ if (lower === "mm") return "mm";
20404
+ if (lower === "cm") return "cm";
20405
+ if (lower === "km") return "km";
20406
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20407
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20408
+ if (lower === "kg") return "kg";
20409
+ if (lower === "g") return "g";
20410
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20411
+ if (lower === "ml") return "mL";
20412
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20413
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20414
+ if (lower === "ms") return "ms";
20415
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20416
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20417
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20418
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20419
+ if (lower === "kib") return "KiB";
20420
+ if (lower === "mib") return "MiB";
20421
+ if (lower === "gib") return "GiB";
20422
+ if (lower === "tib") return "TiB";
20423
+ if (lower === "kb") return "kB";
20424
+ if (lower === "mb") return "MB";
20425
+ if (lower === "gb") return "GB";
20426
+ if (lower === "dbm") return "dBm";
20427
+ if (lower === "dbfs") return "dBFS";
20428
+ if (lower === "inhg") return "inHg";
20429
+ if (lower === "mm/h") return "mm/h";
20430
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20431
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20432
+ if (lower === "kbps") return "kbps";
20433
+ if (lower === "fps") return "fps";
20434
+ return trimmed;
20435
+ }
20436
+ /**
20355
20437
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
20356
20438
  * blob is a free-form `Record<string, unknown>` shared with hardware-
20357
20439
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -41428,7 +41510,8 @@ method(object({
41428
41510
  "enum"
41429
41511
  ]),
41430
41512
  enumValues: array(string()).optional(),
41431
- item: boolean().optional()
41513
+ item: boolean().optional(),
41514
+ quantity: ReadingQuantitySchema.optional()
41432
41515
  })).readonly(),
41433
41516
  itemArray: object({
41434
41517
  path: string(),
@@ -45272,8 +45355,8 @@ var MaskPolygonShapeSchema = object({
45272
45355
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45273
45356
  var MaskGridShapeSchema = object({
45274
45357
  kind: literal("grid"),
45275
- gridWidth: number(),
45276
- gridHeight: number(),
45358
+ gridWidth: number().meta(readingMeta("none")),
45359
+ gridHeight: number().meta(readingMeta("none")),
45277
45360
  cells: array(boolean())
45278
45361
  });
45279
45362
  discriminatedUnion("kind", [
@@ -56753,27 +56836,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56753
56836
  */
56754
56837
  var AirQualitySensorStatusSchema = object({
56755
56838
  /** Carbon dioxide concentration in ppm. */
56756
- co2Ppm: number().min(0).optional(),
56839
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56757
56840
  /** Total volatile organic compounds in ppb. */
56758
- vocPpb: number().min(0).optional(),
56841
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56759
56842
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56760
- pm25: number().min(0).optional(),
56843
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56761
56844
  /** Particulate matter ≤ 10 μm in µg/m³. */
56762
- pm10: number().min(0).optional(),
56845
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56763
56846
  /** Composite AQI value (typically 0..500). */
56764
- aqi: number().optional(),
56847
+ aqi: number().optional().meta(readingMeta("index")),
56765
56848
  /** Ms epoch when the slice was last updated. */
56766
- lastFetchedAt: number(),
56767
- /** Live display unit of the single metric this slice carries (e.g. HA
56768
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56769
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56770
- * per slice is unambiguous. */
56771
- unit: string().optional(),
56772
- /** Suggested decimal places for numeric display.
56773
- * Populated live from the upstream source when provided (e.g. HA
56774
- * `attributes.suggested_display_precision`). Falls back to
56775
- * auto-formatting when absent. */
56776
- precision: number().int().min(0).max(10).optional()
56849
+ lastFetchedAt: number()
56777
56850
  });
56778
56851
  var airQualitySensorCapability = {
56779
56852
  name: "air-quality-sensor",
@@ -56810,12 +56883,12 @@ var airQualitySensorCapability = {
56810
56883
  },
56811
56884
  pm25: {
56812
56885
  kind: "gauge",
56813
- unit: "ug/m3",
56886
+ unit: "µg/m³",
56814
56887
  deadband: 1
56815
56888
  },
56816
56889
  pm10: {
56817
56890
  kind: "gauge",
56818
- unit: "ug/m3",
56891
+ unit: "µg/m³",
56819
56892
  deadband: 1
56820
56893
  },
56821
56894
  aqi: {
@@ -56833,22 +56906,13 @@ var airQualitySensorCapability = {
56833
56906
  * entries with `device_class: illuminance`.
56834
56907
  */
56835
56908
  var AmbientLightSensorStatusSchema = object({
56836
- /** Current illuminance in lux (lx). */
56837
- lux: number().min(0).meta({
56838
- unit: "lx",
56909
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56910
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56911
+ precision: 0,
56839
56912
  label: "Illuminance"
56840
- }),
56913
+ })),
56841
56914
  /** Ms epoch when the slice was last updated. */
56842
- lastFetchedAt: number(),
56843
- /** Live display unit from the upstream source (e.g. HA
56844
- * `attributes.unit_of_measurement`). The UI prefers this over the
56845
- * role's canonical unit. Absent → fall back to the canonical unit. */
56846
- unit: string().optional(),
56847
- /** Suggested decimal places for numeric display.
56848
- * Populated live from the upstream source when provided (e.g. HA
56849
- * `attributes.suggested_display_precision`). Falls back to
56850
- * auto-formatting when absent. */
56851
- precision: number().int().min(0).max(10).optional()
56915
+ lastFetchedAt: number()
56852
56916
  });
56853
56917
  var ambientLightSensorCapability = {
56854
56918
  name: "ambient-light-sensor",
@@ -56880,12 +56944,7 @@ var ambientLightSensorCapability = {
56880
56944
  } } },
56881
56945
  /** Clock fields: written, but excluded from the compare that decides
56882
56946
  * whether persisting is worth a SQLite commit. */
56883
- volatileStateFields: ["lastFetchedAt"],
56884
- customize: {
56885
- version: 1,
56886
- advanced: "derive",
56887
- targets: []
56888
- }
56947
+ volatileStateFields: ["lastFetchedAt"]
56889
56948
  };
56890
56949
  /**
56891
56950
  * Per-class audio metrics aggregated over a sliding window.
@@ -57112,10 +57171,7 @@ var BatteryStatusSchema = object({
57112
57171
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57113
57172
  * declines to band a non-finite reading, which is the correct shape.
57114
57173
  */
57115
- percentage: number().min(0).max(100).nullable().meta({
57116
- unit: "%",
57117
- label: "Charge"
57118
- }),
57174
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57119
57175
  /**
57120
57176
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57121
57177
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57142,7 +57198,14 @@ var BatteryStatusSchema = object({
57142
57198
  */
57143
57199
  sleeping: boolean().meta({ label: "Sleeping" }),
57144
57200
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57145
- lastUpdated: number(),
57201
+ lastUpdated: number().meta(readingMeta("none")),
57202
+ /**
57203
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57204
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57205
+ * that only report charge. A second reading on this cap, not a second
57206
+ * `numeric-sensor` on the same device.
57207
+ */
57208
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57146
57209
  /**
57147
57210
  * Ms epoch of the last time the device PROVED it was reachable — a
57148
57211
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57194,7 +57257,6 @@ var BATTERY_DEVICE_TYPES = [
57194
57257
  /** Generic Customize target for the percentage reading (D936). */
57195
57258
  var batteryCustomize = {
57196
57259
  version: 1,
57197
- advanced: "derive",
57198
57260
  targets: [{
57199
57261
  id: "percentage",
57200
57262
  cardinality: "scalar",
@@ -57203,7 +57265,7 @@ var batteryCustomize = {
57203
57265
  stateField: "percentage"
57204
57266
  },
57205
57267
  deviceTypes: BATTERY_DEVICE_TYPES,
57206
- apply: "add-or-replace",
57268
+ apply: "add",
57207
57269
  path: "percentage",
57208
57270
  sources: [{
57209
57271
  kind: "field",
@@ -57324,10 +57386,16 @@ onStatusChanged: { data: object({
57324
57386
  /**
57325
57387
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57326
57388
  */
57327
- history: { fields: { percentage: {
57328
- kind: "gauge",
57329
- unit: "%"
57330
- } } },
57389
+ history: { fields: {
57390
+ percentage: {
57391
+ kind: "gauge",
57392
+ unit: "%"
57393
+ },
57394
+ volts: {
57395
+ kind: "gauge",
57396
+ unit: "V"
57397
+ }
57398
+ } },
57331
57399
  customize: batteryCustomize,
57332
57400
  /** Clock fields: written, but excluded from the compare that decides
57333
57401
  * whether persisting is worth a SQLite commit. */
@@ -57374,12 +57442,7 @@ var binaryCapability = {
57374
57442
  history: { fields: { on: { kind: "boolean" } } },
57375
57443
  /** Clock fields: written, but excluded from the compare that decides
57376
57444
  * whether persisting is worth a SQLite commit. */
57377
- volatileStateFields: ["lastChangedAt"],
57378
- customize: {
57379
- version: 1,
57380
- advanced: "derive",
57381
- targets: []
57382
- }
57445
+ volatileStateFields: ["lastChangedAt"]
57383
57446
  };
57384
57447
  /**
57385
57448
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57395,7 +57458,7 @@ var binaryCapability = {
57395
57458
  */
57396
57459
  var BrightnessStatusSchema = object({
57397
57460
  /** Current level as 0..100 inclusive. Firmware-reported. */
57398
- percentage: number().min(0).max(100),
57461
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57399
57462
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57400
57463
  lastChangedAt: number()
57401
57464
  });
@@ -57849,21 +57912,21 @@ var ClimateControlStatusSchema = object({
57849
57912
  /** Single setpoint in Celsius. Used by single-target modes
57850
57913
  * (heat / cool / dry). Null when the device is in a range mode
57851
57914
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57852
- target: number().nullable(),
57915
+ target: number().nullable().meta(readingMeta("temperature")),
57853
57916
  /** Upper bound of the dual setpoint in Celsius. Populated only
57854
57917
  * in `heat_cool` mode. */
57855
- targetHigh: number().nullable(),
57918
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57856
57919
  /** Lower bound of the dual setpoint in Celsius. Populated only
57857
57920
  * in `heat_cool` mode. */
57858
- targetLow: number().nullable(),
57921
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57859
57922
  /** Target relative humidity (0..100). Null when the device has no
57860
57923
  * humidity control. */
57861
- targetHumidity: number().min(0).max(100).nullable(),
57924
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57862
57925
  /** Current measured temperature in Celsius. Read-only — pushed by
57863
57926
  * the device's internal sensor. */
57864
- currentTemp: number().nullable(),
57927
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57865
57928
  /** Current measured relative humidity (0..100). Read-only. */
57866
- currentHumidity: number().min(0).max(100).nullable(),
57929
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57867
57930
  /** Vertical louver swing on/off. Null when the device has no vertical
57868
57931
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57869
57932
  swingVertical: boolean().nullable(),
@@ -57871,11 +57934,11 @@ var ClimateControlStatusSchema = object({
57871
57934
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57872
57935
  swingHorizontal: boolean().nullable(),
57873
57936
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57874
- minTemp: number().nullable().optional(),
57937
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57875
57938
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57876
- maxTemp: number().nullable().optional(),
57939
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57877
57940
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57878
- tempStep: number().positive().nullable().optional(),
57941
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57879
57942
  /** Ms epoch when the slice was last updated (push or command). */
57880
57943
  lastFetchedAt: number()
57881
57944
  });
@@ -58017,17 +58080,17 @@ var climateControlCapability = {
58017
58080
  * modes to expose in the picker without a slice fetch.
58018
58081
  */
58019
58082
  var RgbTripletSchema = object({
58020
- r: number().int().min(0).max(255),
58021
- g: number().int().min(0).max(255),
58022
- b: number().int().min(0).max(255)
58083
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58084
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58085
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58023
58086
  });
58024
58087
  var HsvTripletSchema = object({
58025
58088
  /** Hue in degrees, 0..360. */
58026
- h: number().min(0).max(360),
58089
+ h: number().min(0).max(360).meta(readingMeta("none")),
58027
58090
  /** Saturation as 0..100 inclusive. */
58028
- s: number().min(0).max(100),
58091
+ s: number().min(0).max(100).meta(readingMeta("none")),
58029
58092
  /** Value/brightness as 0..100 inclusive. */
58030
- v: number().min(0).max(100)
58093
+ v: number().min(0).max(100).meta(readingMeta("none"))
58031
58094
  });
58032
58095
  var ColorInputSchema = discriminatedUnion("mode", [
58033
58096
  object({
@@ -58056,7 +58119,7 @@ var ColorStatusSchema = object({
58056
58119
  /** Populated when `mode === 'hsv'`. */
58057
58120
  hsv: HsvTripletSchema.optional(),
58058
58121
  /** Populated when `mode === 'mired'`. */
58059
- mireds: number().int().optional(),
58122
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58060
58123
  /** Ms epoch of the last operator-driven change. */
58061
58124
  lastChangedAt: number()
58062
58125
  });
@@ -58251,18 +58314,12 @@ _enum([
58251
58314
  "empty",
58252
58315
  "full"
58253
58316
  ]);
58254
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58255
- * each. */
58256
- var ConsumableRemainingUnitSchema = _enum([
58257
- "days",
58258
- "hours",
58259
- "cycles"
58260
- ]);
58261
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58262
- * days). `value` null = unknown (D393). */
58317
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58318
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58319
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58263
58320
  var ConsumableRemainingSchema = object({
58264
- value: number().min(0).nullable(),
58265
- unit: ConsumableRemainingUnitSchema
58321
+ value: number().min(0).nullable().meta(readingMeta("device")),
58322
+ kind: ConsumableRemainingKindSchema
58266
58323
  });
58267
58324
  /** A single consumable item. Either a continuous `level` (remaining
58268
58325
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58275,7 +58332,7 @@ var ConsumableItemSchema = object({
58275
58332
  /** Display name. */
58276
58333
  label: string().min(1),
58277
58334
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58278
- level: number().min(0).max(100).nullable(),
58335
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58279
58336
  /** The provider's discrete verdict when known (binary mode). */
58280
58337
  status: ConsumableStatusSchema.nullable(),
58281
58338
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58357,8 +58414,8 @@ var consumablesCustomize = {
58357
58414
  transforms: [{ kind: "identity" }]
58358
58415
  }],
58359
58416
  literals: [{
58360
- path: "remaining.unit",
58361
- values: [...ConsumableRemainingUnitSchema.options]
58417
+ path: "remaining.kind",
58418
+ values: [...ConsumableRemainingKindSchema.options]
58362
58419
  }]
58363
58420
  },
58364
58421
  {
@@ -58487,7 +58544,6 @@ reset: method(object({
58487
58544
  },
58488
58545
  "remaining.value": {
58489
58546
  kind: "gauge",
58490
- unitFrom: "remaining.unit",
58491
58547
  deadband: 1,
58492
58548
  displayName: "Remaining"
58493
58549
  }
@@ -58557,10 +58613,9 @@ var contactCapability = {
58557
58613
  * (empty array for the other kinds). It lives in the slice because
58558
58614
  * HA `select.*` entities may update their options at runtime.
58559
58615
  *
58560
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58561
- * `kind === 'numeric'` and are absent (undefined) for all other
58562
- * kinds. They are populated live from HA attributes on each state
58563
- * push — consistent with `options` and the sensor-unit approach.
58616
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58617
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58618
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58564
58619
  */
58565
58620
  var ControlStatusSchema = object({
58566
58621
  kind: _enum([
@@ -58572,39 +58627,17 @@ var ControlStatusSchema = object({
58572
58627
  /** Current value. Numeric kind → number; select/text/datetime → string.
58573
58628
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58574
58629
  * or time-only — HA accepts all three forms). */
58575
- value: union([number(), string()]),
58630
+ value: union([number().meta(readingMeta("device")), string()]),
58576
58631
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58577
58632
  options: array(string()),
58578
58633
  /** Ms epoch when the slice was last updated. */
58579
58634
  lastChangedAt: number(),
58580
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58581
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58582
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58583
- unit: string().optional(),
58584
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58585
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58586
- min: number().optional(),
58587
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58588
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58589
- max: number().optional(),
58590
- /** Step increment. Populated live from HA `attributes.step`.
58591
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58592
- step: number().optional(),
58593
- /** Suggested decimal places for numeric display. Populated live from HA
58594
- * `attributes.suggested_display_precision`, or derived from `step` when the
58595
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58596
- * other kinds. Falls back to auto-formatting when absent. */
58597
- precision: number().int().min(0).max(10).optional(),
58598
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58599
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58600
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58601
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58602
- * `has_date`/`has_time` attributes on each push. */
58603
- format: _enum([
58604
- "date",
58605
- "time",
58606
- "datetime"
58607
- ]).optional()
58635
+ /** Minimum allowed value, in the device quantity's base unit. */
58636
+ min: number().optional().meta(readingMeta("device")),
58637
+ /** Maximum allowed value, in the device quantity's base unit. */
58638
+ max: number().optional().meta(readingMeta("device")),
58639
+ /** Step increment, in the device quantity's base unit. */
58640
+ step: number().optional().meta(readingMeta("device"))
58608
58641
  });
58609
58642
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58610
58643
  object({
@@ -58670,9 +58703,9 @@ var CoverStatusSchema = object({
58670
58703
  ]),
58671
58704
  /** 0 = fully closed, 100 = fully open. Null when the device has
58672
58705
  * no intermediate position surface. */
58673
- position: number().min(0).max(100).nullable(),
58706
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58674
58707
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58675
- tiltPosition: number().min(0).max(100).nullable(),
58708
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58676
58709
  /** Ms epoch when the slice was last updated. */
58677
58710
  lastChangedAt: number()
58678
58711
  });
@@ -58775,9 +58808,9 @@ var NormalizedRangeSchema$1 = object({
58775
58808
  var DayNightStatusSchema = object({
58776
58809
  mode: DayNightModeSchema,
58777
58810
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58778
- sensitivity: number().optional(),
58811
+ sensitivity: number().optional().meta(readingMeta("none")),
58779
58812
  /** Delay before the IR-cut filter flips, in seconds. */
58780
- switchDelaySec: number().optional(),
58813
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58781
58814
  lastFetchedAt: number()
58782
58815
  });
58783
58816
  /**
@@ -58995,11 +59028,11 @@ var DishwasherStatusSchema = object({
58995
59028
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58996
59029
  availablePrograms: array(string()),
58997
59030
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58998
- remainingSeconds: number().int().nonnegative().nullable(),
59031
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58999
59032
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59000
- delayedStartSeconds: number().int().nonnegative().nullable(),
59033
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59001
59034
  /** Cycle completion 0..100, or null when not applicable. */
59002
- progressPercent: number().min(0).max(100).nullable(),
59035
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59003
59036
  /** True when the door is reported open; null when the door state is not known. */
59004
59037
  doorOpen: boolean().nullable(),
59005
59038
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59023,7 +59056,7 @@ var DishwasherStatusSchema = object({
59023
59056
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59024
59057
  machineCareDue: boolean().nullable(),
59025
59058
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59026
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59059
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59027
59060
  /** Option states: null when the appliance does not expose the option or has not said. */
59028
59061
  halfLoad: boolean().nullable(),
59029
59062
  hygienePlus: boolean().nullable(),
@@ -59038,7 +59071,7 @@ var DishwasherStatusSchema = object({
59038
59071
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59039
59072
  activeEvents: array(string()),
59040
59073
  /** Numeric error code when the appliance reports one; null otherwise. */
59041
- errorCode: number().nullable(),
59074
+ errorCode: number().nullable().meta(readingMeta("none")),
59042
59075
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59043
59076
  errorLabel: string().nullable(),
59044
59077
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59269,7 +59302,7 @@ var DoorbellStatusSchema = object({
59269
59302
  /** Ms epoch of the last press. null = never observed since this provider started. */
59270
59303
  lastPressedAt: number().nullable(),
59271
59304
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59272
- pressCountSinceStart: number()
59305
+ pressCountSinceStart: number().meta(readingMeta("count"))
59273
59306
  });
59274
59307
  var DoorbellPressEventSchema = object({
59275
59308
  deviceId: number(),
@@ -59334,17 +59367,7 @@ onPressed: {
59334
59367
  history: "none",
59335
59368
  customize: doorbellCustomize
59336
59369
  };
59337
- /**
59338
- * Enum-state sensor — a string value picked from a finite option set.
59339
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59340
- * states, weather conditions, contact-source channels, mode strings).
59341
- * The option set is stable across observations; it is persisted in the
59342
- * device config blob (via the control slice at adoption) and is NOT
59343
- * stored in `sourceInfo`.
59344
- */
59345
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59346
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59347
- var EnumSensorDateTimeFormatSchema = _enum([
59370
+ _enum([
59348
59371
  "date",
59349
59372
  "time",
59350
59373
  "datetime"
@@ -59352,12 +59375,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59352
59375
  var EnumSensorStatusSchema = object({
59353
59376
  value: string(),
59354
59377
  /**
59355
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59356
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59357
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59358
- * a time-only sensor → `'time'`.
59378
+ * Set when `value` is a timestamp rather than an enum state, so history
59379
+ * does not learn ISO strings as states. The display shape (date / time /
59380
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59359
59381
  */
59360
- format: EnumSensorDateTimeFormatSchema.optional(),
59382
+ timestamp: boolean().optional(),
59361
59383
  /** Ms epoch when the slice was last updated. */
59362
59384
  lastFetchedAt: number()
59363
59385
  });
@@ -59384,11 +59406,11 @@ var enumSensorCapability = {
59384
59406
  * History (D850): the value as an `enum` series (an appliance's program or
59385
59407
  * operation state, an HVAC action). The option set is per device, so the
59386
59408
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59387
- * `format` carries a timestamp, not a state, and is not recorded.
59409
+ * `timestamp` carries a clock, not a state, and is not recorded.
59388
59410
  */
59389
59411
  history: { fields: { value: {
59390
59412
  kind: "enum",
59391
- skipWhenSet: "format"
59413
+ skipWhenSet: "timestamp"
59392
59414
  } } },
59393
59415
  /** Clock fields: written, but excluded from the compare that decides
59394
59416
  * whether persisting is worth a SQLite commit. */
@@ -59408,13 +59430,13 @@ var EventFireSchema = object({
59408
59430
  deviceId: number(),
59409
59431
  eventType: string(),
59410
59432
  data: record(string(), unknown()).nullable(),
59411
- timestamp: number(),
59412
- seq: number()
59433
+ timestamp: number().meta(readingMeta("none")),
59434
+ seq: number().meta(readingMeta("count"))
59413
59435
  });
59414
59436
  var EventEmitterStatusSchema = object({
59415
59437
  eventTypes: array(string()),
59416
59438
  lastEvent: EventFireSchema.nullable(),
59417
- eventCountSinceStart: number()
59439
+ eventCountSinceStart: number().meta(readingMeta("count"))
59418
59440
  });
59419
59441
  var eventEmitterCapability = {
59420
59442
  name: "event-emitter",
@@ -59827,10 +59849,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59827
59849
  var FanControlStatusSchema = object({
59828
59850
  /** Active speed as 0..100 inclusive. Null when the device has no
59829
59851
  * speed surface (single-speed fan). */
59830
- percentage: number().min(0).max(100).nullable(),
59852
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59831
59853
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59832
59854
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59833
- percentageStep: number().positive().optional(),
59855
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59834
59856
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59835
59857
  * — empty string when no preset is active or supported. */
59836
59858
  preset: string(),
@@ -59951,7 +59973,7 @@ var FeatureProbeStatusSchema = object({
59951
59973
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59952
59974
  model: string().nullable(),
59953
59975
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59954
- channelCount: number().nullable(),
59976
+ channelCount: number().nullable().meta(readingMeta("count")),
59955
59977
  /**
59956
59978
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59957
59979
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60076,7 +60098,7 @@ var gasCapability = {
60076
60098
  };
60077
60099
  var HeatingValveStatusSchema = object({
60078
60100
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60079
- position: number().min(0).max(100).nullable(),
60101
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60080
60102
  /** Ms epoch when the slice was last updated (push or poll). */
60081
60103
  lastFetchedAt: number()
60082
60104
  });
@@ -60119,9 +60141,9 @@ var HumidifierStatusSchema = object({
60119
60141
  /** Whether the humidifier is currently on. */
60120
60142
  on: boolean(),
60121
60143
  /** Current measured relative humidity (0..100). Null when not reported. */
60122
- currentHumidity: number().min(0).max(100).nullable(),
60144
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60123
60145
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60124
- targetHumidity: number().min(0).max(100).nullable(),
60146
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60125
60147
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60126
60148
  * device has no mode surface. */
60127
60149
  mode: string().nullable(),
@@ -60131,9 +60153,9 @@ var HumidifierStatusSchema = object({
60131
60153
  * `idle` / `off`). Null when the device doesn't report it. */
60132
60154
  action: string().nullable(),
60133
60155
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60134
- minHumidity: number().nullable(),
60156
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60135
60157
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60136
- maxHumidity: number().nullable(),
60158
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60137
60159
  /** Ms epoch when the slice was last updated. */
60138
60160
  lastChangedAt: number()
60139
60161
  });
@@ -60203,22 +60225,13 @@ var humidifierCapability = {
60203
60225
  * `sourceInfo`.
60204
60226
  */
60205
60227
  var HumiditySensorStatusSchema = object({
60206
- /** Current relative humidity, 0..100. */
60207
- percent: number().min(0).max(100).meta({
60208
- unit: "%",
60228
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60229
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60230
+ precision: 0,
60209
60231
  label: "Humidity"
60210
- }),
60232
+ })),
60211
60233
  /** Ms epoch when the slice was last updated. */
60212
- lastFetchedAt: number(),
60213
- /** Live display unit from the upstream source (e.g. HA
60214
- * `attributes.unit_of_measurement`). The UI prefers this over the
60215
- * role's canonical unit. Absent → fall back to the canonical unit. */
60216
- unit: string().optional(),
60217
- /** Suggested decimal places for numeric display.
60218
- * Populated live from the upstream source when provided (e.g. HA
60219
- * `attributes.suggested_display_precision`). Falls back to
60220
- * auto-formatting when absent. */
60221
- precision: number().int().min(0).max(10).optional()
60234
+ lastFetchedAt: number()
60222
60235
  });
60223
60236
  var humiditySensorCapability = {
60224
60237
  name: "humidity-sensor",
@@ -60249,12 +60262,7 @@ var humiditySensorCapability = {
60249
60262
  } } },
60250
60263
  /** Clock fields: written, but excluded from the compare that decides
60251
60264
  * whether persisting is worth a SQLite commit. */
60252
- volatileStateFields: ["lastFetchedAt"],
60253
- customize: {
60254
- version: 1,
60255
- advanced: "derive",
60256
- targets: []
60257
- }
60265
+ volatileStateFields: ["lastFetchedAt"]
60258
60266
  };
60259
60267
  /**
60260
60268
  * Image display cap. Models a single still image exposed by an integration —
@@ -60275,7 +60283,7 @@ var ImageStatusSchema = object({
60275
60283
  * entity exposes no `entity_picture` (yet). */
60276
60284
  url: string().nullable(),
60277
60285
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60278
- lastUpdated: number().nullable()
60286
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60279
60287
  });
60280
60288
  var imageCapability = {
60281
60289
  name: "image",
@@ -60363,19 +60371,19 @@ var NormalizedRangeSchema = object({
60363
60371
  */
60364
60372
  var ImageSettingsStatusSchema = object({
60365
60373
  /** Normalized 0–100. */
60366
- brightness: number().optional(),
60374
+ brightness: number().optional().meta(readingMeta("none")),
60367
60375
  /** Normalized 0–100. */
60368
- contrast: number().optional(),
60376
+ contrast: number().optional().meta(readingMeta("none")),
60369
60377
  /** Normalized 0–100. */
60370
- saturation: number().optional(),
60378
+ saturation: number().optional().meta(readingMeta("none")),
60371
60379
  /** Normalized 0–100. */
60372
- sharpness: number().optional(),
60380
+ sharpness: number().optional().meta(readingMeta("none")),
60373
60381
  mirror: boolean().optional(),
60374
60382
  flip: boolean().optional(),
60375
60383
  rotate: ImageRotateSchema.optional(),
60376
60384
  whiteBalance: WhiteBalanceModeSchema.optional(),
60377
60385
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60378
- warmth: number().optional(),
60386
+ warmth: number().optional().meta(readingMeta("none")),
60379
60387
  exposureMode: ExposureModeSchema.optional(),
60380
60388
  backlightMode: BacklightModeSchema.optional(),
60381
60389
  lastFetchedAt: number()
@@ -60654,10 +60662,10 @@ var IntercomAbilitySchema = object({
60654
60662
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60655
60663
  codecs: array(string()),
60656
60664
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60657
- sampleRate: number(),
60665
+ sampleRate: number().meta(readingMeta("none")),
60658
60666
  duplex: _enum(["half", "full"]),
60659
60667
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60660
- maxBacklogMs: number()
60668
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60661
60669
  });
60662
60670
  var IntercomStatusSchema = object({
60663
60671
  /** True while a talk session is open. */
@@ -60884,13 +60892,13 @@ var LawnMowerControlStatusSchema = object({
60884
60892
  activity: LawnMowerActivitySchema,
60885
60893
  /** 0..100 battery percentage. Null when the device has no battery
60886
60894
  * reading. */
60887
- batteryLevel: number().min(0).max(100).nullable(),
60895
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60888
60896
  /** 0..100 mowing-completion percentage of the current task, or null when no
60889
60897
  * task is active / progress is unavailable. */
60890
- progressPercent: number().min(0).max(100).nullable(),
60898
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60891
60899
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60892
60900
  * or null when unknown. */
60893
- currentCode: number().nullable(),
60901
+ currentCode: number().nullable().meta(readingMeta("none")),
60894
60902
  /** Human label for {@link currentCode}, or null when undecodable. */
60895
60903
  currentCodeLabel: string().nullable(),
60896
60904
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61423,7 +61431,7 @@ var MediaPlayerStatusSchema = object({
61423
61431
  state: MediaPlayerStateSchema,
61424
61432
  /** Volume as 0..100 inclusive. Null when the device has no volume
61425
61433
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61426
- volumeLevel: number().min(0).max(100).nullable(),
61434
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61427
61435
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61428
61436
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61429
61437
  isMuted: boolean().nullable(),
@@ -61436,10 +61444,10 @@ var MediaPlayerStatusSchema = object({
61436
61444
  currentMedia: MediaInfoSchema.nullable(),
61437
61445
  /** Current playback position in ms. Null when not seekable or
61438
61446
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61439
- positionMs: number().int().nonnegative().nullable(),
61447
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61440
61448
  /** Total duration of the current media in ms. Null when unknown
61441
61449
  * (live stream) or nothing is playing. */
61442
- durationMs: number().int().nonnegative().nullable(),
61450
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61443
61451
  /** Shuffle toggle. Null when no shuffle surface.
61444
61452
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61445
61453
  shuffle: boolean().nullable(),
@@ -61748,7 +61756,7 @@ var motionCustomize = {
61748
61756
  eventName: "onMotionChanged"
61749
61757
  },
61750
61758
  deviceTypes: [DeviceType.Camera],
61751
- apply: "replace",
61759
+ apply: "add",
61752
61760
  applyAuthority: {
61753
61761
  kind: "camera-sensors",
61754
61762
  timeoutField: "motionCooldownMs"
@@ -61820,7 +61828,7 @@ var MotionStatusSchema = object({
61820
61828
  * it, every provider seeded it at boot, and every session left it
61821
61829
  * `detected: true` after its detach.
61822
61830
  */
61823
- autoClearAfterMs: number().nullable()
61831
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61824
61832
  });
61825
61833
  /**
61826
61834
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62002,7 +62010,7 @@ var MotionZoneRegionSchema = object({
62002
62010
  * the grid region(s). */
62003
62011
  var MotionZoneStatusSchema = object({
62004
62012
  enabled: boolean(),
62005
- sensitivity: number(),
62013
+ sensitivity: number().meta(readingMeta("none")),
62006
62014
  /** Grid region(s). Today exactly one `grid` shape. */
62007
62015
  regions: array(MotionZoneRegionSchema),
62008
62016
  lastFetchedAt: number()
@@ -62372,7 +62380,7 @@ var NavigationStatusSchema = object({
62372
62380
  following: boolean(),
62373
62381
  flash: boolean(),
62374
62382
  lightMode: NavigationLightModeSchema,
62375
- lightLevel: number().min(40).max(100),
62383
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62376
62384
  /** Ms epoch when the slice was last updated. */
62377
62385
  lastChangedAt: number()
62378
62386
  });
@@ -62506,13 +62514,13 @@ var NetworkLinkStatusSchema = object({
62506
62514
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62507
62515
  * SKIP a null rather than coerce it.
62508
62516
  */
62509
- signalPercent: number().min(0).max(100).nullable(),
62517
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62510
62518
  /** Raw received signal strength in dBm, when the firmware reports one. */
62511
- rssiDbm: number().optional(),
62519
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62512
62520
  /** Network name of a wireless link, when the firmware reports it. */
62513
62521
  ssid: string().optional(),
62514
62522
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62515
- lastUpdated: number()
62523
+ lastUpdated: number().meta(readingMeta("none"))
62516
62524
  });
62517
62525
  var networkLinkCapability = {
62518
62526
  name: "network-link",
@@ -62897,7 +62905,7 @@ var NotifierStatusSchema = object({
62897
62905
  lastError: string().nullable(),
62898
62906
  /** Number of deliveries currently buffered server-side (rate-limited
62899
62907
  * notifiers may queue). 0 when the provider sends synchronously. */
62900
- queueDepth: number().int().nonnegative(),
62908
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62901
62909
  /** Per-feature capability matrix — authoritative source for the
62902
62910
  * compose-form field gating. */
62903
62911
  supports: NotifierSupportsSchema
@@ -62989,11 +62997,9 @@ onSent: { data: object({
62989
62997
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62990
62998
  * for: water flow, distance, weight, frequency, signal strength, …).
62991
62999
  *
62992
- * The `unit` and `precision` fields carry the live display metadata that
62993
- * the upstream source provides with each state push (HA
62994
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62995
- * The UI reads them directly from the slice — they are the single source
62996
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63000
+ * The quantity is not on this slice. It is chosen when the device is
63001
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63002
+ * that quantity's base unit. `null` is a refused sample (D393).
62997
63003
  *
62998
63004
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62999
63005
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63002,15 +63008,8 @@ onSent: { data: object({
63002
63008
  * numeric values land here.
63003
63009
  */
63004
63010
  var NumericSensorStatusSchema = object({
63005
- value: number(),
63006
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63007
- * Populated live from the upstream source on each state push.
63008
- * Absent for unitless sensors. */
63009
- unit: string().optional(),
63010
- /** Suggested decimal places for numeric display.
63011
- * Populated live from the upstream source when provided.
63012
- * Falls back to auto-formatting when absent. */
63013
- precision: number().int().min(0).max(10).optional(),
63011
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63012
+ value: number().nullable().meta(readingMeta("device")),
63014
63013
  /** Ms epoch when the slice was last updated. */
63015
63014
  lastFetchedAt: number()
63016
63015
  });
@@ -63034,11 +63033,11 @@ var numericSensorCapability = {
63034
63033
  */
63035
63034
  durability: "restored",
63036
63035
  /**
63037
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63036
+ * History (D850): recorded series. The unit is the device quantity's base,
63037
+ * resolved when the series is read — the cap has no single unit.
63038
63038
  */
63039
63039
  history: { fields: { value: {
63040
63040
  kind: "gauge",
63041
- unitFrom: "unit",
63042
63041
  minIntervalMs: 3e4
63043
63042
  } } },
63044
63043
  /** Clock fields: written, but excluded from the compare that decides
@@ -63330,23 +63329,26 @@ var PetFeederStatusSchema = object({
63330
63329
  /** Food currently in the bowl (grams). Null when the device has not
63331
63330
  * reported a reading yet. On dual-hopper models this is the combined
63332
63331
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63333
- foodLevel: number().nullable(),
63332
+ foodLevel: number().nullable().meta(readingMeta("none")),
63334
63333
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63335
63334
  * single-hopper models. */
63336
- food1: number().nullable(),
63335
+ food1: number().nullable().meta(readingMeta("none")),
63337
63336
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63338
63337
  * single-hopper models. */
63339
- food2: number().nullable(),
63338
+ food2: number().nullable().meta(readingMeta("none")),
63340
63339
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63341
63340
  * (`device_class: problem`, on = low). True when the bowl is empty /
63342
63341
  * below the feeder's low threshold. */
63343
63342
  lowFood: boolean(),
63344
63343
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63345
63344
  * device has no battery reading. */
63346
- batteryPower: number().min(0).max(100).nullable(),
63347
- /** Days of desiccant life remaining. Null when the model has no
63348
- * desiccant sensor. */
63349
- desiccantLeftDays: number().nullable(),
63345
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63346
+ /**
63347
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63348
+ * Providers convert from the days the feeder reports. Null when the
63349
+ * model has no desiccant sensor.
63350
+ */
63351
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63350
63352
  /** True while a feed is in progress. */
63351
63353
  feeding: boolean(),
63352
63354
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63357,7 +63359,7 @@ var PetFeederStatusSchema = object({
63357
63359
  * with `errorCode` for consumers that want the raw integer. */
63358
63360
  error: string().nullable(),
63359
63361
  /** Raw device error code (0 / null = no error). */
63360
- errorCode: number().nullable(),
63362
+ errorCode: number().nullable().meta(readingMeta("none")),
63361
63363
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63362
63364
  isDualHopper: boolean(),
63363
63365
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63367,7 +63369,7 @@ var PetFeederStatusSchema = object({
63367
63369
  /** Play a chime when dispensing. */
63368
63370
  feedSound: boolean(),
63369
63371
  /** Speaker / prompt volume level (device-scaled integer). */
63370
- volume: number(),
63372
+ volume: number().meta(readingMeta("none")),
63371
63373
  /** Ms epoch when the slice was last refreshed from the cloud. */
63372
63374
  lastFetchedAt: number()
63373
63375
  });
@@ -63486,7 +63488,7 @@ var petFeederCapability = {
63486
63488
  },
63487
63489
  desiccantLeftDays: {
63488
63490
  kind: "gauge",
63489
- unit: "days"
63491
+ unit: "s"
63490
63492
  }
63491
63493
  } }
63492
63494
  };
@@ -63887,36 +63889,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63887
63889
  */
63888
63890
  var PowerMeterStatusSchema = object({
63889
63891
  /** Instantaneous power draw in watts. */
63890
- watts: number().optional().meta({
63891
- unit: "W",
63892
- label: "Power"
63893
- }),
63892
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63894
63893
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63895
- kwhTotal: number().optional().meta({
63896
- unit: "kWh",
63897
- label: "Energy"
63898
- }),
63894
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63899
63895
  /** Voltage in volts. */
63900
- volts: number().optional(),
63896
+ volts: number().optional().meta(readingMeta("voltage")),
63901
63897
  /** Current in amperes. */
63902
- amps: number().optional(),
63903
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63904
- va: number().optional(),
63898
+ amps: number().optional().meta(readingMeta("current")),
63899
+ /** Apparent power in volt-amperes. */
63900
+ va: number().optional().meta(readingMeta("apparent-power")),
63905
63901
  /** Power factor (0..1) if reported. */
63906
- powerFactor: number().min(0).max(1).optional(),
63902
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63907
63903
  /** Ms epoch when the slice was last updated. */
63908
- lastFetchedAt: number(),
63909
- /** Live display unit of the single metric this slice carries (e.g. HA
63910
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63911
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63912
- * unit per slice is unambiguous. The UI prefers this over the role's
63913
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63914
- unit: string().optional(),
63915
- /** Suggested decimal places for numeric display.
63916
- * Populated live from the upstream source when provided (e.g. HA
63917
- * `attributes.suggested_display_precision`). Falls back to
63918
- * auto-formatting when absent. */
63919
- precision: number().int().min(0).max(10).optional()
63904
+ lastFetchedAt: number()
63920
63905
  });
63921
63906
  var powerMeterCapability = {
63922
63907
  name: "power-meter",
@@ -63980,12 +63965,7 @@ var powerMeterCapability = {
63980
63965
  } },
63981
63966
  /** Clock fields: written, but excluded from the compare that decides
63982
63967
  * whether persisting is worth a SQLite commit. */
63983
- volatileStateFields: ["lastFetchedAt"],
63984
- customize: {
63985
- version: 1,
63986
- advanced: "derive",
63987
- targets: []
63988
- }
63968
+ volatileStateFields: ["lastFetchedAt"]
63989
63969
  };
63990
63970
  /**
63991
63971
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64005,11 +63985,11 @@ var powerMeterCapability = {
64005
63985
  */
64006
63986
  var GpsLocationSchema = object({
64007
63987
  /** Latitude in decimal degrees, -90..90. */
64008
- latitude: number().min(-90).max(90),
63988
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64009
63989
  /** Longitude in decimal degrees, -180..180. */
64010
- longitude: number().min(-180).max(180),
63990
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64011
63991
  /** Reported accuracy in meters (lower = better). */
64012
- accuracyMeters: number().nonnegative()
63992
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64013
63993
  });
64014
63994
  var PresenceStatusSchema = object({
64015
63995
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64021,7 +64001,7 @@ var PresenceStatusSchema = object({
64021
64001
  gps: GpsLocationSchema.nullable(),
64022
64002
  /** Optional battery level of the tracking device (0..100). Null
64023
64003
  * when not reported. */
64024
- batteryPercent: number().min(0).max(100).nullable(),
64004
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64025
64005
  /** Ms epoch when the slice was last updated. */
64026
64006
  lastChangedAt: number()
64027
64007
  });
@@ -64071,19 +64051,10 @@ var presenceCapability = {
64071
64051
  * `sourceInfo`.
64072
64052
  */
64073
64053
  var PressureSensorStatusSchema = object({
64074
- /** Current pressure in hPa. */
64075
- hpa: number(),
64054
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64055
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64076
64056
  /** Ms epoch when the slice was last updated. */
64077
- lastFetchedAt: number(),
64078
- /** Live display unit from the upstream source (e.g. HA
64079
- * `attributes.unit_of_measurement`). The UI prefers this over the
64080
- * role's canonical unit. Absent → fall back to the canonical unit. */
64081
- unit: string().optional(),
64082
- /** Suggested decimal places for numeric display.
64083
- * Populated live from the upstream source when provided (e.g. HA
64084
- * `attributes.suggested_display_precision`). Falls back to
64085
- * auto-formatting when absent. */
64086
- precision: number().int().min(0).max(10).optional()
64057
+ lastFetchedAt: number()
64087
64058
  });
64088
64059
  var pressureSensorCapability = {
64089
64060
  name: "pressure-sensor",
@@ -64497,8 +64468,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64497
64468
  }), _void(), { kind: "mutation" });
64498
64469
  var PtzAutotrackSettingsSchema = object({
64499
64470
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64500
- stopDelaySeconds: number().int().min(0).max(300),
64501
- disappearDelaySeconds: number().int().min(0).max(300)
64471
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64472
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64502
64473
  });
64503
64474
  /**
64504
64475
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64652,7 +64623,7 @@ var RunningStateStatusSchema = object({
64652
64623
  /** null = unknown — never coerced to false (D393). */
64653
64624
  running: boolean().nullable(),
64654
64625
  /** Epoch ms the current boolean began; null while `running` is null. */
64655
- since: number().nullable(),
64626
+ since: number().nullable().meta(readingMeta("none")),
64656
64627
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64657
64628
  phase: string().nullable(),
64658
64629
  /** null until a reading has been published (D393). */
@@ -64667,7 +64638,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64667
64638
  /** Generic Customize target for the running boolean (D936). */
64668
64639
  var runningCustomize = {
64669
64640
  version: 1,
64670
- advanced: "derive",
64671
64641
  targets: [{
64672
64642
  id: "running",
64673
64643
  cardinality: "scalar",
@@ -64687,7 +64657,7 @@ var runningCustomize = {
64687
64657
  DeviceType.Thermostat,
64688
64658
  DeviceType.PetFeeder
64689
64659
  ],
64690
- apply: "add-or-replace",
64660
+ apply: "add",
64691
64661
  path: "running",
64692
64662
  sources: [
64693
64663
  {
@@ -65951,9 +65921,9 @@ var RecordTriggerSchema = _enum([
65951
65921
  */
65952
65922
  var RecordWindowSchema = object({
65953
65923
  trigger: RecordTriggerSchema,
65954
- day: number().int().min(0).max(6),
65955
- startMinute: number().int().min(0).max(1439),
65956
- endMinute: number().int().min(1).max(1440)
65924
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65925
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65926
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65957
65927
  });
65958
65928
  /** Status of one physical volume, as the camera itself describes it. */
65959
65929
  var OnboardStorageVolumeSchema = object({
@@ -66029,10 +65999,10 @@ var RecordingOnboardStatusSchema = object({
66029
65999
  /** Master "record to the card at all" switch. */
66030
66000
  enabled: boolean().nullable(),
66031
66001
  overwriteWhenFull: boolean().nullable(),
66032
- preRecordSec: number().nullable(),
66033
- postRecordSec: number().nullable(),
66002
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66003
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66034
66004
  /** Length of one recorded file, in minutes. */
66035
- segmentMinutes: number().nullable(),
66005
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66036
66006
  /** The primary track's weekly windows, flattened. */
66037
66007
  windows: array(RecordWindowSchema),
66038
66008
  /**
@@ -66048,7 +66018,7 @@ var RecordingOnboardStatusSchema = object({
66048
66018
  * provider reporting a non-zero count also reports the schedule as not
66049
66019
  * writable.
66050
66020
  */
66051
- unreadableWindows: number(),
66021
+ unreadableWindows: number().meta(readingMeta("count")),
66052
66022
  /**
66053
66023
  * The camera is scheduled to record and has NO usable storage.
66054
66024
  *
@@ -66893,7 +66863,7 @@ var CamStreamDescriptorSchema = object({
66893
66863
  url: string().optional(),
66894
66864
  codec: string().optional(),
66895
66865
  resolution: CamStreamResolutionSchema.optional(),
66896
- fps: number().positive().optional(),
66866
+ fps: number().positive().optional().meta(readingMeta("rate")),
66897
66867
  label: string().optional(),
66898
66868
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66899
66869
  deviceFeatures: array(string()).optional(),
@@ -66980,18 +66950,18 @@ var StreamProfileSchema = _enum([
66980
66950
  ]);
66981
66951
  /** The current encoding config of one stream profile. */
66982
66952
  var StreamProfileConfigSchema = object({
66983
- width: number(),
66984
- height: number(),
66953
+ width: number().meta(readingMeta("none")),
66954
+ height: number().meta(readingMeta("none")),
66985
66955
  codec: _enum(["h264", "h265"]),
66986
- framerate: number(),
66987
- bitrate: number(),
66956
+ framerate: number().meta(readingMeta("rate")),
66957
+ bitrate: number().meta(readingMeta("bitrate")),
66988
66958
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66989
66959
  encoderProfile: _enum([
66990
66960
  "high",
66991
66961
  "main",
66992
66962
  "baseline"
66993
66963
  ]).optional(),
66994
- gop: number().optional(),
66964
+ gop: number().optional().meta(readingMeta("none")),
66995
66965
  /**
66996
66966
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66997
66967
  *
@@ -67212,12 +67182,7 @@ var switchCapability = {
67212
67182
  label: "On",
67213
67183
  kind: "boolean"
67214
67184
  }
67215
- }] },
67216
- customize: {
67217
- version: 1,
67218
- advanced: "derive",
67219
- targets: []
67220
- }
67185
+ }] }
67221
67186
  };
67222
67187
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67223
67188
  function hasControlCharacter(value) {
@@ -67800,35 +67765,21 @@ var tamperCapability = {
67800
67765
  * entries with `device_class: temperature` and any future native
67801
67766
  * thermometer.
67802
67767
  *
67803
- * Unit normalisation: providers convert to Celsius before storing.
67804
- * The slice value is always Celsius so cross-cap aggregators
67805
- * (climate-control's `currentTemp`, energy analytics) can compose
67806
- * without per-source unit fixups. The canonical display unit (`°C`) is
67807
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67808
- * `sourceInfo`.
67768
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67769
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67809
67770
  *
67810
67771
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67811
67772
  * frozen feed (provider hung) distinct from a "temperature hasn't
67812
67773
  * changed" steady state.
67813
67774
  */
67814
67775
  var TemperatureSensorStatusSchema = object({
67815
- /** Current temperature in Celsius. */
67816
- celsius: number().meta({
67817
- unit: "°C",
67776
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67777
+ celsius: number().nullable().meta(readingMeta("temperature", {
67778
+ precision: 1,
67818
67779
  label: "Temperature"
67819
- }),
67780
+ })),
67820
67781
  /** Ms epoch when the slice was last updated (push or poll). */
67821
- lastFetchedAt: number(),
67822
- /** Live display unit from the upstream source (e.g. HA
67823
- * `attributes.unit_of_measurement`). The UI prefers this over the
67824
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67825
- * Absent → the UI falls back to the role's canonical unit. */
67826
- unit: string().optional(),
67827
- /** Suggested decimal places for numeric display.
67828
- * Populated live from the upstream source when provided (e.g. HA
67829
- * `attributes.suggested_display_precision`). Falls back to
67830
- * auto-formatting when absent. */
67831
- precision: number().int().min(0).max(10).optional()
67782
+ lastFetchedAt: number()
67832
67783
  });
67833
67784
  var temperatureSensorCapability = {
67834
67785
  name: "temperature-sensor",
@@ -67859,12 +67810,7 @@ var temperatureSensorCapability = {
67859
67810
  } } },
67860
67811
  /** Clock fields: written, but excluded from the compare that decides
67861
67812
  * whether persisting is worth a SQLite commit. */
67862
- volatileStateFields: ["lastFetchedAt"],
67863
- customize: {
67864
- version: 1,
67865
- advanced: "derive",
67866
- targets: []
67867
- }
67813
+ volatileStateFields: ["lastFetchedAt"]
67868
67814
  };
67869
67815
  /**
67870
67816
  * toast — system-scoped singleton capability that streams toast
@@ -68147,16 +68093,16 @@ var VacuumControlStatusSchema = object({
68147
68093
  ]),
68148
68094
  /** 0..100 battery percentage. Null when the device has no battery
68149
68095
  * reading. */
68150
- batteryLevel: number().min(0).max(100).nullable(),
68096
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68151
68097
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68152
68098
  * the vacuum has no speed control. */
68153
68099
  fanSpeed: string().nullable(),
68154
68100
  /** Speed tokens the hardware accepts — drives the UI selector. */
68155
68101
  availableFanSpeeds: array(string()),
68156
68102
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68157
- progressPercent: number().min(0).max(100).nullable(),
68103
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68158
68104
  /** Current error code (0 / null = no error). */
68159
- errorCode: number().nullable(),
68105
+ errorCode: number().nullable().meta(readingMeta("none")),
68160
68106
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68161
68107
  errorLabel: string().nullable(),
68162
68108
  /** Ms epoch when the slice was last updated. */
@@ -68234,7 +68180,7 @@ var ValveStatusSchema = object({
68234
68180
  ]),
68235
68181
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68236
68182
  * intermediate position surface. */
68237
- position: number().min(0).max(100).nullable(),
68183
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68238
68184
  /** Ms epoch when the slice was last updated. */
68239
68185
  lastChangedAt: number()
68240
68186
  });
@@ -68342,9 +68288,9 @@ var vibrationCapability = {
68342
68288
  */
68343
68289
  var WaterHeaterStatusSchema = object({
68344
68290
  /** Current measured temperature. Null when not reported. */
68345
- currentTemp: number().nullable(),
68291
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68346
68292
  /** Target temperature setpoint. Null when no setpoint surface. */
68347
- targetTemp: number().nullable(),
68293
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68348
68294
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68349
68295
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68350
68296
  * device reports an unknown state. */
@@ -68355,9 +68301,9 @@ var WaterHeaterStatusSchema = object({
68355
68301
  * has no away surface. */
68356
68302
  away: boolean().nullable(),
68357
68303
  /** HA `min_temp` attribute. Null when not reported. */
68358
- minTemp: number().nullable(),
68304
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68359
68305
  /** HA `max_temp` attribute. Null when not reported. */
68360
- maxTemp: number().nullable(),
68306
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68361
68307
  /** Ms epoch when the slice was last updated. */
68362
68308
  lastChangedAt: number()
68363
68309
  });
@@ -68442,25 +68388,18 @@ var waterHeaterCapability = {
68442
68388
  * Forecast deferred — current conditions only for v1.
68443
68389
  */
68444
68390
  var WeatherStatusSchema = object({
68445
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68446
- * when no condition has been reported yet. */
68391
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68447
68392
  condition: string().nullable(),
68448
- /** Current temperature in the reported unit. Null when not provided. */
68449
- temperature: number().nullable(),
68450
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68451
- temperatureUnit: string().nullable(),
68393
+ /** Current temperature in Celsius. Null when not provided or refused. */
68394
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68452
68395
  /** Relative humidity (0..100). Null when not provided. */
68453
- humidity: number().min(0).max(100).nullable(),
68454
- /** Barometric pressure in the reported unit. Null when not provided. */
68455
- pressure: number().nullable(),
68456
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68457
- pressureUnit: string().nullable(),
68458
- /** Wind speed in the reported unit. Null when not provided. */
68459
- windSpeed: number().nullable(),
68460
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68461
- windSpeedUnit: string().nullable(),
68396
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68397
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68398
+ pressure: number().nullable().meta(readingMeta("pressure")),
68399
+ /** Wind speed in metres per second. Null when not provided or refused. */
68400
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68462
68401
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68463
- windBearing: number().nullable(),
68402
+ windBearing: number().nullable().meta(readingMeta("angle")),
68464
68403
  /** Ms epoch when the slice was last updated. */
68465
68404
  lastFetchedAt: number()
68466
68405
  });
@@ -68488,12 +68427,12 @@ var weatherCapability = {
68488
68427
  */
68489
68428
  durability: "session",
68490
68429
  /**
68491
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68430
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68492
68431
  */
68493
68432
  history: { fields: {
68494
68433
  temperature: {
68495
68434
  kind: "gauge",
68496
- unitFrom: "temperatureUnit",
68435
+ unit: "°C",
68497
68436
  deadband: .2
68498
68437
  },
68499
68438
  humidity: {
@@ -68503,12 +68442,12 @@ var weatherCapability = {
68503
68442
  },
68504
68443
  pressure: {
68505
68444
  kind: "gauge",
68506
- unitFrom: "pressureUnit",
68445
+ unit: "hPa",
68507
68446
  deadband: .5
68508
68447
  },
68509
68448
  windSpeed: {
68510
68449
  kind: "gauge",
68511
- unitFrom: "windSpeedUnit",
68450
+ unit: "m/s",
68512
68451
  deadband: .5
68513
68452
  }
68514
68453
  } }
@@ -69409,6 +69348,389 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69409
69348
  kind: "mutation",
69410
69349
  auth: "admin"
69411
69350
  });
69351
+ /**
69352
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69353
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69354
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69355
+ * decimals). K derives from `°C = K − 273.15`.
69356
+ */
69357
+ var UNIT_TABLE = {
69358
+ "°C": {
69359
+ dimension: "temperature",
69360
+ factor: 1,
69361
+ offset: 0
69362
+ },
69363
+ "°F": {
69364
+ dimension: "temperature",
69365
+ factor: 5 / 9,
69366
+ offset: -160 / 9
69367
+ },
69368
+ K: {
69369
+ dimension: "temperature",
69370
+ factor: 1,
69371
+ offset: -273.15
69372
+ },
69373
+ hPa: {
69374
+ dimension: "pressure",
69375
+ factor: 1,
69376
+ offset: 0
69377
+ },
69378
+ kPa: {
69379
+ dimension: "pressure",
69380
+ factor: 10,
69381
+ offset: 0
69382
+ },
69383
+ Pa: {
69384
+ dimension: "pressure",
69385
+ factor: .01,
69386
+ offset: 0
69387
+ },
69388
+ mbar: {
69389
+ dimension: "pressure",
69390
+ factor: 1,
69391
+ offset: 0
69392
+ },
69393
+ bar: {
69394
+ dimension: "pressure",
69395
+ factor: 1e3,
69396
+ offset: 0
69397
+ },
69398
+ inHg: {
69399
+ dimension: "pressure",
69400
+ factor: 33.8639,
69401
+ offset: 0
69402
+ },
69403
+ mmHg: {
69404
+ dimension: "pressure",
69405
+ factor: 1.33322,
69406
+ offset: 0
69407
+ },
69408
+ psi: {
69409
+ dimension: "pressure",
69410
+ factor: 68.9476,
69411
+ offset: 0
69412
+ },
69413
+ "m/s": {
69414
+ dimension: "speed",
69415
+ factor: 1,
69416
+ offset: 0
69417
+ },
69418
+ "km/h": {
69419
+ dimension: "speed",
69420
+ factor: 1 / 3.6,
69421
+ offset: 0
69422
+ },
69423
+ mph: {
69424
+ dimension: "speed",
69425
+ factor: .44704,
69426
+ offset: 0
69427
+ },
69428
+ kn: {
69429
+ dimension: "speed",
69430
+ factor: .514444,
69431
+ offset: 0
69432
+ },
69433
+ "mm/h": {
69434
+ dimension: "precipitation-rate",
69435
+ factor: 1,
69436
+ offset: 0
69437
+ },
69438
+ "in/h": {
69439
+ dimension: "precipitation-rate",
69440
+ factor: 25.4,
69441
+ offset: 0
69442
+ },
69443
+ m: {
69444
+ dimension: "length",
69445
+ factor: 1,
69446
+ offset: 0
69447
+ },
69448
+ mm: {
69449
+ dimension: "length",
69450
+ factor: .001,
69451
+ offset: 0
69452
+ },
69453
+ cm: {
69454
+ dimension: "length",
69455
+ factor: .01,
69456
+ offset: 0
69457
+ },
69458
+ km: {
69459
+ dimension: "length",
69460
+ factor: 1e3,
69461
+ offset: 0
69462
+ },
69463
+ in: {
69464
+ dimension: "length",
69465
+ factor: .0254,
69466
+ offset: 0
69467
+ },
69468
+ ft: {
69469
+ dimension: "length",
69470
+ factor: .3048,
69471
+ offset: 0
69472
+ },
69473
+ mi: {
69474
+ dimension: "length",
69475
+ factor: 1609.344,
69476
+ offset: 0
69477
+ },
69478
+ "m²": {
69479
+ dimension: "area",
69480
+ factor: 1,
69481
+ offset: 0
69482
+ },
69483
+ lx: {
69484
+ dimension: "illuminance",
69485
+ factor: 1,
69486
+ offset: 0
69487
+ },
69488
+ "W/m²": {
69489
+ dimension: "irradiance",
69490
+ factor: 1,
69491
+ offset: 0
69492
+ },
69493
+ W: {
69494
+ dimension: "power",
69495
+ factor: 1,
69496
+ offset: 0
69497
+ },
69498
+ kW: {
69499
+ dimension: "power",
69500
+ factor: 1e3,
69501
+ offset: 0
69502
+ },
69503
+ VA: {
69504
+ dimension: "apparent-power",
69505
+ factor: 1,
69506
+ offset: 0
69507
+ },
69508
+ Wh: {
69509
+ dimension: "energy",
69510
+ factor: 1,
69511
+ offset: 0
69512
+ },
69513
+ kWh: {
69514
+ dimension: "energy",
69515
+ factor: 1e3,
69516
+ offset: 0
69517
+ },
69518
+ MWh: {
69519
+ dimension: "energy",
69520
+ factor: 1e6,
69521
+ offset: 0
69522
+ },
69523
+ V: {
69524
+ dimension: "voltage",
69525
+ factor: 1,
69526
+ offset: 0
69527
+ },
69528
+ mV: {
69529
+ dimension: "voltage",
69530
+ factor: .001,
69531
+ offset: 0
69532
+ },
69533
+ A: {
69534
+ dimension: "current",
69535
+ factor: 1,
69536
+ offset: 0
69537
+ },
69538
+ mA: {
69539
+ dimension: "current",
69540
+ factor: .001,
69541
+ offset: 0
69542
+ },
69543
+ ppm: {
69544
+ dimension: "concentration-volume",
69545
+ factor: 1,
69546
+ offset: 0
69547
+ },
69548
+ ppb: {
69549
+ dimension: "concentration-volume",
69550
+ factor: .001,
69551
+ offset: 0
69552
+ },
69553
+ "µg/m³": {
69554
+ dimension: "concentration-mass",
69555
+ factor: 1,
69556
+ offset: 0
69557
+ },
69558
+ "mg/m³": {
69559
+ dimension: "concentration-mass",
69560
+ factor: 1e3,
69561
+ offset: 0
69562
+ },
69563
+ kg: {
69564
+ dimension: "mass",
69565
+ factor: 1,
69566
+ offset: 0
69567
+ },
69568
+ g: {
69569
+ dimension: "mass",
69570
+ factor: .001,
69571
+ offset: 0
69572
+ },
69573
+ lb: {
69574
+ dimension: "mass",
69575
+ factor: .453592,
69576
+ offset: 0
69577
+ },
69578
+ oz: {
69579
+ dimension: "mass",
69580
+ factor: .0283495,
69581
+ offset: 0
69582
+ },
69583
+ "%": {
69584
+ dimension: "percentage",
69585
+ factor: 1,
69586
+ offset: 0
69587
+ },
69588
+ L: {
69589
+ dimension: "volume",
69590
+ factor: 1,
69591
+ offset: 0
69592
+ },
69593
+ mL: {
69594
+ dimension: "volume",
69595
+ factor: .001,
69596
+ offset: 0
69597
+ },
69598
+ gal: {
69599
+ dimension: "volume",
69600
+ factor: 3.785411784,
69601
+ offset: 0
69602
+ },
69603
+ s: {
69604
+ dimension: "duration",
69605
+ factor: 1,
69606
+ offset: 0
69607
+ },
69608
+ ms: {
69609
+ dimension: "duration",
69610
+ factor: .001,
69611
+ offset: 0
69612
+ },
69613
+ min: {
69614
+ dimension: "duration",
69615
+ factor: 60,
69616
+ offset: 0
69617
+ },
69618
+ h: {
69619
+ dimension: "duration",
69620
+ factor: 3600,
69621
+ offset: 0
69622
+ },
69623
+ d: {
69624
+ dimension: "duration",
69625
+ factor: 86400,
69626
+ offset: 0
69627
+ },
69628
+ B: {
69629
+ dimension: "information",
69630
+ factor: 1,
69631
+ offset: 0
69632
+ },
69633
+ KiB: {
69634
+ dimension: "information",
69635
+ factor: 1024,
69636
+ offset: 0
69637
+ },
69638
+ MiB: {
69639
+ dimension: "information",
69640
+ factor: 1024 ** 2,
69641
+ offset: 0
69642
+ },
69643
+ GiB: {
69644
+ dimension: "information",
69645
+ factor: 1024 ** 3,
69646
+ offset: 0
69647
+ },
69648
+ TiB: {
69649
+ dimension: "information",
69650
+ factor: 1024 ** 4,
69651
+ offset: 0
69652
+ },
69653
+ kB: {
69654
+ dimension: "information",
69655
+ factor: 1e3,
69656
+ offset: 0
69657
+ },
69658
+ MB: {
69659
+ dimension: "information",
69660
+ factor: 1e6,
69661
+ offset: 0
69662
+ },
69663
+ GB: {
69664
+ dimension: "information",
69665
+ factor: 1e9,
69666
+ offset: 0
69667
+ },
69668
+ "°": {
69669
+ dimension: "angle",
69670
+ factor: 1,
69671
+ offset: 0
69672
+ },
69673
+ dBFS: {
69674
+ dimension: "sound-level",
69675
+ factor: 1,
69676
+ offset: 0
69677
+ },
69678
+ dBm: {
69679
+ dimension: "signal-level",
69680
+ factor: 1,
69681
+ offset: 0
69682
+ },
69683
+ mired: {
69684
+ dimension: "colour-temperature",
69685
+ factor: 1,
69686
+ offset: 0
69687
+ },
69688
+ kbps: {
69689
+ dimension: "bitrate",
69690
+ factor: 1,
69691
+ offset: 0
69692
+ },
69693
+ Mbps: {
69694
+ dimension: "bitrate",
69695
+ factor: 1e3,
69696
+ offset: 0
69697
+ },
69698
+ fps: {
69699
+ dimension: "rate",
69700
+ factor: 1,
69701
+ offset: 0
69702
+ }
69703
+ };
69704
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69705
+ function specFor(unit) {
69706
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
69707
+ }
69708
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
69709
+ function unitDimension(unit) {
69710
+ return specFor(unit)?.dimension;
69711
+ }
69712
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
69713
+ function toCanonical(value, spec) {
69714
+ return value * spec.factor + spec.offset;
69715
+ }
69716
+ /** Canonical-unit value → the unit described by `spec`. */
69717
+ function fromCanonical(canonical, spec) {
69718
+ return (canonical - spec.offset) / spec.factor;
69719
+ }
69720
+ /**
69721
+ * Graceful variant for the render path: returns the converted value, or
69722
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
69723
+ * Never throws — a render caller falls back to the raw value + source unit.
69724
+ */
69725
+ function tryConvertUnit(value, from, to) {
69726
+ if (from === to) return value;
69727
+ const fromSpec = specFor(from);
69728
+ if (!fromSpec) return void 0;
69729
+ const toSpec = specFor(to);
69730
+ if (!toSpec) return void 0;
69731
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
69732
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
69733
+ }
69412
69734
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69413
69735
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
69414
69736
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -84078,7 +84400,6 @@ var SENSOR_CAP_VOCABULARY = {
84078
84400
  field: "value",
84079
84401
  quantity: t("the reading", "il valore"),
84080
84402
  label: t("Reading", "Valore"),
84081
- unitField: "unit",
84082
84403
  icon: "gauge"
84083
84404
  }]
84084
84405
  },
@@ -84214,7 +84535,7 @@ var SENSOR_CAP_VOCABULARY = {
84214
84535
  field: "temperature",
84215
84536
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84216
84537
  label: t("Temperature", "Temperatura"),
84217
- unitField: "temperatureUnit",
84538
+ unit: "°C",
84218
84539
  icon: "thermometer"
84219
84540
  },
84220
84541
  {
@@ -84230,7 +84551,7 @@ var SENSOR_CAP_VOCABULARY = {
84230
84551
  field: "windSpeed",
84231
84552
  quantity: t("the wind speed", "la velocità del vento"),
84232
84553
  label: t("Wind", "Vento"),
84233
- unitField: "windSpeedUnit",
84554
+ unit: "m/s",
84234
84555
  icon: "wind"
84235
84556
  },
84236
84557
  {
@@ -84238,7 +84559,7 @@ var SENSOR_CAP_VOCABULARY = {
84238
84559
  field: "pressure",
84239
84560
  quantity: t("the air pressure", "la pressione atmosferica"),
84240
84561
  label: t("Pressure", "Pressione"),
84241
- unitField: "pressureUnit",
84562
+ unit: "hPa",
84242
84563
  icon: "gauge"
84243
84564
  }
84244
84565
  ]
@@ -88604,6 +88925,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88604
88925
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88605
88926
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
88606
88927
  /**
88928
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
88929
+ * An unknown unit or a unit from another dimension is a refusal: the caller
88930
+ * logs it and publishes null (D393). This function never guesses.
88931
+ */
88932
+ var DIMENSION_QUANTITY = {
88933
+ temperature: "temperature",
88934
+ pressure: "pressure",
88935
+ speed: "speed",
88936
+ "precipitation-rate": "precipitation-rate",
88937
+ length: "length",
88938
+ area: "area",
88939
+ illuminance: "illuminance",
88940
+ irradiance: "irradiance",
88941
+ power: "power",
88942
+ "apparent-power": "apparent-power",
88943
+ energy: "energy",
88944
+ voltage: "voltage",
88945
+ current: "current",
88946
+ "concentration-volume": "concentration-volume",
88947
+ "concentration-mass": "concentration-mass",
88948
+ mass: "mass",
88949
+ percentage: "percentage",
88950
+ volume: "volume",
88951
+ duration: "duration",
88952
+ information: "information",
88953
+ angle: "angle",
88954
+ "sound-level": "sound-level",
88955
+ "signal-level": "signal-level",
88956
+ "colour-temperature": "colour-temperature",
88957
+ bitrate: "bitrate",
88958
+ rate: "rate"
88959
+ };
88960
+ /**
88961
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
88962
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
88963
+ * precipitation is chosen by the caller (a device class), not by the symbol.
88964
+ */
88965
+ function quantityFromUnit(rawUnit) {
88966
+ const unit = normalizeUnit(rawUnit);
88967
+ if (unit === void 0) return void 0;
88968
+ if (unit === "ppb") return "concentration-ppb";
88969
+ if (unit === "ms") return "duration-ms";
88970
+ const dimension = unitDimension(unit);
88971
+ if (dimension === void 0) return void 0;
88972
+ return DIMENSION_QUANTITY[dimension];
88973
+ }
88974
+ function ingestReading(input) {
88975
+ if (!Number.isFinite(input.value)) return {
88976
+ ok: false,
88977
+ reason: "non-finite"
88978
+ };
88979
+ if (input.quantity === "device") return {
88980
+ ok: false,
88981
+ reason: "unknown-unit"
88982
+ };
88983
+ const raw = input.rawUnit?.trim();
88984
+ if (raw === void 0 || raw.length === 0) return {
88985
+ ok: true,
88986
+ value: input.value
88987
+ };
88988
+ const unit = normalizeUnit(raw);
88989
+ if (unit === void 0) return {
88990
+ ok: false,
88991
+ reason: "unknown-unit"
88992
+ };
88993
+ const base = baseUnitOf(input.quantity);
88994
+ if (base === void 0) {
88995
+ if (unitDimension(unit) !== void 0) return {
88996
+ ok: false,
88997
+ reason: "cross-dimension"
88998
+ };
88999
+ return {
89000
+ ok: false,
89001
+ reason: "unknown-unit"
89002
+ };
89003
+ }
89004
+ const converted = tryConvertUnit(input.value, unit, base);
89005
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
89006
+ ok: false,
89007
+ reason: "unknown-unit"
89008
+ } : {
89009
+ ok: false,
89010
+ reason: "cross-dimension"
89011
+ };
89012
+ return {
89013
+ ok: true,
89014
+ value: converted
89015
+ };
89016
+ }
89017
+ /**
88607
89018
  * Pure fuzzy matcher for adoption location import. Normalized
88608
89019
  * case-insensitive Levenshtein similarity, used to reuse an existing
88609
89020
  * CamStack location when an imported source location (e.g. an HA area) is
@@ -114238,11 +114649,7 @@ var hmDiagnosticSchema = hmChildSchema.extend({
114238
114649
  sourceParams: array(string()).min(1)
114239
114650
  });
114240
114651
  /** Cold-start slices keyed by cap. */
114241
- var COLD_NUMERIC = (unit) => unit !== void 0 ? {
114242
- value: 0,
114243
- unit,
114244
- lastFetchedAt: 0
114245
- } : {
114652
+ var COLD_NUMERIC = {
114246
114653
  value: 0,
114247
114654
  lastFetchedAt: 0
114248
114655
  };
@@ -114286,7 +114693,7 @@ var HmDiagnosticDevice = class extends HmChildDevice {
114286
114693
  this.runtimeState.setCapState("temperature-sensor", COLD_TEMP);
114287
114694
  return;
114288
114695
  default: {
114289
- const cold = COLD_NUMERIC(this.unit);
114696
+ const cold = COLD_NUMERIC;
114290
114697
  this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState("numeric-sensor") ?? cold });
114291
114698
  this.runtimeState.setCapState("numeric-sensor", cold);
114292
114699
  return;
@@ -114311,22 +114718,55 @@ var HmDiagnosticDevice = class extends HmChildDevice {
114311
114718
  return;
114312
114719
  }
114313
114720
  if (typeof value !== "number") return;
114721
+ const stored = this.toBase(value);
114722
+ if (stored === null) return;
114314
114723
  if (this.capName === "temperature-sensor") {
114315
114724
  this.runtimeState.setCapState("temperature-sensor", {
114316
- celsius: value,
114725
+ celsius: stored,
114317
114726
  lastFetchedAt: now
114318
114727
  });
114319
114728
  return;
114320
114729
  }
114321
- const next = this.unit !== void 0 ? {
114322
- value,
114323
- unit: this.unit,
114730
+ this.runtimeState.setCapState("numeric-sensor", {
114731
+ value: stored,
114324
114732
  lastFetchedAt: now
114325
- } : {
114733
+ });
114734
+ }
114735
+ /** Upstream diagnostic number → the quantity's base. Unknown units publish nothing. */
114736
+ toBase(value) {
114737
+ const quantity = this.readingQuantity();
114738
+ if (quantity === null) {
114739
+ this.warnRefusal("unknown-unit");
114740
+ return null;
114741
+ }
114742
+ const result = ingestReading({
114326
114743
  value,
114327
- lastFetchedAt: now
114328
- };
114329
- this.runtimeState.setCapState("numeric-sensor", next);
114744
+ rawUnit: this.unit,
114745
+ quantity
114746
+ });
114747
+ if (!result.ok) {
114748
+ this.warnRefusal(result.reason);
114749
+ return null;
114750
+ }
114751
+ if (this.capName === "numeric-sensor" && this.sourceInfo.quantity !== quantity) this.updateSourceInfo({ quantity });
114752
+ return result.value;
114753
+ }
114754
+ readingQuantity() {
114755
+ if (this.capName === "temperature-sensor") return "temperature";
114756
+ if (this.unit === void 0) return "none";
114757
+ const learned = quantityFromUnit(this.unit);
114758
+ if (learned === void 0 || learned === "device") return null;
114759
+ return learned;
114760
+ }
114761
+ warnRefusal(reason) {
114762
+ this.ctx.logger.warn("numeric reading refused", {
114763
+ tags: { deviceId: this.id },
114764
+ meta: {
114765
+ quantity: this.readingQuantity() ?? "device",
114766
+ unit: this.unit ?? "",
114767
+ reason
114768
+ }
114769
+ });
114330
114770
  }
114331
114771
  };
114332
114772
  //#endregion