@camstack/addon-provider-homeassistant 1.2.625 → 1.2.626

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.
@@ -5567,7 +5567,7 @@ var ZodIssueCode = {
5567
5567
  var ZodFirstPartyTypeKind;
5568
5568
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5569
5569
  //#endregion
5570
- //#region ../types/dist/sleep-BErAxhul.mjs
5570
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5571
5571
  /**
5572
5572
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5573
5573
  * window to float samples (D455).
@@ -8309,6 +8309,102 @@ function defineEntryKind(name, spec) {
8309
8309
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8310
8310
  return defined;
8311
8311
  }
8312
+ var READING_QUANTITIES = [
8313
+ "temperature",
8314
+ "pressure",
8315
+ "speed",
8316
+ "precipitation",
8317
+ "precipitation-rate",
8318
+ "length",
8319
+ "area",
8320
+ "illuminance",
8321
+ "irradiance",
8322
+ "power",
8323
+ "energy",
8324
+ "voltage",
8325
+ "current",
8326
+ "apparent-power",
8327
+ "humidity",
8328
+ "percentage",
8329
+ "concentration-volume",
8330
+ "concentration-mass",
8331
+ "concentration-ppb",
8332
+ "volume",
8333
+ "duration",
8334
+ "duration-ms",
8335
+ "information",
8336
+ "sound-level",
8337
+ "signal-level",
8338
+ "colour-temperature",
8339
+ "angle",
8340
+ "bitrate",
8341
+ "rate",
8342
+ "mass",
8343
+ "index",
8344
+ "ratio",
8345
+ "count",
8346
+ "none",
8347
+ "device"
8348
+ ];
8349
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8350
+ var QUANTITY_SET = new Set(READING_QUANTITIES);
8351
+ function isReadingQuantity(value) {
8352
+ return typeof value === "string" && QUANTITY_SET.has(value);
8353
+ }
8354
+ /**
8355
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8356
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8357
+ */
8358
+ var QUANTITY_BASE = {
8359
+ temperature: "°C",
8360
+ pressure: "hPa",
8361
+ speed: "m/s",
8362
+ precipitation: "mm",
8363
+ "precipitation-rate": "mm/h",
8364
+ length: "m",
8365
+ area: "m²",
8366
+ illuminance: "lx",
8367
+ irradiance: "W/m²",
8368
+ power: "W",
8369
+ energy: "kWh",
8370
+ voltage: "V",
8371
+ current: "A",
8372
+ "apparent-power": "VA",
8373
+ humidity: "%",
8374
+ percentage: "%",
8375
+ "concentration-volume": "ppm",
8376
+ "concentration-mass": "µg/m³",
8377
+ "concentration-ppb": "ppb",
8378
+ volume: "L",
8379
+ duration: "s",
8380
+ "duration-ms": "ms",
8381
+ information: "B",
8382
+ "sound-level": "dBFS",
8383
+ "signal-level": "dBm",
8384
+ "colour-temperature": "mired",
8385
+ angle: "°",
8386
+ bitrate: "kbps",
8387
+ rate: "fps",
8388
+ mass: "kg",
8389
+ index: "none",
8390
+ ratio: "none",
8391
+ count: "none",
8392
+ none: "none",
8393
+ device: "device"
8394
+ };
8395
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8396
+ function baseUnitOf(quantity) {
8397
+ const symbol = QUANTITY_BASE[quantity];
8398
+ if (symbol === "none" || symbol === "device") return void 0;
8399
+ return symbol;
8400
+ }
8401
+ function readingMeta(quantity, extras) {
8402
+ return {
8403
+ quantity,
8404
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8405
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8406
+ };
8407
+ }
8312
8408
  /** Shared Zod schemas used across streaming capabilities. */
8313
8409
  var CamProfileSchema = _enum([
8314
8410
  "high",
@@ -8336,8 +8432,8 @@ var CamStreamKindSchema = _enum([
8336
8432
  "derived"
8337
8433
  ]);
8338
8434
  var CamStreamResolutionSchema = object({
8339
- width: number().int().positive(),
8340
- height: number().int().positive()
8435
+ width: number().int().positive().meta(readingMeta("none")),
8436
+ height: number().int().positive().meta(readingMeta("none"))
8341
8437
  });
8342
8438
  var CameraStreamSchema = object({
8343
8439
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9043,14 +9139,6 @@ Object.freeze({
9043
9139
  {
9044
9140
  "path": "aqi",
9045
9141
  "valueType": "number"
9046
- },
9047
- {
9048
- "path": "unit",
9049
- "valueType": "string"
9050
- },
9051
- {
9052
- "path": "precision",
9053
- "valueType": "number"
9054
9142
  }
9055
9143
  ],
9056
9144
  "alarm-panel": [
@@ -9086,20 +9174,10 @@ Object.freeze({
9086
9174
  "valueType": "boolean"
9087
9175
  }
9088
9176
  ],
9089
- "ambient-light-sensor": [
9090
- {
9091
- "path": "lux",
9092
- "valueType": "number"
9093
- },
9094
- {
9095
- "path": "unit",
9096
- "valueType": "string"
9097
- },
9098
- {
9099
- "path": "precision",
9100
- "valueType": "number"
9101
- }
9102
- ],
9177
+ "ambient-light-sensor": [{
9178
+ "path": "lux",
9179
+ "valueType": "number"
9180
+ }],
9103
9181
  "audio-metrics": [
9104
9182
  {
9105
9183
  "path": "ts",
@@ -9179,6 +9257,10 @@ Object.freeze({
9179
9257
  "path": "lastUpdated",
9180
9258
  "valueType": "number"
9181
9259
  },
9260
+ {
9261
+ "path": "volts",
9262
+ "valueType": "number"
9263
+ },
9182
9264
  {
9183
9265
  "path": "lastContactAt",
9184
9266
  "valueType": "number"
@@ -9421,10 +9503,6 @@ Object.freeze({
9421
9503
  "datetime"
9422
9504
  ]
9423
9505
  },
9424
- {
9425
- "path": "unit",
9426
- "valueType": "string"
9427
- },
9428
9506
  {
9429
9507
  "path": "min",
9430
9508
  "valueType": "number"
@@ -9436,19 +9514,6 @@ Object.freeze({
9436
9514
  {
9437
9515
  "path": "step",
9438
9516
  "valueType": "number"
9439
- },
9440
- {
9441
- "path": "precision",
9442
- "valueType": "number"
9443
- },
9444
- {
9445
- "path": "format",
9446
- "valueType": "enum",
9447
- "enumValues": [
9448
- "date",
9449
- "time",
9450
- "datetime"
9451
- ]
9452
9517
  }
9453
9518
  ],
9454
9519
  "cover": [
@@ -9689,13 +9754,8 @@ Object.freeze({
9689
9754
  "path": "value",
9690
9755
  "valueType": "string"
9691
9756
  }, {
9692
- "path": "format",
9693
- "valueType": "enum",
9694
- "enumValues": [
9695
- "date",
9696
- "time",
9697
- "datetime"
9698
- ]
9757
+ "path": "timestamp",
9758
+ "valueType": "boolean"
9699
9759
  }],
9700
9760
  "event-emitter": [
9701
9761
  {
@@ -9802,20 +9862,10 @@ Object.freeze({
9802
9862
  "valueType": "number"
9803
9863
  }
9804
9864
  ],
9805
- "humidity-sensor": [
9806
- {
9807
- "path": "percent",
9808
- "valueType": "number"
9809
- },
9810
- {
9811
- "path": "unit",
9812
- "valueType": "string"
9813
- },
9814
- {
9815
- "path": "precision",
9816
- "valueType": "number"
9817
- }
9818
- ],
9865
+ "humidity-sensor": [{
9866
+ "path": "percent",
9867
+ "valueType": "number"
9868
+ }],
9819
9869
  "image": [{
9820
9870
  "path": "url",
9821
9871
  "valueType": "string"
@@ -10154,20 +10204,10 @@ Object.freeze({
10154
10204
  "valueType": "boolean"
10155
10205
  }
10156
10206
  ],
10157
- "numeric-sensor": [
10158
- {
10159
- "path": "value",
10160
- "valueType": "number"
10161
- },
10162
- {
10163
- "path": "unit",
10164
- "valueType": "string"
10165
- },
10166
- {
10167
- "path": "precision",
10168
- "valueType": "number"
10169
- }
10170
- ],
10207
+ "numeric-sensor": [{
10208
+ "path": "value",
10209
+ "valueType": "number"
10210
+ }],
10171
10211
  "pet-feeder": [
10172
10212
  {
10173
10213
  "path": "foodLevel",
@@ -10259,14 +10299,6 @@ Object.freeze({
10259
10299
  {
10260
10300
  "path": "powerFactor",
10261
10301
  "valueType": "number"
10262
- },
10263
- {
10264
- "path": "unit",
10265
- "valueType": "string"
10266
- },
10267
- {
10268
- "path": "precision",
10269
- "valueType": "number"
10270
10302
  }
10271
10303
  ],
10272
10304
  "presence": [
@@ -10295,20 +10327,10 @@ Object.freeze({
10295
10327
  "valueType": "number"
10296
10328
  }
10297
10329
  ],
10298
- "pressure-sensor": [
10299
- {
10300
- "path": "hpa",
10301
- "valueType": "number"
10302
- },
10303
- {
10304
- "path": "unit",
10305
- "valueType": "string"
10306
- },
10307
- {
10308
- "path": "precision",
10309
- "valueType": "number"
10310
- }
10311
- ],
10330
+ "pressure-sensor": [{
10331
+ "path": "hpa",
10332
+ "valueType": "number"
10333
+ }],
10312
10334
  "privacy-mask": [{
10313
10335
  "path": "enabled",
10314
10336
  "valueType": "boolean"
@@ -10619,20 +10641,10 @@ Object.freeze({
10619
10641
  "path": "tampered",
10620
10642
  "valueType": "boolean"
10621
10643
  }],
10622
- "temperature-sensor": [
10623
- {
10624
- "path": "celsius",
10625
- "valueType": "number"
10626
- },
10627
- {
10628
- "path": "unit",
10629
- "valueType": "string"
10630
- },
10631
- {
10632
- "path": "precision",
10633
- "valueType": "number"
10634
- }
10635
- ],
10644
+ "temperature-sensor": [{
10645
+ "path": "celsius",
10646
+ "valueType": "number"
10647
+ }],
10636
10648
  "update": [
10637
10649
  {
10638
10650
  "path": "currentVersion",
@@ -10743,10 +10755,6 @@ Object.freeze({
10743
10755
  "path": "temperature",
10744
10756
  "valueType": "number"
10745
10757
  },
10746
- {
10747
- "path": "temperatureUnit",
10748
- "valueType": "string"
10749
- },
10750
10758
  {
10751
10759
  "path": "humidity",
10752
10760
  "valueType": "number"
@@ -10755,18 +10763,10 @@ Object.freeze({
10755
10763
  "path": "pressure",
10756
10764
  "valueType": "number"
10757
10765
  },
10758
- {
10759
- "path": "pressureUnit",
10760
- "valueType": "string"
10761
- },
10762
10766
  {
10763
10767
  "path": "windSpeed",
10764
10768
  "valueType": "number"
10765
10769
  },
10766
- {
10767
- "path": "windSpeedUnit",
10768
- "valueType": "string"
10769
- },
10770
10770
  {
10771
10771
  "path": "windBearing",
10772
10772
  "valueType": "number"
@@ -18442,7 +18442,8 @@ var CompositionProblemSchema = object({
18442
18442
  "item-field-reserved",
18443
18443
  "target-missing",
18444
18444
  "target-unreadable",
18445
- "not-replaceable"
18445
+ "not-replaceable",
18446
+ "native-capability-replace"
18446
18447
  ]),
18447
18448
  path: string(),
18448
18449
  message: string()
@@ -18500,9 +18501,11 @@ var CompositionFieldPlanSchema = object({
18500
18501
  reads: array(CompositionFieldReadSchema)
18501
18502
  });
18502
18503
  /**
18503
- * What a feature does to its target (D663): `new` builds a capability on a new
18504
- * device; on an existing device it `add`s a capability the device lacks, or
18505
- * `replace`s named fields of one it has natively.
18504
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18505
+ * new device; on an existing device it `add`s a capability the device lacks.
18506
+ * Replacing a capability the device already serves is refused
18507
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18508
+ * wire for a claim the hub already holds.
18506
18509
  */
18507
18510
  var CompositionFeatureModeSchema = _enum([
18508
18511
  "new",
@@ -20535,12 +20538,12 @@ function parseAdminBase(baseUrl) {
20535
20538
  * providers detect renames by matching `uniqueId` and patch `id`
20536
20539
  * in-place so dispatch keeps working.
20537
20540
  *
20538
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20539
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20540
- * state push and are the single source of truth. The `device_class` tag is
20541
- * an HA-specific input that is consumed at the discovery boundary
20542
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20543
- * stored after that point.
20541
+ * **Display unit and precision are NOT stored here.** They are a read-time
20542
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20543
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20544
+ * neither is repeated on each sample. The `device_class` tag is an
20545
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20546
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20544
20547
  *
20545
20548
  * The `raw` escape hatch carries provider-specific attributes that don't
20546
20549
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20554,6 +20557,14 @@ var SourceInfoSchema = object({
20554
20557
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20555
20558
  * Used to detect rename when `id` changes. */
20556
20559
  uniqueId: string().optional(),
20560
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20561
+ quantity: ReadingQuantitySchema.optional(),
20562
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20563
+ dateTimeShape: _enum([
20564
+ "date",
20565
+ "time",
20566
+ "datetime"
20567
+ ]).optional(),
20557
20568
  /** Provider-specific extras that don't fit the structured slots.
20558
20569
  * Whitelist what goes here per provider — do NOT dump entire HA
20559
20570
  * attribute blobs (would explode the meta JSON column). */
@@ -20638,9 +20649,38 @@ function normalizeUnit(raw) {
20638
20649
  if (lower === "mph") return "mph";
20639
20650
  if (lower === "m/s") return "m/s";
20640
20651
  if (lower === "m") return "m";
20652
+ if (lower === "m²" || lower === "m2") return "m²";
20653
+ if (lower === "mm") return "mm";
20654
+ if (lower === "cm") return "cm";
20641
20655
  if (lower === "km") return "km";
20656
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20657
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20642
20658
  if (lower === "kg") return "kg";
20643
20659
  if (lower === "g") return "g";
20660
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20661
+ if (lower === "ml") return "mL";
20662
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20663
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20664
+ if (lower === "ms") return "ms";
20665
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20666
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20667
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20668
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20669
+ if (lower === "kib") return "KiB";
20670
+ if (lower === "mib") return "MiB";
20671
+ if (lower === "gib") return "GiB";
20672
+ if (lower === "tib") return "TiB";
20673
+ if (lower === "kb") return "kB";
20674
+ if (lower === "mb") return "MB";
20675
+ if (lower === "gb") return "GB";
20676
+ if (lower === "dbm") return "dBm";
20677
+ if (lower === "dbfs") return "dBFS";
20678
+ if (lower === "inhg") return "inHg";
20679
+ if (lower === "mm/h") return "mm/h";
20680
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20681
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20682
+ if (lower === "kbps") return "kbps";
20683
+ if (lower === "fps") return "fps";
20644
20684
  return trimmed;
20645
20685
  }
20646
20686
  /**
@@ -41745,7 +41785,8 @@ method(object({
41745
41785
  "enum"
41746
41786
  ]),
41747
41787
  enumValues: array(string()).optional(),
41748
- item: boolean().optional()
41788
+ item: boolean().optional(),
41789
+ quantity: ReadingQuantitySchema.optional()
41749
41790
  })).readonly(),
41750
41791
  itemArray: object({
41751
41792
  path: string(),
@@ -45611,8 +45652,8 @@ var MaskPolygonShapeSchema = object({
45611
45652
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45612
45653
  var MaskGridShapeSchema = object({
45613
45654
  kind: literal("grid"),
45614
- gridWidth: number(),
45615
- gridHeight: number(),
45655
+ gridWidth: number().meta(readingMeta("none")),
45656
+ gridHeight: number().meta(readingMeta("none")),
45616
45657
  cells: array(boolean())
45617
45658
  });
45618
45659
  discriminatedUnion("kind", [
@@ -57180,27 +57221,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57180
57221
  */
57181
57222
  var AirQualitySensorStatusSchema = object({
57182
57223
  /** Carbon dioxide concentration in ppm. */
57183
- co2Ppm: number().min(0).optional(),
57224
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57184
57225
  /** Total volatile organic compounds in ppb. */
57185
- vocPpb: number().min(0).optional(),
57226
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57186
57227
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57187
- pm25: number().min(0).optional(),
57228
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57188
57229
  /** Particulate matter ≤ 10 μm in µg/m³. */
57189
- pm10: number().min(0).optional(),
57230
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57190
57231
  /** Composite AQI value (typically 0..500). */
57191
- aqi: number().optional(),
57232
+ aqi: number().optional().meta(readingMeta("index")),
57192
57233
  /** Ms epoch when the slice was last updated. */
57193
- lastFetchedAt: number(),
57194
- /** Live display unit of the single metric this slice carries (e.g. HA
57195
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57196
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57197
- * per slice is unambiguous. */
57198
- unit: string().optional(),
57199
- /** Suggested decimal places for numeric display.
57200
- * Populated live from the upstream source when provided (e.g. HA
57201
- * `attributes.suggested_display_precision`). Falls back to
57202
- * auto-formatting when absent. */
57203
- precision: number().int().min(0).max(10).optional()
57234
+ lastFetchedAt: number()
57204
57235
  });
57205
57236
  var airQualitySensorCapability = {
57206
57237
  name: "air-quality-sensor",
@@ -57237,12 +57268,12 @@ var airQualitySensorCapability = {
57237
57268
  },
57238
57269
  pm25: {
57239
57270
  kind: "gauge",
57240
- unit: "ug/m3",
57271
+ unit: "µg/m³",
57241
57272
  deadband: 1
57242
57273
  },
57243
57274
  pm10: {
57244
57275
  kind: "gauge",
57245
- unit: "ug/m3",
57276
+ unit: "µg/m³",
57246
57277
  deadband: 1
57247
57278
  },
57248
57279
  aqi: {
@@ -57260,22 +57291,13 @@ var airQualitySensorCapability = {
57260
57291
  * entries with `device_class: illuminance`.
57261
57292
  */
57262
57293
  var AmbientLightSensorStatusSchema = object({
57263
- /** Current illuminance in lux (lx). */
57264
- lux: number().min(0).meta({
57265
- unit: "lx",
57294
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57295
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57296
+ precision: 0,
57266
57297
  label: "Illuminance"
57267
- }),
57298
+ })),
57268
57299
  /** Ms epoch when the slice was last updated. */
57269
- lastFetchedAt: number(),
57270
- /** Live display unit from the upstream source (e.g. HA
57271
- * `attributes.unit_of_measurement`). The UI prefers this over the
57272
- * role's canonical unit. Absent → fall back to the canonical unit. */
57273
- unit: string().optional(),
57274
- /** Suggested decimal places for numeric display.
57275
- * Populated live from the upstream source when provided (e.g. HA
57276
- * `attributes.suggested_display_precision`). Falls back to
57277
- * auto-formatting when absent. */
57278
- precision: number().int().min(0).max(10).optional()
57300
+ lastFetchedAt: number()
57279
57301
  });
57280
57302
  var ambientLightSensorCapability = {
57281
57303
  name: "ambient-light-sensor",
@@ -57307,12 +57329,7 @@ var ambientLightSensorCapability = {
57307
57329
  } } },
57308
57330
  /** Clock fields: written, but excluded from the compare that decides
57309
57331
  * whether persisting is worth a SQLite commit. */
57310
- volatileStateFields: ["lastFetchedAt"],
57311
- customize: {
57312
- version: 1,
57313
- advanced: "derive",
57314
- targets: []
57315
- }
57332
+ volatileStateFields: ["lastFetchedAt"]
57316
57333
  };
57317
57334
  /**
57318
57335
  * Per-class audio metrics aggregated over a sliding window.
@@ -57539,10 +57556,7 @@ var BatteryStatusSchema = object({
57539
57556
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57540
57557
  * declines to band a non-finite reading, which is the correct shape.
57541
57558
  */
57542
- percentage: number().min(0).max(100).nullable().meta({
57543
- unit: "%",
57544
- label: "Charge"
57545
- }),
57559
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57546
57560
  /**
57547
57561
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57548
57562
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57569,7 +57583,14 @@ var BatteryStatusSchema = object({
57569
57583
  */
57570
57584
  sleeping: boolean().meta({ label: "Sleeping" }),
57571
57585
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57572
- lastUpdated: number(),
57586
+ lastUpdated: number().meta(readingMeta("none")),
57587
+ /**
57588
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57589
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57590
+ * that only report charge. A second reading on this cap, not a second
57591
+ * `numeric-sensor` on the same device.
57592
+ */
57593
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57573
57594
  /**
57574
57595
  * Ms epoch of the last time the device PROVED it was reachable — a
57575
57596
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57621,7 +57642,6 @@ var BATTERY_DEVICE_TYPES = [
57621
57642
  /** Generic Customize target for the percentage reading (D936). */
57622
57643
  var batteryCustomize = {
57623
57644
  version: 1,
57624
- advanced: "derive",
57625
57645
  targets: [{
57626
57646
  id: "percentage",
57627
57647
  cardinality: "scalar",
@@ -57630,7 +57650,7 @@ var batteryCustomize = {
57630
57650
  stateField: "percentage"
57631
57651
  },
57632
57652
  deviceTypes: BATTERY_DEVICE_TYPES,
57633
- apply: "add-or-replace",
57653
+ apply: "add",
57634
57654
  path: "percentage",
57635
57655
  sources: [{
57636
57656
  kind: "field",
@@ -57751,10 +57771,16 @@ onStatusChanged: { data: object({
57751
57771
  /**
57752
57772
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57753
57773
  */
57754
- history: { fields: { percentage: {
57755
- kind: "gauge",
57756
- unit: "%"
57757
- } } },
57774
+ history: { fields: {
57775
+ percentage: {
57776
+ kind: "gauge",
57777
+ unit: "%"
57778
+ },
57779
+ volts: {
57780
+ kind: "gauge",
57781
+ unit: "V"
57782
+ }
57783
+ } },
57758
57784
  customize: batteryCustomize,
57759
57785
  /** Clock fields: written, but excluded from the compare that decides
57760
57786
  * whether persisting is worth a SQLite commit. */
@@ -57801,12 +57827,7 @@ var binaryCapability = {
57801
57827
  history: { fields: { on: { kind: "boolean" } } },
57802
57828
  /** Clock fields: written, but excluded from the compare that decides
57803
57829
  * whether persisting is worth a SQLite commit. */
57804
- volatileStateFields: ["lastChangedAt"],
57805
- customize: {
57806
- version: 1,
57807
- advanced: "derive",
57808
- targets: []
57809
- }
57830
+ volatileStateFields: ["lastChangedAt"]
57810
57831
  };
57811
57832
  /**
57812
57833
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57822,7 +57843,7 @@ var binaryCapability = {
57822
57843
  */
57823
57844
  var BrightnessStatusSchema = object({
57824
57845
  /** Current level as 0..100 inclusive. Firmware-reported. */
57825
- percentage: number().min(0).max(100),
57846
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57826
57847
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57827
57848
  lastChangedAt: number()
57828
57849
  });
@@ -58303,21 +58324,21 @@ var ClimateControlStatusSchema = object({
58303
58324
  /** Single setpoint in Celsius. Used by single-target modes
58304
58325
  * (heat / cool / dry). Null when the device is in a range mode
58305
58326
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58306
- target: number().nullable(),
58327
+ target: number().nullable().meta(readingMeta("temperature")),
58307
58328
  /** Upper bound of the dual setpoint in Celsius. Populated only
58308
58329
  * in `heat_cool` mode. */
58309
- targetHigh: number().nullable(),
58330
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58310
58331
  /** Lower bound of the dual setpoint in Celsius. Populated only
58311
58332
  * in `heat_cool` mode. */
58312
- targetLow: number().nullable(),
58333
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58313
58334
  /** Target relative humidity (0..100). Null when the device has no
58314
58335
  * humidity control. */
58315
- targetHumidity: number().min(0).max(100).nullable(),
58336
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58316
58337
  /** Current measured temperature in Celsius. Read-only — pushed by
58317
58338
  * the device's internal sensor. */
58318
- currentTemp: number().nullable(),
58339
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58319
58340
  /** Current measured relative humidity (0..100). Read-only. */
58320
- currentHumidity: number().min(0).max(100).nullable(),
58341
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58321
58342
  /** Vertical louver swing on/off. Null when the device has no vertical
58322
58343
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58323
58344
  swingVertical: boolean().nullable(),
@@ -58325,11 +58346,11 @@ var ClimateControlStatusSchema = object({
58325
58346
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58326
58347
  swingHorizontal: boolean().nullable(),
58327
58348
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58328
- minTemp: number().nullable().optional(),
58349
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58329
58350
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58330
- maxTemp: number().nullable().optional(),
58351
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58331
58352
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58332
- tempStep: number().positive().nullable().optional(),
58353
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58333
58354
  /** Ms epoch when the slice was last updated (push or command). */
58334
58355
  lastFetchedAt: number()
58335
58356
  });
@@ -58471,17 +58492,17 @@ var climateControlCapability = {
58471
58492
  * modes to expose in the picker without a slice fetch.
58472
58493
  */
58473
58494
  var RgbTripletSchema = object({
58474
- r: number().int().min(0).max(255),
58475
- g: number().int().min(0).max(255),
58476
- b: number().int().min(0).max(255)
58495
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58496
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58497
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58477
58498
  });
58478
58499
  var HsvTripletSchema = object({
58479
58500
  /** Hue in degrees, 0..360. */
58480
- h: number().min(0).max(360),
58501
+ h: number().min(0).max(360).meta(readingMeta("none")),
58481
58502
  /** Saturation as 0..100 inclusive. */
58482
- s: number().min(0).max(100),
58503
+ s: number().min(0).max(100).meta(readingMeta("none")),
58483
58504
  /** Value/brightness as 0..100 inclusive. */
58484
- v: number().min(0).max(100)
58505
+ v: number().min(0).max(100).meta(readingMeta("none"))
58485
58506
  });
58486
58507
  var ColorInputSchema = discriminatedUnion("mode", [
58487
58508
  object({
@@ -58510,7 +58531,7 @@ var ColorStatusSchema = object({
58510
58531
  /** Populated when `mode === 'hsv'`. */
58511
58532
  hsv: HsvTripletSchema.optional(),
58512
58533
  /** Populated when `mode === 'mired'`. */
58513
- mireds: number().int().optional(),
58534
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58514
58535
  /** Ms epoch of the last operator-driven change. */
58515
58536
  lastChangedAt: number()
58516
58537
  });
@@ -58719,18 +58740,12 @@ var FORM_OF_KIND = {
58719
58740
  function consumableFormOf(item) {
58720
58741
  return item.form ?? FORM_OF_KIND[consumableKindOf(item)];
58721
58742
  }
58722
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58723
- * each. */
58724
- var ConsumableRemainingUnitSchema = _enum([
58725
- "days",
58726
- "hours",
58727
- "cycles"
58728
- ]);
58729
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58730
- * days). `value` null = unknown (D393). */
58743
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58744
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58745
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58731
58746
  var ConsumableRemainingSchema = object({
58732
- value: number().min(0).nullable(),
58733
- unit: ConsumableRemainingUnitSchema
58747
+ value: number().min(0).nullable().meta(readingMeta("device")),
58748
+ kind: ConsumableRemainingKindSchema
58734
58749
  });
58735
58750
  /** A single consumable item. Either a continuous `level` (remaining
58736
58751
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58743,7 +58758,7 @@ var ConsumableItemSchema = object({
58743
58758
  /** Display name. */
58744
58759
  label: string().min(1),
58745
58760
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58746
- level: number().min(0).max(100).nullable(),
58761
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58747
58762
  /** The provider's discrete verdict when known (binary mode). */
58748
58763
  status: ConsumableStatusSchema.nullable(),
58749
58764
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58825,8 +58840,8 @@ var consumablesCustomize = {
58825
58840
  transforms: [{ kind: "identity" }]
58826
58841
  }],
58827
58842
  literals: [{
58828
- path: "remaining.unit",
58829
- values: [...ConsumableRemainingUnitSchema.options]
58843
+ path: "remaining.kind",
58844
+ values: [...ConsumableRemainingKindSchema.options]
58830
58845
  }]
58831
58846
  },
58832
58847
  {
@@ -58955,7 +58970,6 @@ reset: method(object({
58955
58970
  },
58956
58971
  "remaining.value": {
58957
58972
  kind: "gauge",
58958
- unitFrom: "remaining.unit",
58959
58973
  deadband: 1,
58960
58974
  displayName: "Remaining"
58961
58975
  }
@@ -59025,10 +59039,9 @@ var contactCapability = {
59025
59039
  * (empty array for the other kinds). It lives in the slice because
59026
59040
  * HA `select.*` entities may update their options at runtime.
59027
59041
  *
59028
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59029
- * `kind === 'numeric'` and are absent (undefined) for all other
59030
- * kinds. They are populated live from HA attributes on each state
59031
- * push — consistent with `options` and the sensor-unit approach.
59042
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59043
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59044
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59032
59045
  */
59033
59046
  var ControlStatusSchema = object({
59034
59047
  kind: _enum([
@@ -59040,39 +59053,17 @@ var ControlStatusSchema = object({
59040
59053
  /** Current value. Numeric kind → number; select/text/datetime → string.
59041
59054
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59042
59055
  * or time-only — HA accepts all three forms). */
59043
- value: union([number(), string()]),
59056
+ value: union([number().meta(readingMeta("device")), string()]),
59044
59057
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59045
59058
  options: array(string()),
59046
59059
  /** Ms epoch when the slice was last updated. */
59047
59060
  lastChangedAt: number(),
59048
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59049
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59050
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59051
- unit: string().optional(),
59052
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59053
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59054
- min: number().optional(),
59055
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59056
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59057
- max: number().optional(),
59058
- /** Step increment. Populated live from HA `attributes.step`.
59059
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59060
- step: number().optional(),
59061
- /** Suggested decimal places for numeric display. Populated live from HA
59062
- * `attributes.suggested_display_precision`, or derived from `step` when the
59063
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59064
- * other kinds. Falls back to auto-formatting when absent. */
59065
- precision: number().int().min(0).max(10).optional(),
59066
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59067
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59068
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59069
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59070
- * `has_date`/`has_time` attributes on each push. */
59071
- format: _enum([
59072
- "date",
59073
- "time",
59074
- "datetime"
59075
- ]).optional()
59061
+ /** Minimum allowed value, in the device quantity's base unit. */
59062
+ min: number().optional().meta(readingMeta("device")),
59063
+ /** Maximum allowed value, in the device quantity's base unit. */
59064
+ max: number().optional().meta(readingMeta("device")),
59065
+ /** Step increment, in the device quantity's base unit. */
59066
+ step: number().optional().meta(readingMeta("device"))
59076
59067
  });
59077
59068
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59078
59069
  object({
@@ -59138,9 +59129,9 @@ var CoverStatusSchema = object({
59138
59129
  ]),
59139
59130
  /** 0 = fully closed, 100 = fully open. Null when the device has
59140
59131
  * no intermediate position surface. */
59141
- position: number().min(0).max(100).nullable(),
59132
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59142
59133
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59143
- tiltPosition: number().min(0).max(100).nullable(),
59134
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59144
59135
  /** Ms epoch when the slice was last updated. */
59145
59136
  lastChangedAt: number()
59146
59137
  });
@@ -59243,9 +59234,9 @@ var NormalizedRangeSchema$1 = object({
59243
59234
  var DayNightStatusSchema = object({
59244
59235
  mode: DayNightModeSchema,
59245
59236
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59246
- sensitivity: number().optional(),
59237
+ sensitivity: number().optional().meta(readingMeta("none")),
59247
59238
  /** Delay before the IR-cut filter flips, in seconds. */
59248
- switchDelaySec: number().optional(),
59239
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59249
59240
  lastFetchedAt: number()
59250
59241
  });
59251
59242
  /**
@@ -59463,11 +59454,11 @@ var DishwasherStatusSchema = object({
59463
59454
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59464
59455
  availablePrograms: array(string()),
59465
59456
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59466
- remainingSeconds: number().int().nonnegative().nullable(),
59457
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59467
59458
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59468
- delayedStartSeconds: number().int().nonnegative().nullable(),
59459
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59469
59460
  /** Cycle completion 0..100, or null when not applicable. */
59470
- progressPercent: number().min(0).max(100).nullable(),
59461
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59471
59462
  /** True when the door is reported open; null when the door state is not known. */
59472
59463
  doorOpen: boolean().nullable(),
59473
59464
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59491,7 +59482,7 @@ var DishwasherStatusSchema = object({
59491
59482
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59492
59483
  machineCareDue: boolean().nullable(),
59493
59484
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59494
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59485
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59495
59486
  /** Option states: null when the appliance does not expose the option or has not said. */
59496
59487
  halfLoad: boolean().nullable(),
59497
59488
  hygienePlus: boolean().nullable(),
@@ -59506,7 +59497,7 @@ var DishwasherStatusSchema = object({
59506
59497
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59507
59498
  activeEvents: array(string()),
59508
59499
  /** Numeric error code when the appliance reports one; null otherwise. */
59509
- errorCode: number().nullable(),
59500
+ errorCode: number().nullable().meta(readingMeta("none")),
59510
59501
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59511
59502
  errorLabel: string().nullable(),
59512
59503
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59737,7 +59728,7 @@ var DoorbellStatusSchema = object({
59737
59728
  /** Ms epoch of the last press. null = never observed since this provider started. */
59738
59729
  lastPressedAt: number().nullable(),
59739
59730
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59740
- pressCountSinceStart: number()
59731
+ pressCountSinceStart: number().meta(readingMeta("count"))
59741
59732
  });
59742
59733
  var DoorbellPressEventSchema = object({
59743
59734
  deviceId: number(),
@@ -59820,12 +59811,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59820
59811
  var EnumSensorStatusSchema = object({
59821
59812
  value: string(),
59822
59813
  /**
59823
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59824
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59825
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59826
- * a time-only sensor → `'time'`.
59814
+ * Set when `value` is a timestamp rather than an enum state, so history
59815
+ * does not learn ISO strings as states. The display shape (date / time /
59816
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59827
59817
  */
59828
- format: EnumSensorDateTimeFormatSchema.optional(),
59818
+ timestamp: boolean().optional(),
59829
59819
  /** Ms epoch when the slice was last updated. */
59830
59820
  lastFetchedAt: number()
59831
59821
  });
@@ -59852,11 +59842,11 @@ var enumSensorCapability = {
59852
59842
  * History (D850): the value as an `enum` series (an appliance's program or
59853
59843
  * operation state, an HVAC action). The option set is per device, so the
59854
59844
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59855
- * `format` carries a timestamp, not a state, and is not recorded.
59845
+ * `timestamp` carries a clock, not a state, and is not recorded.
59856
59846
  */
59857
59847
  history: { fields: { value: {
59858
59848
  kind: "enum",
59859
- skipWhenSet: "format"
59849
+ skipWhenSet: "timestamp"
59860
59850
  } } },
59861
59851
  /** Clock fields: written, but excluded from the compare that decides
59862
59852
  * whether persisting is worth a SQLite commit. */
@@ -59876,13 +59866,13 @@ var EventFireSchema = object({
59876
59866
  deviceId: number(),
59877
59867
  eventType: string(),
59878
59868
  data: record(string(), unknown()).nullable(),
59879
- timestamp: number(),
59880
- seq: number()
59869
+ timestamp: number().meta(readingMeta("none")),
59870
+ seq: number().meta(readingMeta("count"))
59881
59871
  });
59882
59872
  var EventEmitterStatusSchema = object({
59883
59873
  eventTypes: array(string()),
59884
59874
  lastEvent: EventFireSchema.nullable(),
59885
- eventCountSinceStart: number()
59875
+ eventCountSinceStart: number().meta(readingMeta("count"))
59886
59876
  });
59887
59877
  var eventEmitterCapability = {
59888
59878
  name: "event-emitter",
@@ -60295,10 +60285,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60295
60285
  var FanControlStatusSchema = object({
60296
60286
  /** Active speed as 0..100 inclusive. Null when the device has no
60297
60287
  * speed surface (single-speed fan). */
60298
- percentage: number().min(0).max(100).nullable(),
60288
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60299
60289
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60300
60290
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60301
- percentageStep: number().positive().optional(),
60291
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60302
60292
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60303
60293
  * — empty string when no preset is active or supported. */
60304
60294
  preset: string(),
@@ -60419,7 +60409,7 @@ var FeatureProbeStatusSchema = object({
60419
60409
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60420
60410
  model: string().nullable(),
60421
60411
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60422
- channelCount: number().nullable(),
60412
+ channelCount: number().nullable().meta(readingMeta("count")),
60423
60413
  /**
60424
60414
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60425
60415
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60544,7 +60534,7 @@ var gasCapability = {
60544
60534
  };
60545
60535
  var HeatingValveStatusSchema = object({
60546
60536
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60547
- position: number().min(0).max(100).nullable(),
60537
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60548
60538
  /** Ms epoch when the slice was last updated (push or poll). */
60549
60539
  lastFetchedAt: number()
60550
60540
  });
@@ -60587,9 +60577,9 @@ var HumidifierStatusSchema = object({
60587
60577
  /** Whether the humidifier is currently on. */
60588
60578
  on: boolean(),
60589
60579
  /** Current measured relative humidity (0..100). Null when not reported. */
60590
- currentHumidity: number().min(0).max(100).nullable(),
60580
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60591
60581
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60592
- targetHumidity: number().min(0).max(100).nullable(),
60582
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60593
60583
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60594
60584
  * device has no mode surface. */
60595
60585
  mode: string().nullable(),
@@ -60599,9 +60589,9 @@ var HumidifierStatusSchema = object({
60599
60589
  * `idle` / `off`). Null when the device doesn't report it. */
60600
60590
  action: string().nullable(),
60601
60591
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60602
- minHumidity: number().nullable(),
60592
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60603
60593
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60604
- maxHumidity: number().nullable(),
60594
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60605
60595
  /** Ms epoch when the slice was last updated. */
60606
60596
  lastChangedAt: number()
60607
60597
  });
@@ -60671,22 +60661,13 @@ var humidifierCapability = {
60671
60661
  * `sourceInfo`.
60672
60662
  */
60673
60663
  var HumiditySensorStatusSchema = object({
60674
- /** Current relative humidity, 0..100. */
60675
- percent: number().min(0).max(100).meta({
60676
- unit: "%",
60664
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60665
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60666
+ precision: 0,
60677
60667
  label: "Humidity"
60678
- }),
60668
+ })),
60679
60669
  /** Ms epoch when the slice was last updated. */
60680
- lastFetchedAt: number(),
60681
- /** Live display unit from the upstream source (e.g. HA
60682
- * `attributes.unit_of_measurement`). The UI prefers this over the
60683
- * role's canonical unit. Absent → fall back to the canonical unit. */
60684
- unit: string().optional(),
60685
- /** Suggested decimal places for numeric display.
60686
- * Populated live from the upstream source when provided (e.g. HA
60687
- * `attributes.suggested_display_precision`). Falls back to
60688
- * auto-formatting when absent. */
60689
- precision: number().int().min(0).max(10).optional()
60670
+ lastFetchedAt: number()
60690
60671
  });
60691
60672
  var humiditySensorCapability = {
60692
60673
  name: "humidity-sensor",
@@ -60717,12 +60698,7 @@ var humiditySensorCapability = {
60717
60698
  } } },
60718
60699
  /** Clock fields: written, but excluded from the compare that decides
60719
60700
  * whether persisting is worth a SQLite commit. */
60720
- volatileStateFields: ["lastFetchedAt"],
60721
- customize: {
60722
- version: 1,
60723
- advanced: "derive",
60724
- targets: []
60725
- }
60701
+ volatileStateFields: ["lastFetchedAt"]
60726
60702
  };
60727
60703
  /**
60728
60704
  * Image display cap. Models a single still image exposed by an integration —
@@ -60743,7 +60719,7 @@ var ImageStatusSchema = object({
60743
60719
  * entity exposes no `entity_picture` (yet). */
60744
60720
  url: string().nullable(),
60745
60721
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60746
- lastUpdated: number().nullable()
60722
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60747
60723
  });
60748
60724
  var imageCapability = {
60749
60725
  name: "image",
@@ -60831,19 +60807,19 @@ var NormalizedRangeSchema = object({
60831
60807
  */
60832
60808
  var ImageSettingsStatusSchema = object({
60833
60809
  /** Normalized 0–100. */
60834
- brightness: number().optional(),
60810
+ brightness: number().optional().meta(readingMeta("none")),
60835
60811
  /** Normalized 0–100. */
60836
- contrast: number().optional(),
60812
+ contrast: number().optional().meta(readingMeta("none")),
60837
60813
  /** Normalized 0–100. */
60838
- saturation: number().optional(),
60814
+ saturation: number().optional().meta(readingMeta("none")),
60839
60815
  /** Normalized 0–100. */
60840
- sharpness: number().optional(),
60816
+ sharpness: number().optional().meta(readingMeta("none")),
60841
60817
  mirror: boolean().optional(),
60842
60818
  flip: boolean().optional(),
60843
60819
  rotate: ImageRotateSchema.optional(),
60844
60820
  whiteBalance: WhiteBalanceModeSchema.optional(),
60845
60821
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60846
- warmth: number().optional(),
60822
+ warmth: number().optional().meta(readingMeta("none")),
60847
60823
  exposureMode: ExposureModeSchema.optional(),
60848
60824
  backlightMode: BacklightModeSchema.optional(),
60849
60825
  lastFetchedAt: number()
@@ -61122,10 +61098,10 @@ var IntercomAbilitySchema = object({
61122
61098
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61123
61099
  codecs: array(string()),
61124
61100
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61125
- sampleRate: number(),
61101
+ sampleRate: number().meta(readingMeta("none")),
61126
61102
  duplex: _enum(["half", "full"]),
61127
61103
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61128
- maxBacklogMs: number()
61104
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61129
61105
  });
61130
61106
  var IntercomStatusSchema = object({
61131
61107
  /** True while a talk session is open. */
@@ -61352,13 +61328,13 @@ var LawnMowerControlStatusSchema = object({
61352
61328
  activity: LawnMowerActivitySchema,
61353
61329
  /** 0..100 battery percentage. Null when the device has no battery
61354
61330
  * reading. */
61355
- batteryLevel: number().min(0).max(100).nullable(),
61331
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61356
61332
  /** 0..100 mowing-completion percentage of the current task, or null when no
61357
61333
  * task is active / progress is unavailable. */
61358
- progressPercent: number().min(0).max(100).nullable(),
61334
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61359
61335
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61360
61336
  * or null when unknown. */
61361
- currentCode: number().nullable(),
61337
+ currentCode: number().nullable().meta(readingMeta("none")),
61362
61338
  /** Human label for {@link currentCode}, or null when undecodable. */
61363
61339
  currentCodeLabel: string().nullable(),
61364
61340
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61891,7 +61867,7 @@ var MediaPlayerStatusSchema = object({
61891
61867
  state: MediaPlayerStateSchema,
61892
61868
  /** Volume as 0..100 inclusive. Null when the device has no volume
61893
61869
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61894
- volumeLevel: number().min(0).max(100).nullable(),
61870
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61895
61871
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61896
61872
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61897
61873
  isMuted: boolean().nullable(),
@@ -61904,10 +61880,10 @@ var MediaPlayerStatusSchema = object({
61904
61880
  currentMedia: MediaInfoSchema.nullable(),
61905
61881
  /** Current playback position in ms. Null when not seekable or
61906
61882
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61907
- positionMs: number().int().nonnegative().nullable(),
61883
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61908
61884
  /** Total duration of the current media in ms. Null when unknown
61909
61885
  * (live stream) or nothing is playing. */
61910
- durationMs: number().int().nonnegative().nullable(),
61886
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61911
61887
  /** Shuffle toggle. Null when no shuffle surface.
61912
61888
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61913
61889
  shuffle: boolean().nullable(),
@@ -62216,7 +62192,7 @@ var motionCustomize = {
62216
62192
  eventName: "onMotionChanged"
62217
62193
  },
62218
62194
  deviceTypes: [DeviceType.Camera],
62219
- apply: "replace",
62195
+ apply: "add",
62220
62196
  applyAuthority: {
62221
62197
  kind: "camera-sensors",
62222
62198
  timeoutField: "motionCooldownMs"
@@ -62288,7 +62264,7 @@ var MotionStatusSchema = object({
62288
62264
  * it, every provider seeded it at boot, and every session left it
62289
62265
  * `detected: true` after its detach.
62290
62266
  */
62291
- autoClearAfterMs: number().nullable()
62267
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62292
62268
  });
62293
62269
  /**
62294
62270
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62470,7 +62446,7 @@ var MotionZoneRegionSchema = object({
62470
62446
  * the grid region(s). */
62471
62447
  var MotionZoneStatusSchema = object({
62472
62448
  enabled: boolean(),
62473
- sensitivity: number(),
62449
+ sensitivity: number().meta(readingMeta("none")),
62474
62450
  /** Grid region(s). Today exactly one `grid` shape. */
62475
62451
  regions: array(MotionZoneRegionSchema),
62476
62452
  lastFetchedAt: number()
@@ -62840,7 +62816,7 @@ var NavigationStatusSchema = object({
62840
62816
  following: boolean(),
62841
62817
  flash: boolean(),
62842
62818
  lightMode: NavigationLightModeSchema,
62843
- lightLevel: number().min(40).max(100),
62819
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62844
62820
  /** Ms epoch when the slice was last updated. */
62845
62821
  lastChangedAt: number()
62846
62822
  });
@@ -62974,13 +62950,13 @@ var NetworkLinkStatusSchema = object({
62974
62950
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62975
62951
  * SKIP a null rather than coerce it.
62976
62952
  */
62977
- signalPercent: number().min(0).max(100).nullable(),
62953
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62978
62954
  /** Raw received signal strength in dBm, when the firmware reports one. */
62979
- rssiDbm: number().optional(),
62955
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62980
62956
  /** Network name of a wireless link, when the firmware reports it. */
62981
62957
  ssid: string().optional(),
62982
62958
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62983
- lastUpdated: number()
62959
+ lastUpdated: number().meta(readingMeta("none"))
62984
62960
  });
62985
62961
  var networkLinkCapability = {
62986
62962
  name: "network-link",
@@ -63365,7 +63341,7 @@ var NotifierStatusSchema = object({
63365
63341
  lastError: string().nullable(),
63366
63342
  /** Number of deliveries currently buffered server-side (rate-limited
63367
63343
  * notifiers may queue). 0 when the provider sends synchronously. */
63368
- queueDepth: number().int().nonnegative(),
63344
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63369
63345
  /** Per-feature capability matrix — authoritative source for the
63370
63346
  * compose-form field gating. */
63371
63347
  supports: NotifierSupportsSchema
@@ -63457,11 +63433,9 @@ onSent: { data: object({
63457
63433
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63458
63434
  * for: water flow, distance, weight, frequency, signal strength, …).
63459
63435
  *
63460
- * The `unit` and `precision` fields carry the live display metadata that
63461
- * the upstream source provides with each state push (HA
63462
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63463
- * The UI reads them directly from the slice — they are the single source
63464
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63436
+ * The quantity is not on this slice. It is chosen when the device is
63437
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63438
+ * that quantity's base unit. `null` is a refused sample (D393).
63465
63439
  *
63466
63440
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63467
63441
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63470,15 +63444,8 @@ onSent: { data: object({
63470
63444
  * numeric values land here.
63471
63445
  */
63472
63446
  var NumericSensorStatusSchema = object({
63473
- value: number(),
63474
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63475
- * Populated live from the upstream source on each state push.
63476
- * Absent for unitless sensors. */
63477
- unit: string().optional(),
63478
- /** Suggested decimal places for numeric display.
63479
- * Populated live from the upstream source when provided.
63480
- * Falls back to auto-formatting when absent. */
63481
- precision: number().int().min(0).max(10).optional(),
63447
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63448
+ value: number().nullable().meta(readingMeta("device")),
63482
63449
  /** Ms epoch when the slice was last updated. */
63483
63450
  lastFetchedAt: number()
63484
63451
  });
@@ -63502,11 +63469,11 @@ var numericSensorCapability = {
63502
63469
  */
63503
63470
  durability: "restored",
63504
63471
  /**
63505
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63472
+ * History (D850): recorded series. The unit is the device quantity's base,
63473
+ * resolved when the series is read — the cap has no single unit.
63506
63474
  */
63507
63475
  history: { fields: { value: {
63508
63476
  kind: "gauge",
63509
- unitFrom: "unit",
63510
63477
  minIntervalMs: 3e4
63511
63478
  } } },
63512
63479
  /** Clock fields: written, but excluded from the compare that decides
@@ -63798,23 +63765,26 @@ var PetFeederStatusSchema = object({
63798
63765
  /** Food currently in the bowl (grams). Null when the device has not
63799
63766
  * reported a reading yet. On dual-hopper models this is the combined
63800
63767
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63801
- foodLevel: number().nullable(),
63768
+ foodLevel: number().nullable().meta(readingMeta("none")),
63802
63769
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63803
63770
  * single-hopper models. */
63804
- food1: number().nullable(),
63771
+ food1: number().nullable().meta(readingMeta("none")),
63805
63772
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63806
63773
  * single-hopper models. */
63807
- food2: number().nullable(),
63774
+ food2: number().nullable().meta(readingMeta("none")),
63808
63775
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63809
63776
  * (`device_class: problem`, on = low). True when the bowl is empty /
63810
63777
  * below the feeder's low threshold. */
63811
63778
  lowFood: boolean(),
63812
63779
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63813
63780
  * device has no battery reading. */
63814
- batteryPower: number().min(0).max(100).nullable(),
63815
- /** Days of desiccant life remaining. Null when the model has no
63816
- * desiccant sensor. */
63817
- desiccantLeftDays: number().nullable(),
63781
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63782
+ /**
63783
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63784
+ * Providers convert from the days the feeder reports. Null when the
63785
+ * model has no desiccant sensor.
63786
+ */
63787
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63818
63788
  /** True while a feed is in progress. */
63819
63789
  feeding: boolean(),
63820
63790
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63825,7 +63795,7 @@ var PetFeederStatusSchema = object({
63825
63795
  * with `errorCode` for consumers that want the raw integer. */
63826
63796
  error: string().nullable(),
63827
63797
  /** Raw device error code (0 / null = no error). */
63828
- errorCode: number().nullable(),
63798
+ errorCode: number().nullable().meta(readingMeta("none")),
63829
63799
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63830
63800
  isDualHopper: boolean(),
63831
63801
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63835,7 +63805,7 @@ var PetFeederStatusSchema = object({
63835
63805
  /** Play a chime when dispensing. */
63836
63806
  feedSound: boolean(),
63837
63807
  /** Speaker / prompt volume level (device-scaled integer). */
63838
- volume: number(),
63808
+ volume: number().meta(readingMeta("none")),
63839
63809
  /** Ms epoch when the slice was last refreshed from the cloud. */
63840
63810
  lastFetchedAt: number()
63841
63811
  });
@@ -63954,7 +63924,7 @@ var petFeederCapability = {
63954
63924
  },
63955
63925
  desiccantLeftDays: {
63956
63926
  kind: "gauge",
63957
- unit: "days"
63927
+ unit: "s"
63958
63928
  }
63959
63929
  } }
63960
63930
  };
@@ -64355,36 +64325,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64355
64325
  */
64356
64326
  var PowerMeterStatusSchema = object({
64357
64327
  /** Instantaneous power draw in watts. */
64358
- watts: number().optional().meta({
64359
- unit: "W",
64360
- label: "Power"
64361
- }),
64328
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64362
64329
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64363
- kwhTotal: number().optional().meta({
64364
- unit: "kWh",
64365
- label: "Energy"
64366
- }),
64330
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64367
64331
  /** Voltage in volts. */
64368
- volts: number().optional(),
64332
+ volts: number().optional().meta(readingMeta("voltage")),
64369
64333
  /** Current in amperes. */
64370
- amps: number().optional(),
64371
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64372
- va: number().optional(),
64334
+ amps: number().optional().meta(readingMeta("current")),
64335
+ /** Apparent power in volt-amperes. */
64336
+ va: number().optional().meta(readingMeta("apparent-power")),
64373
64337
  /** Power factor (0..1) if reported. */
64374
- powerFactor: number().min(0).max(1).optional(),
64338
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64375
64339
  /** Ms epoch when the slice was last updated. */
64376
- lastFetchedAt: number(),
64377
- /** Live display unit of the single metric this slice carries (e.g. HA
64378
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64379
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64380
- * unit per slice is unambiguous. The UI prefers this over the role's
64381
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64382
- unit: string().optional(),
64383
- /** Suggested decimal places for numeric display.
64384
- * Populated live from the upstream source when provided (e.g. HA
64385
- * `attributes.suggested_display_precision`). Falls back to
64386
- * auto-formatting when absent. */
64387
- precision: number().int().min(0).max(10).optional()
64340
+ lastFetchedAt: number()
64388
64341
  });
64389
64342
  var powerMeterCapability = {
64390
64343
  name: "power-meter",
@@ -64448,12 +64401,7 @@ var powerMeterCapability = {
64448
64401
  } },
64449
64402
  /** Clock fields: written, but excluded from the compare that decides
64450
64403
  * whether persisting is worth a SQLite commit. */
64451
- volatileStateFields: ["lastFetchedAt"],
64452
- customize: {
64453
- version: 1,
64454
- advanced: "derive",
64455
- targets: []
64456
- }
64404
+ volatileStateFields: ["lastFetchedAt"]
64457
64405
  };
64458
64406
  /**
64459
64407
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64473,11 +64421,11 @@ var powerMeterCapability = {
64473
64421
  */
64474
64422
  var GpsLocationSchema = object({
64475
64423
  /** Latitude in decimal degrees, -90..90. */
64476
- latitude: number().min(-90).max(90),
64424
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64477
64425
  /** Longitude in decimal degrees, -180..180. */
64478
- longitude: number().min(-180).max(180),
64426
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64479
64427
  /** Reported accuracy in meters (lower = better). */
64480
- accuracyMeters: number().nonnegative()
64428
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64481
64429
  });
64482
64430
  var PresenceStatusSchema = object({
64483
64431
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64489,7 +64437,7 @@ var PresenceStatusSchema = object({
64489
64437
  gps: GpsLocationSchema.nullable(),
64490
64438
  /** Optional battery level of the tracking device (0..100). Null
64491
64439
  * when not reported. */
64492
- batteryPercent: number().min(0).max(100).nullable(),
64440
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64493
64441
  /** Ms epoch when the slice was last updated. */
64494
64442
  lastChangedAt: number()
64495
64443
  });
@@ -64539,19 +64487,10 @@ var presenceCapability = {
64539
64487
  * `sourceInfo`.
64540
64488
  */
64541
64489
  var PressureSensorStatusSchema = object({
64542
- /** Current pressure in hPa. */
64543
- hpa: number(),
64490
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64491
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64544
64492
  /** Ms epoch when the slice was last updated. */
64545
- lastFetchedAt: number(),
64546
- /** Live display unit from the upstream source (e.g. HA
64547
- * `attributes.unit_of_measurement`). The UI prefers this over the
64548
- * role's canonical unit. Absent → fall back to the canonical unit. */
64549
- unit: string().optional(),
64550
- /** Suggested decimal places for numeric display.
64551
- * Populated live from the upstream source when provided (e.g. HA
64552
- * `attributes.suggested_display_precision`). Falls back to
64553
- * auto-formatting when absent. */
64554
- precision: number().int().min(0).max(10).optional()
64493
+ lastFetchedAt: number()
64555
64494
  });
64556
64495
  var pressureSensorCapability = {
64557
64496
  name: "pressure-sensor",
@@ -64965,8 +64904,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64965
64904
  }), _void(), { kind: "mutation" });
64966
64905
  var PtzAutotrackSettingsSchema = object({
64967
64906
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64968
- stopDelaySeconds: number().int().min(0).max(300),
64969
- disappearDelaySeconds: number().int().min(0).max(300)
64907
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64908
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64970
64909
  });
64971
64910
  /**
64972
64911
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65128,7 +65067,7 @@ var RunningStateStatusSchema = object({
65128
65067
  /** null = unknown — never coerced to false (D393). */
65129
65068
  running: boolean().nullable(),
65130
65069
  /** Epoch ms the current boolean began; null while `running` is null. */
65131
- since: number().nullable(),
65070
+ since: number().nullable().meta(readingMeta("none")),
65132
65071
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65133
65072
  phase: string().nullable(),
65134
65073
  /** null until a reading has been published (D393). */
@@ -65143,7 +65082,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65143
65082
  /** Generic Customize target for the running boolean (D936). */
65144
65083
  var runningCustomize = {
65145
65084
  version: 1,
65146
- advanced: "derive",
65147
65085
  targets: [{
65148
65086
  id: "running",
65149
65087
  cardinality: "scalar",
@@ -65163,7 +65101,7 @@ var runningCustomize = {
65163
65101
  DeviceType.Thermostat,
65164
65102
  DeviceType.PetFeeder
65165
65103
  ],
65166
- apply: "add-or-replace",
65104
+ apply: "add",
65167
65105
  path: "running",
65168
65106
  sources: [
65169
65107
  {
@@ -66427,9 +66365,9 @@ var RecordTriggerSchema = _enum([
66427
66365
  */
66428
66366
  var RecordWindowSchema = object({
66429
66367
  trigger: RecordTriggerSchema,
66430
- day: number().int().min(0).max(6),
66431
- startMinute: number().int().min(0).max(1439),
66432
- endMinute: number().int().min(1).max(1440)
66368
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66369
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66370
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66433
66371
  });
66434
66372
  /** Status of one physical volume, as the camera itself describes it. */
66435
66373
  var OnboardStorageVolumeSchema = object({
@@ -66505,10 +66443,10 @@ var RecordingOnboardStatusSchema = object({
66505
66443
  /** Master "record to the card at all" switch. */
66506
66444
  enabled: boolean().nullable(),
66507
66445
  overwriteWhenFull: boolean().nullable(),
66508
- preRecordSec: number().nullable(),
66509
- postRecordSec: number().nullable(),
66446
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66447
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66510
66448
  /** Length of one recorded file, in minutes. */
66511
- segmentMinutes: number().nullable(),
66449
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66512
66450
  /** The primary track's weekly windows, flattened. */
66513
66451
  windows: array(RecordWindowSchema),
66514
66452
  /**
@@ -66524,7 +66462,7 @@ var RecordingOnboardStatusSchema = object({
66524
66462
  * provider reporting a non-zero count also reports the schedule as not
66525
66463
  * writable.
66526
66464
  */
66527
- unreadableWindows: number(),
66465
+ unreadableWindows: number().meta(readingMeta("count")),
66528
66466
  /**
66529
66467
  * The camera is scheduled to record and has NO usable storage.
66530
66468
  *
@@ -67369,7 +67307,7 @@ var CamStreamDescriptorSchema = object({
67369
67307
  url: string().optional(),
67370
67308
  codec: string().optional(),
67371
67309
  resolution: CamStreamResolutionSchema.optional(),
67372
- fps: number().positive().optional(),
67310
+ fps: number().positive().optional().meta(readingMeta("rate")),
67373
67311
  label: string().optional(),
67374
67312
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67375
67313
  deviceFeatures: array(string()).optional(),
@@ -67456,18 +67394,18 @@ var StreamProfileSchema = _enum([
67456
67394
  ]);
67457
67395
  /** The current encoding config of one stream profile. */
67458
67396
  var StreamProfileConfigSchema = object({
67459
- width: number(),
67460
- height: number(),
67397
+ width: number().meta(readingMeta("none")),
67398
+ height: number().meta(readingMeta("none")),
67461
67399
  codec: _enum(["h264", "h265"]),
67462
- framerate: number(),
67463
- bitrate: number(),
67400
+ framerate: number().meta(readingMeta("rate")),
67401
+ bitrate: number().meta(readingMeta("bitrate")),
67464
67402
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67465
67403
  encoderProfile: _enum([
67466
67404
  "high",
67467
67405
  "main",
67468
67406
  "baseline"
67469
67407
  ]).optional(),
67470
- gop: number().optional(),
67408
+ gop: number().optional().meta(readingMeta("none")),
67471
67409
  /**
67472
67410
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67473
67411
  *
@@ -67688,12 +67626,7 @@ var switchCapability = {
67688
67626
  label: "On",
67689
67627
  kind: "boolean"
67690
67628
  }
67691
- }] },
67692
- customize: {
67693
- version: 1,
67694
- advanced: "derive",
67695
- targets: []
67696
- }
67629
+ }] }
67697
67630
  };
67698
67631
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67699
67632
  function hasControlCharacter(value) {
@@ -68276,35 +68209,21 @@ var tamperCapability = {
68276
68209
  * entries with `device_class: temperature` and any future native
68277
68210
  * thermometer.
68278
68211
  *
68279
- * Unit normalisation: providers convert to Celsius before storing.
68280
- * The slice value is always Celsius so cross-cap aggregators
68281
- * (climate-control's `currentTemp`, energy analytics) can compose
68282
- * without per-source unit fixups. The canonical display unit (`°C`) is
68283
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68284
- * `sourceInfo`.
68212
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68213
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68285
68214
  *
68286
68215
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68287
68216
  * frozen feed (provider hung) distinct from a "temperature hasn't
68288
68217
  * changed" steady state.
68289
68218
  */
68290
68219
  var TemperatureSensorStatusSchema = object({
68291
- /** Current temperature in Celsius. */
68292
- celsius: number().meta({
68293
- unit: "°C",
68220
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68221
+ celsius: number().nullable().meta(readingMeta("temperature", {
68222
+ precision: 1,
68294
68223
  label: "Temperature"
68295
- }),
68224
+ })),
68296
68225
  /** Ms epoch when the slice was last updated (push or poll). */
68297
- lastFetchedAt: number(),
68298
- /** Live display unit from the upstream source (e.g. HA
68299
- * `attributes.unit_of_measurement`). The UI prefers this over the
68300
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68301
- * Absent → the UI falls back to the role's canonical unit. */
68302
- unit: string().optional(),
68303
- /** Suggested decimal places for numeric display.
68304
- * Populated live from the upstream source when provided (e.g. HA
68305
- * `attributes.suggested_display_precision`). Falls back to
68306
- * auto-formatting when absent. */
68307
- precision: number().int().min(0).max(10).optional()
68226
+ lastFetchedAt: number()
68308
68227
  });
68309
68228
  var temperatureSensorCapability = {
68310
68229
  name: "temperature-sensor",
@@ -68335,12 +68254,7 @@ var temperatureSensorCapability = {
68335
68254
  } } },
68336
68255
  /** Clock fields: written, but excluded from the compare that decides
68337
68256
  * whether persisting is worth a SQLite commit. */
68338
- volatileStateFields: ["lastFetchedAt"],
68339
- customize: {
68340
- version: 1,
68341
- advanced: "derive",
68342
- targets: []
68343
- }
68257
+ volatileStateFields: ["lastFetchedAt"]
68344
68258
  };
68345
68259
  /**
68346
68260
  * toast — system-scoped singleton capability that streams toast
@@ -68623,16 +68537,16 @@ var VacuumControlStatusSchema = object({
68623
68537
  ]),
68624
68538
  /** 0..100 battery percentage. Null when the device has no battery
68625
68539
  * reading. */
68626
- batteryLevel: number().min(0).max(100).nullable(),
68540
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68627
68541
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68628
68542
  * the vacuum has no speed control. */
68629
68543
  fanSpeed: string().nullable(),
68630
68544
  /** Speed tokens the hardware accepts — drives the UI selector. */
68631
68545
  availableFanSpeeds: array(string()),
68632
68546
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68633
- progressPercent: number().min(0).max(100).nullable(),
68547
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68634
68548
  /** Current error code (0 / null = no error). */
68635
- errorCode: number().nullable(),
68549
+ errorCode: number().nullable().meta(readingMeta("none")),
68636
68550
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68637
68551
  errorLabel: string().nullable(),
68638
68552
  /** Ms epoch when the slice was last updated. */
@@ -68710,7 +68624,7 @@ var ValveStatusSchema = object({
68710
68624
  ]),
68711
68625
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68712
68626
  * intermediate position surface. */
68713
- position: number().min(0).max(100).nullable(),
68627
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68714
68628
  /** Ms epoch when the slice was last updated. */
68715
68629
  lastChangedAt: number()
68716
68630
  });
@@ -68818,9 +68732,9 @@ var vibrationCapability = {
68818
68732
  */
68819
68733
  var WaterHeaterStatusSchema = object({
68820
68734
  /** Current measured temperature. Null when not reported. */
68821
- currentTemp: number().nullable(),
68735
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68822
68736
  /** Target temperature setpoint. Null when no setpoint surface. */
68823
- targetTemp: number().nullable(),
68737
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68824
68738
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68825
68739
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68826
68740
  * device reports an unknown state. */
@@ -68831,9 +68745,9 @@ var WaterHeaterStatusSchema = object({
68831
68745
  * has no away surface. */
68832
68746
  away: boolean().nullable(),
68833
68747
  /** HA `min_temp` attribute. Null when not reported. */
68834
- minTemp: number().nullable(),
68748
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68835
68749
  /** HA `max_temp` attribute. Null when not reported. */
68836
- maxTemp: number().nullable(),
68750
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68837
68751
  /** Ms epoch when the slice was last updated. */
68838
68752
  lastChangedAt: number()
68839
68753
  });
@@ -68918,25 +68832,18 @@ var waterHeaterCapability = {
68918
68832
  * Forecast deferred — current conditions only for v1.
68919
68833
  */
68920
68834
  var WeatherStatusSchema = object({
68921
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68922
- * when no condition has been reported yet. */
68835
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68923
68836
  condition: string().nullable(),
68924
- /** Current temperature in the reported unit. Null when not provided. */
68925
- temperature: number().nullable(),
68926
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68927
- temperatureUnit: string().nullable(),
68837
+ /** Current temperature in Celsius. Null when not provided or refused. */
68838
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68928
68839
  /** Relative humidity (0..100). Null when not provided. */
68929
- humidity: number().min(0).max(100).nullable(),
68930
- /** Barometric pressure in the reported unit. Null when not provided. */
68931
- pressure: number().nullable(),
68932
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68933
- pressureUnit: string().nullable(),
68934
- /** Wind speed in the reported unit. Null when not provided. */
68935
- windSpeed: number().nullable(),
68936
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68937
- windSpeedUnit: string().nullable(),
68840
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68841
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68842
+ pressure: number().nullable().meta(readingMeta("pressure")),
68843
+ /** Wind speed in metres per second. Null when not provided or refused. */
68844
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68938
68845
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68939
- windBearing: number().nullable(),
68846
+ windBearing: number().nullable().meta(readingMeta("angle")),
68940
68847
  /** Ms epoch when the slice was last updated. */
68941
68848
  lastFetchedAt: number()
68942
68849
  });
@@ -68964,12 +68871,12 @@ var weatherCapability = {
68964
68871
  */
68965
68872
  durability: "session",
68966
68873
  /**
68967
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68874
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68968
68875
  */
68969
68876
  history: { fields: {
68970
68877
  temperature: {
68971
68878
  kind: "gauge",
68972
- unitFrom: "temperatureUnit",
68879
+ unit: "°C",
68973
68880
  deadband: .2
68974
68881
  },
68975
68882
  humidity: {
@@ -68979,12 +68886,12 @@ var weatherCapability = {
68979
68886
  },
68980
68887
  pressure: {
68981
68888
  kind: "gauge",
68982
- unitFrom: "pressureUnit",
68889
+ unit: "hPa",
68983
68890
  deadband: .5
68984
68891
  },
68985
68892
  windSpeed: {
68986
68893
  kind: "gauge",
68987
- unitFrom: "windSpeedUnit",
68894
+ unit: "m/s",
68988
68895
  deadband: .5
68989
68896
  }
68990
68897
  } }
@@ -69922,6 +69829,404 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
69922
69829
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
69923
69830
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
69924
69831
  }
69832
+ /**
69833
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69834
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69835
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69836
+ * decimals). K derives from `°C = K − 273.15`.
69837
+ */
69838
+ var UNIT_TABLE = {
69839
+ "°C": {
69840
+ dimension: "temperature",
69841
+ factor: 1,
69842
+ offset: 0
69843
+ },
69844
+ "°F": {
69845
+ dimension: "temperature",
69846
+ factor: 5 / 9,
69847
+ offset: -160 / 9
69848
+ },
69849
+ K: {
69850
+ dimension: "temperature",
69851
+ factor: 1,
69852
+ offset: -273.15
69853
+ },
69854
+ hPa: {
69855
+ dimension: "pressure",
69856
+ factor: 1,
69857
+ offset: 0
69858
+ },
69859
+ kPa: {
69860
+ dimension: "pressure",
69861
+ factor: 10,
69862
+ offset: 0
69863
+ },
69864
+ Pa: {
69865
+ dimension: "pressure",
69866
+ factor: .01,
69867
+ offset: 0
69868
+ },
69869
+ mbar: {
69870
+ dimension: "pressure",
69871
+ factor: 1,
69872
+ offset: 0
69873
+ },
69874
+ bar: {
69875
+ dimension: "pressure",
69876
+ factor: 1e3,
69877
+ offset: 0
69878
+ },
69879
+ inHg: {
69880
+ dimension: "pressure",
69881
+ factor: 33.8639,
69882
+ offset: 0
69883
+ },
69884
+ mmHg: {
69885
+ dimension: "pressure",
69886
+ factor: 1.33322,
69887
+ offset: 0
69888
+ },
69889
+ psi: {
69890
+ dimension: "pressure",
69891
+ factor: 68.9476,
69892
+ offset: 0
69893
+ },
69894
+ "m/s": {
69895
+ dimension: "speed",
69896
+ factor: 1,
69897
+ offset: 0
69898
+ },
69899
+ "km/h": {
69900
+ dimension: "speed",
69901
+ factor: 1 / 3.6,
69902
+ offset: 0
69903
+ },
69904
+ mph: {
69905
+ dimension: "speed",
69906
+ factor: .44704,
69907
+ offset: 0
69908
+ },
69909
+ kn: {
69910
+ dimension: "speed",
69911
+ factor: .514444,
69912
+ offset: 0
69913
+ },
69914
+ "mm/h": {
69915
+ dimension: "precipitation-rate",
69916
+ factor: 1,
69917
+ offset: 0
69918
+ },
69919
+ "in/h": {
69920
+ dimension: "precipitation-rate",
69921
+ factor: 25.4,
69922
+ offset: 0
69923
+ },
69924
+ m: {
69925
+ dimension: "length",
69926
+ factor: 1,
69927
+ offset: 0
69928
+ },
69929
+ mm: {
69930
+ dimension: "length",
69931
+ factor: .001,
69932
+ offset: 0
69933
+ },
69934
+ cm: {
69935
+ dimension: "length",
69936
+ factor: .01,
69937
+ offset: 0
69938
+ },
69939
+ km: {
69940
+ dimension: "length",
69941
+ factor: 1e3,
69942
+ offset: 0
69943
+ },
69944
+ in: {
69945
+ dimension: "length",
69946
+ factor: .0254,
69947
+ offset: 0
69948
+ },
69949
+ ft: {
69950
+ dimension: "length",
69951
+ factor: .3048,
69952
+ offset: 0
69953
+ },
69954
+ mi: {
69955
+ dimension: "length",
69956
+ factor: 1609.344,
69957
+ offset: 0
69958
+ },
69959
+ "m²": {
69960
+ dimension: "area",
69961
+ factor: 1,
69962
+ offset: 0
69963
+ },
69964
+ lx: {
69965
+ dimension: "illuminance",
69966
+ factor: 1,
69967
+ offset: 0
69968
+ },
69969
+ "W/m²": {
69970
+ dimension: "irradiance",
69971
+ factor: 1,
69972
+ offset: 0
69973
+ },
69974
+ W: {
69975
+ dimension: "power",
69976
+ factor: 1,
69977
+ offset: 0
69978
+ },
69979
+ kW: {
69980
+ dimension: "power",
69981
+ factor: 1e3,
69982
+ offset: 0
69983
+ },
69984
+ VA: {
69985
+ dimension: "apparent-power",
69986
+ factor: 1,
69987
+ offset: 0
69988
+ },
69989
+ Wh: {
69990
+ dimension: "energy",
69991
+ factor: 1,
69992
+ offset: 0
69993
+ },
69994
+ kWh: {
69995
+ dimension: "energy",
69996
+ factor: 1e3,
69997
+ offset: 0
69998
+ },
69999
+ MWh: {
70000
+ dimension: "energy",
70001
+ factor: 1e6,
70002
+ offset: 0
70003
+ },
70004
+ V: {
70005
+ dimension: "voltage",
70006
+ factor: 1,
70007
+ offset: 0
70008
+ },
70009
+ mV: {
70010
+ dimension: "voltage",
70011
+ factor: .001,
70012
+ offset: 0
70013
+ },
70014
+ A: {
70015
+ dimension: "current",
70016
+ factor: 1,
70017
+ offset: 0
70018
+ },
70019
+ mA: {
70020
+ dimension: "current",
70021
+ factor: .001,
70022
+ offset: 0
70023
+ },
70024
+ ppm: {
70025
+ dimension: "concentration-volume",
70026
+ factor: 1,
70027
+ offset: 0
70028
+ },
70029
+ ppb: {
70030
+ dimension: "concentration-volume",
70031
+ factor: .001,
70032
+ offset: 0
70033
+ },
70034
+ "µg/m³": {
70035
+ dimension: "concentration-mass",
70036
+ factor: 1,
70037
+ offset: 0
70038
+ },
70039
+ "mg/m³": {
70040
+ dimension: "concentration-mass",
70041
+ factor: 1e3,
70042
+ offset: 0
70043
+ },
70044
+ kg: {
70045
+ dimension: "mass",
70046
+ factor: 1,
70047
+ offset: 0
70048
+ },
70049
+ g: {
70050
+ dimension: "mass",
70051
+ factor: .001,
70052
+ offset: 0
70053
+ },
70054
+ lb: {
70055
+ dimension: "mass",
70056
+ factor: .453592,
70057
+ offset: 0
70058
+ },
70059
+ oz: {
70060
+ dimension: "mass",
70061
+ factor: .0283495,
70062
+ offset: 0
70063
+ },
70064
+ "%": {
70065
+ dimension: "percentage",
70066
+ factor: 1,
70067
+ offset: 0
70068
+ },
70069
+ L: {
70070
+ dimension: "volume",
70071
+ factor: 1,
70072
+ offset: 0
70073
+ },
70074
+ mL: {
70075
+ dimension: "volume",
70076
+ factor: .001,
70077
+ offset: 0
70078
+ },
70079
+ gal: {
70080
+ dimension: "volume",
70081
+ factor: 3.785411784,
70082
+ offset: 0
70083
+ },
70084
+ s: {
70085
+ dimension: "duration",
70086
+ factor: 1,
70087
+ offset: 0
70088
+ },
70089
+ ms: {
70090
+ dimension: "duration",
70091
+ factor: .001,
70092
+ offset: 0
70093
+ },
70094
+ min: {
70095
+ dimension: "duration",
70096
+ factor: 60,
70097
+ offset: 0
70098
+ },
70099
+ h: {
70100
+ dimension: "duration",
70101
+ factor: 3600,
70102
+ offset: 0
70103
+ },
70104
+ d: {
70105
+ dimension: "duration",
70106
+ factor: 86400,
70107
+ offset: 0
70108
+ },
70109
+ B: {
70110
+ dimension: "information",
70111
+ factor: 1,
70112
+ offset: 0
70113
+ },
70114
+ KiB: {
70115
+ dimension: "information",
70116
+ factor: 1024,
70117
+ offset: 0
70118
+ },
70119
+ MiB: {
70120
+ dimension: "information",
70121
+ factor: 1024 ** 2,
70122
+ offset: 0
70123
+ },
70124
+ GiB: {
70125
+ dimension: "information",
70126
+ factor: 1024 ** 3,
70127
+ offset: 0
70128
+ },
70129
+ TiB: {
70130
+ dimension: "information",
70131
+ factor: 1024 ** 4,
70132
+ offset: 0
70133
+ },
70134
+ kB: {
70135
+ dimension: "information",
70136
+ factor: 1e3,
70137
+ offset: 0
70138
+ },
70139
+ MB: {
70140
+ dimension: "information",
70141
+ factor: 1e6,
70142
+ offset: 0
70143
+ },
70144
+ GB: {
70145
+ dimension: "information",
70146
+ factor: 1e9,
70147
+ offset: 0
70148
+ },
70149
+ "°": {
70150
+ dimension: "angle",
70151
+ factor: 1,
70152
+ offset: 0
70153
+ },
70154
+ dBFS: {
70155
+ dimension: "sound-level",
70156
+ factor: 1,
70157
+ offset: 0
70158
+ },
70159
+ dBm: {
70160
+ dimension: "signal-level",
70161
+ factor: 1,
70162
+ offset: 0
70163
+ },
70164
+ mired: {
70165
+ dimension: "colour-temperature",
70166
+ factor: 1,
70167
+ offset: 0
70168
+ },
70169
+ kbps: {
70170
+ dimension: "bitrate",
70171
+ factor: 1,
70172
+ offset: 0
70173
+ },
70174
+ Mbps: {
70175
+ dimension: "bitrate",
70176
+ factor: 1e3,
70177
+ offset: 0
70178
+ },
70179
+ fps: {
70180
+ dimension: "rate",
70181
+ factor: 1,
70182
+ offset: 0
70183
+ }
70184
+ };
70185
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
70186
+ function specFor(unit) {
70187
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
70188
+ }
70189
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
70190
+ function unitDimension(unit) {
70191
+ return specFor(unit)?.dimension;
70192
+ }
70193
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
70194
+ function toCanonical(value, spec) {
70195
+ return value * spec.factor + spec.offset;
70196
+ }
70197
+ /** Canonical-unit value → the unit described by `spec`. */
70198
+ function fromCanonical(canonical, spec) {
70199
+ return (canonical - spec.offset) / spec.factor;
70200
+ }
70201
+ /**
70202
+ * Graceful variant for the render path: returns the converted value, or
70203
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
70204
+ * Never throws — a render caller falls back to the raw value + source unit.
70205
+ */
70206
+ function tryConvertUnit(value, from, to) {
70207
+ if (from === to) return value;
70208
+ const fromSpec = specFor(from);
70209
+ if (!fromSpec) return void 0;
70210
+ const toSpec = specFor(to);
70211
+ if (!toSpec) return void 0;
70212
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
70213
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
70214
+ }
70215
+ /**
70216
+ * Every known unit spelling in a dimension (table insertion order — canonical
70217
+ * unit first). Powers the UI's same-dimension unit picker.
70218
+ */
70219
+ /**
70220
+ * Convert a difference, not a reading. Affine units (°F) shift an absolute
70221
+ * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
70222
+ */
70223
+ function tryConvertDelta(value, from, to) {
70224
+ if (from === to) return value;
70225
+ const origin = tryConvertUnit(0, from, to);
70226
+ const shifted = tryConvertUnit(value, from, to);
70227
+ if (origin === void 0 || shifted === void 0) return void 0;
70228
+ return shifted - origin;
70229
+ }
69925
70230
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69926
70231
  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;
69927
70232
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -84591,7 +84896,6 @@ var SENSOR_CAP_VOCABULARY = {
84591
84896
  field: "value",
84592
84897
  quantity: t("the reading", "il valore"),
84593
84898
  label: t("Reading", "Valore"),
84594
- unitField: "unit",
84595
84899
  icon: "gauge"
84596
84900
  }]
84597
84901
  },
@@ -84727,7 +85031,7 @@ var SENSOR_CAP_VOCABULARY = {
84727
85031
  field: "temperature",
84728
85032
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84729
85033
  label: t("Temperature", "Temperatura"),
84730
- unitField: "temperatureUnit",
85034
+ unit: "°C",
84731
85035
  icon: "thermometer"
84732
85036
  },
84733
85037
  {
@@ -84743,7 +85047,7 @@ var SENSOR_CAP_VOCABULARY = {
84743
85047
  field: "windSpeed",
84744
85048
  quantity: t("the wind speed", "la velocità del vento"),
84745
85049
  label: t("Wind", "Vento"),
84746
- unitField: "windSpeedUnit",
85050
+ unit: "m/s",
84747
85051
  icon: "wind"
84748
85052
  },
84749
85053
  {
@@ -84751,7 +85055,7 @@ var SENSOR_CAP_VOCABULARY = {
84751
85055
  field: "pressure",
84752
85056
  quantity: t("the air pressure", "la pressione atmosferica"),
84753
85057
  label: t("Pressure", "Pressione"),
84754
- unitField: "pressureUnit",
85058
+ unit: "hPa",
84755
85059
  icon: "gauge"
84756
85060
  }
84757
85061
  ]
@@ -89324,6 +89628,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
89324
89628
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
89325
89629
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
89326
89630
  /**
89631
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
89632
+ * An unknown unit or a unit from another dimension is a refusal: the caller
89633
+ * logs it and publishes null (D393). This function never guesses.
89634
+ */
89635
+ var DIMENSION_QUANTITY = {
89636
+ temperature: "temperature",
89637
+ pressure: "pressure",
89638
+ speed: "speed",
89639
+ "precipitation-rate": "precipitation-rate",
89640
+ length: "length",
89641
+ area: "area",
89642
+ illuminance: "illuminance",
89643
+ irradiance: "irradiance",
89644
+ power: "power",
89645
+ "apparent-power": "apparent-power",
89646
+ energy: "energy",
89647
+ voltage: "voltage",
89648
+ current: "current",
89649
+ "concentration-volume": "concentration-volume",
89650
+ "concentration-mass": "concentration-mass",
89651
+ mass: "mass",
89652
+ percentage: "percentage",
89653
+ volume: "volume",
89654
+ duration: "duration",
89655
+ information: "information",
89656
+ angle: "angle",
89657
+ "sound-level": "sound-level",
89658
+ "signal-level": "signal-level",
89659
+ "colour-temperature": "colour-temperature",
89660
+ bitrate: "bitrate",
89661
+ rate: "rate"
89662
+ };
89663
+ /**
89664
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
89665
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
89666
+ * precipitation is chosen by the caller (a device class), not by the symbol.
89667
+ */
89668
+ function quantityFromUnit(rawUnit) {
89669
+ const unit = normalizeUnit(rawUnit);
89670
+ if (unit === void 0) return void 0;
89671
+ if (unit === "ppb") return "concentration-ppb";
89672
+ if (unit === "ms") return "duration-ms";
89673
+ const dimension = unitDimension(unit);
89674
+ if (dimension === void 0) return void 0;
89675
+ return DIMENSION_QUANTITY[dimension];
89676
+ }
89677
+ function ingestReading(input) {
89678
+ if (!Number.isFinite(input.value)) return {
89679
+ ok: false,
89680
+ reason: "non-finite"
89681
+ };
89682
+ if (input.quantity === "device") return {
89683
+ ok: false,
89684
+ reason: "unknown-unit"
89685
+ };
89686
+ const raw = input.rawUnit?.trim();
89687
+ if (raw === void 0 || raw.length === 0) return {
89688
+ ok: true,
89689
+ value: input.value
89690
+ };
89691
+ const unit = normalizeUnit(raw);
89692
+ if (unit === void 0) return {
89693
+ ok: false,
89694
+ reason: "unknown-unit"
89695
+ };
89696
+ const base = baseUnitOf(input.quantity);
89697
+ if (base === void 0) {
89698
+ if (unitDimension(unit) !== void 0) return {
89699
+ ok: false,
89700
+ reason: "cross-dimension"
89701
+ };
89702
+ return {
89703
+ ok: false,
89704
+ reason: "unknown-unit"
89705
+ };
89706
+ }
89707
+ const converted = tryConvertUnit(input.value, unit, base);
89708
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
89709
+ ok: false,
89710
+ reason: "unknown-unit"
89711
+ } : {
89712
+ ok: false,
89713
+ reason: "cross-dimension"
89714
+ };
89715
+ return {
89716
+ ok: true,
89717
+ value: converted
89718
+ };
89719
+ }
89720
+ /**
89327
89721
  * Pure fuzzy matcher for adoption location import. Normalized
89328
89722
  * case-insensitive Levenshtein similarity, used to reuse an existing
89329
89723
  * CamStack location when an imported source location (e.g. an HA area) is
@@ -89552,6 +89946,12 @@ Object.defineProperty(exports, "automationControlCapability", {
89552
89946
  return automationControlCapability;
89553
89947
  }
89554
89948
  });
89949
+ Object.defineProperty(exports, "baseUnitOf", {
89950
+ enumerable: true,
89951
+ get: function() {
89952
+ return baseUnitOf;
89953
+ }
89954
+ });
89555
89955
  Object.defineProperty(exports, "batteryCapability", {
89556
89956
  enumerable: true,
89557
89957
  get: function() {
@@ -89768,6 +90168,18 @@ Object.defineProperty(exports, "imageCapability", {
89768
90168
  return imageCapability;
89769
90169
  }
89770
90170
  });
90171
+ Object.defineProperty(exports, "ingestReading", {
90172
+ enumerable: true,
90173
+ get: function() {
90174
+ return ingestReading;
90175
+ }
90176
+ });
90177
+ Object.defineProperty(exports, "isReadingQuantity", {
90178
+ enumerable: true,
90179
+ get: function() {
90180
+ return isReadingQuantity;
90181
+ }
90182
+ });
89771
90183
  Object.defineProperty(exports, "lawnMowerControlCapability", {
89772
90184
  enumerable: true,
89773
90185
  get: function() {
@@ -89876,6 +90288,12 @@ Object.defineProperty(exports, "pressureSensorCapability", {
89876
90288
  return pressureSensorCapability;
89877
90289
  }
89878
90290
  });
90291
+ Object.defineProperty(exports, "quantityFromUnit", {
90292
+ enumerable: true,
90293
+ get: function() {
90294
+ return quantityFromUnit;
90295
+ }
90296
+ });
89879
90297
  Object.defineProperty(exports, "record", {
89880
90298
  enumerable: true,
89881
90299
  get: function() {
@@ -89942,6 +90360,18 @@ Object.defineProperty(exports, "temperatureSensorCapability", {
89942
90360
  return temperatureSensorCapability;
89943
90361
  }
89944
90362
  });
90363
+ Object.defineProperty(exports, "tryConvertDelta", {
90364
+ enumerable: true,
90365
+ get: function() {
90366
+ return tryConvertDelta;
90367
+ }
90368
+ });
90369
+ Object.defineProperty(exports, "tryConvertUnit", {
90370
+ enumerable: true,
90371
+ get: function() {
90372
+ return tryConvertUnit;
90373
+ }
90374
+ });
89945
90375
  Object.defineProperty(exports, "union", {
89946
90376
  enumerable: true,
89947
90377
  get: function() {