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