@camstack/addon-provider-homeassistant 1.2.624 → 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. */
@@ -59718,6 +59709,7 @@ var doorbellCustomize = {
59718
59709
  applyAuthority: { kind: "composition-claim" },
59719
59710
  eventName: "onPressed",
59720
59711
  edges: ["rising"],
59712
+ defaultEdge: "rising",
59721
59713
  timestampPath: "lastPressedAt",
59722
59714
  counterPath: "pressCountSinceStart",
59723
59715
  highWater: "composer-advance",
@@ -59736,7 +59728,7 @@ var DoorbellStatusSchema = object({
59736
59728
  /** Ms epoch of the last press. null = never observed since this provider started. */
59737
59729
  lastPressedAt: number().nullable(),
59738
59730
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59739
- pressCountSinceStart: number()
59731
+ pressCountSinceStart: number().meta(readingMeta("count"))
59740
59732
  });
59741
59733
  var DoorbellPressEventSchema = object({
59742
59734
  deviceId: number(),
@@ -59819,12 +59811,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59819
59811
  var EnumSensorStatusSchema = object({
59820
59812
  value: string(),
59821
59813
  /**
59822
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59823
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59824
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59825
- * 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.
59826
59817
  */
59827
- format: EnumSensorDateTimeFormatSchema.optional(),
59818
+ timestamp: boolean().optional(),
59828
59819
  /** Ms epoch when the slice was last updated. */
59829
59820
  lastFetchedAt: number()
59830
59821
  });
@@ -59851,11 +59842,11 @@ var enumSensorCapability = {
59851
59842
  * History (D850): the value as an `enum` series (an appliance's program or
59852
59843
  * operation state, an HVAC action). The option set is per device, so the
59853
59844
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59854
- * `format` carries a timestamp, not a state, and is not recorded.
59845
+ * `timestamp` carries a clock, not a state, and is not recorded.
59855
59846
  */
59856
59847
  history: { fields: { value: {
59857
59848
  kind: "enum",
59858
- skipWhenSet: "format"
59849
+ skipWhenSet: "timestamp"
59859
59850
  } } },
59860
59851
  /** Clock fields: written, but excluded from the compare that decides
59861
59852
  * whether persisting is worth a SQLite commit. */
@@ -59875,13 +59866,13 @@ var EventFireSchema = object({
59875
59866
  deviceId: number(),
59876
59867
  eventType: string(),
59877
59868
  data: record(string(), unknown()).nullable(),
59878
- timestamp: number(),
59879
- seq: number()
59869
+ timestamp: number().meta(readingMeta("none")),
59870
+ seq: number().meta(readingMeta("count"))
59880
59871
  });
59881
59872
  var EventEmitterStatusSchema = object({
59882
59873
  eventTypes: array(string()),
59883
59874
  lastEvent: EventFireSchema.nullable(),
59884
- eventCountSinceStart: number()
59875
+ eventCountSinceStart: number().meta(readingMeta("count"))
59885
59876
  });
59886
59877
  var eventEmitterCapability = {
59887
59878
  name: "event-emitter",
@@ -60294,10 +60285,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60294
60285
  var FanControlStatusSchema = object({
60295
60286
  /** Active speed as 0..100 inclusive. Null when the device has no
60296
60287
  * speed surface (single-speed fan). */
60297
- percentage: number().min(0).max(100).nullable(),
60288
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60298
60289
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60299
60290
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60300
- percentageStep: number().positive().optional(),
60291
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60301
60292
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60302
60293
  * — empty string when no preset is active or supported. */
60303
60294
  preset: string(),
@@ -60418,7 +60409,7 @@ var FeatureProbeStatusSchema = object({
60418
60409
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60419
60410
  model: string().nullable(),
60420
60411
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60421
- channelCount: number().nullable(),
60412
+ channelCount: number().nullable().meta(readingMeta("count")),
60422
60413
  /**
60423
60414
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60424
60415
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60543,7 +60534,7 @@ var gasCapability = {
60543
60534
  };
60544
60535
  var HeatingValveStatusSchema = object({
60545
60536
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60546
- position: number().min(0).max(100).nullable(),
60537
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60547
60538
  /** Ms epoch when the slice was last updated (push or poll). */
60548
60539
  lastFetchedAt: number()
60549
60540
  });
@@ -60586,9 +60577,9 @@ var HumidifierStatusSchema = object({
60586
60577
  /** Whether the humidifier is currently on. */
60587
60578
  on: boolean(),
60588
60579
  /** Current measured relative humidity (0..100). Null when not reported. */
60589
- currentHumidity: number().min(0).max(100).nullable(),
60580
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60590
60581
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60591
- targetHumidity: number().min(0).max(100).nullable(),
60582
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60592
60583
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60593
60584
  * device has no mode surface. */
60594
60585
  mode: string().nullable(),
@@ -60598,9 +60589,9 @@ var HumidifierStatusSchema = object({
60598
60589
  * `idle` / `off`). Null when the device doesn't report it. */
60599
60590
  action: string().nullable(),
60600
60591
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60601
- minHumidity: number().nullable(),
60592
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60602
60593
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60603
- maxHumidity: number().nullable(),
60594
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60604
60595
  /** Ms epoch when the slice was last updated. */
60605
60596
  lastChangedAt: number()
60606
60597
  });
@@ -60670,22 +60661,13 @@ var humidifierCapability = {
60670
60661
  * `sourceInfo`.
60671
60662
  */
60672
60663
  var HumiditySensorStatusSchema = object({
60673
- /** Current relative humidity, 0..100. */
60674
- percent: number().min(0).max(100).meta({
60675
- 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,
60676
60667
  label: "Humidity"
60677
- }),
60668
+ })),
60678
60669
  /** Ms epoch when the slice was last updated. */
60679
- lastFetchedAt: number(),
60680
- /** Live display unit from the upstream source (e.g. HA
60681
- * `attributes.unit_of_measurement`). The UI prefers this over the
60682
- * role's canonical unit. Absent → fall back to the canonical unit. */
60683
- unit: string().optional(),
60684
- /** Suggested decimal places for numeric display.
60685
- * Populated live from the upstream source when provided (e.g. HA
60686
- * `attributes.suggested_display_precision`). Falls back to
60687
- * auto-formatting when absent. */
60688
- precision: number().int().min(0).max(10).optional()
60670
+ lastFetchedAt: number()
60689
60671
  });
60690
60672
  var humiditySensorCapability = {
60691
60673
  name: "humidity-sensor",
@@ -60716,12 +60698,7 @@ var humiditySensorCapability = {
60716
60698
  } } },
60717
60699
  /** Clock fields: written, but excluded from the compare that decides
60718
60700
  * whether persisting is worth a SQLite commit. */
60719
- volatileStateFields: ["lastFetchedAt"],
60720
- customize: {
60721
- version: 1,
60722
- advanced: "derive",
60723
- targets: []
60724
- }
60701
+ volatileStateFields: ["lastFetchedAt"]
60725
60702
  };
60726
60703
  /**
60727
60704
  * Image display cap. Models a single still image exposed by an integration —
@@ -60742,7 +60719,7 @@ var ImageStatusSchema = object({
60742
60719
  * entity exposes no `entity_picture` (yet). */
60743
60720
  url: string().nullable(),
60744
60721
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60745
- lastUpdated: number().nullable()
60722
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60746
60723
  });
60747
60724
  var imageCapability = {
60748
60725
  name: "image",
@@ -60830,19 +60807,19 @@ var NormalizedRangeSchema = object({
60830
60807
  */
60831
60808
  var ImageSettingsStatusSchema = object({
60832
60809
  /** Normalized 0–100. */
60833
- brightness: number().optional(),
60810
+ brightness: number().optional().meta(readingMeta("none")),
60834
60811
  /** Normalized 0–100. */
60835
- contrast: number().optional(),
60812
+ contrast: number().optional().meta(readingMeta("none")),
60836
60813
  /** Normalized 0–100. */
60837
- saturation: number().optional(),
60814
+ saturation: number().optional().meta(readingMeta("none")),
60838
60815
  /** Normalized 0–100. */
60839
- sharpness: number().optional(),
60816
+ sharpness: number().optional().meta(readingMeta("none")),
60840
60817
  mirror: boolean().optional(),
60841
60818
  flip: boolean().optional(),
60842
60819
  rotate: ImageRotateSchema.optional(),
60843
60820
  whiteBalance: WhiteBalanceModeSchema.optional(),
60844
60821
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60845
- warmth: number().optional(),
60822
+ warmth: number().optional().meta(readingMeta("none")),
60846
60823
  exposureMode: ExposureModeSchema.optional(),
60847
60824
  backlightMode: BacklightModeSchema.optional(),
60848
60825
  lastFetchedAt: number()
@@ -61121,10 +61098,10 @@ var IntercomAbilitySchema = object({
61121
61098
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61122
61099
  codecs: array(string()),
61123
61100
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61124
- sampleRate: number(),
61101
+ sampleRate: number().meta(readingMeta("none")),
61125
61102
  duplex: _enum(["half", "full"]),
61126
61103
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61127
- maxBacklogMs: number()
61104
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61128
61105
  });
61129
61106
  var IntercomStatusSchema = object({
61130
61107
  /** True while a talk session is open. */
@@ -61351,13 +61328,13 @@ var LawnMowerControlStatusSchema = object({
61351
61328
  activity: LawnMowerActivitySchema,
61352
61329
  /** 0..100 battery percentage. Null when the device has no battery
61353
61330
  * reading. */
61354
- batteryLevel: number().min(0).max(100).nullable(),
61331
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61355
61332
  /** 0..100 mowing-completion percentage of the current task, or null when no
61356
61333
  * task is active / progress is unavailable. */
61357
- progressPercent: number().min(0).max(100).nullable(),
61334
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61358
61335
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61359
61336
  * or null when unknown. */
61360
- currentCode: number().nullable(),
61337
+ currentCode: number().nullable().meta(readingMeta("none")),
61361
61338
  /** Human label for {@link currentCode}, or null when undecodable. */
61362
61339
  currentCodeLabel: string().nullable(),
61363
61340
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61890,7 +61867,7 @@ var MediaPlayerStatusSchema = object({
61890
61867
  state: MediaPlayerStateSchema,
61891
61868
  /** Volume as 0..100 inclusive. Null when the device has no volume
61892
61869
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61893
- volumeLevel: number().min(0).max(100).nullable(),
61870
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61894
61871
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61895
61872
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61896
61873
  isMuted: boolean().nullable(),
@@ -61903,10 +61880,10 @@ var MediaPlayerStatusSchema = object({
61903
61880
  currentMedia: MediaInfoSchema.nullable(),
61904
61881
  /** Current playback position in ms. Null when not seekable or
61905
61882
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61906
- positionMs: number().int().nonnegative().nullable(),
61883
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61907
61884
  /** Total duration of the current media in ms. Null when unknown
61908
61885
  * (live stream) or nothing is playing. */
61909
- durationMs: number().int().nonnegative().nullable(),
61886
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61910
61887
  /** Shuffle toggle. Null when no shuffle surface.
61911
61888
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61912
61889
  shuffle: boolean().nullable(),
@@ -62215,7 +62192,7 @@ var motionCustomize = {
62215
62192
  eventName: "onMotionChanged"
62216
62193
  },
62217
62194
  deviceTypes: [DeviceType.Camera],
62218
- apply: "replace",
62195
+ apply: "add",
62219
62196
  applyAuthority: {
62220
62197
  kind: "camera-sensors",
62221
62198
  timeoutField: "motionCooldownMs"
@@ -62287,7 +62264,7 @@ var MotionStatusSchema = object({
62287
62264
  * it, every provider seeded it at boot, and every session left it
62288
62265
  * `detected: true` after its detach.
62289
62266
  */
62290
- autoClearAfterMs: number().nullable()
62267
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62291
62268
  });
62292
62269
  /**
62293
62270
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62469,7 +62446,7 @@ var MotionZoneRegionSchema = object({
62469
62446
  * the grid region(s). */
62470
62447
  var MotionZoneStatusSchema = object({
62471
62448
  enabled: boolean(),
62472
- sensitivity: number(),
62449
+ sensitivity: number().meta(readingMeta("none")),
62473
62450
  /** Grid region(s). Today exactly one `grid` shape. */
62474
62451
  regions: array(MotionZoneRegionSchema),
62475
62452
  lastFetchedAt: number()
@@ -62839,7 +62816,7 @@ var NavigationStatusSchema = object({
62839
62816
  following: boolean(),
62840
62817
  flash: boolean(),
62841
62818
  lightMode: NavigationLightModeSchema,
62842
- lightLevel: number().min(40).max(100),
62819
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62843
62820
  /** Ms epoch when the slice was last updated. */
62844
62821
  lastChangedAt: number()
62845
62822
  });
@@ -62973,13 +62950,13 @@ var NetworkLinkStatusSchema = object({
62973
62950
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62974
62951
  * SKIP a null rather than coerce it.
62975
62952
  */
62976
- signalPercent: number().min(0).max(100).nullable(),
62953
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62977
62954
  /** Raw received signal strength in dBm, when the firmware reports one. */
62978
- rssiDbm: number().optional(),
62955
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62979
62956
  /** Network name of a wireless link, when the firmware reports it. */
62980
62957
  ssid: string().optional(),
62981
62958
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62982
- lastUpdated: number()
62959
+ lastUpdated: number().meta(readingMeta("none"))
62983
62960
  });
62984
62961
  var networkLinkCapability = {
62985
62962
  name: "network-link",
@@ -63364,7 +63341,7 @@ var NotifierStatusSchema = object({
63364
63341
  lastError: string().nullable(),
63365
63342
  /** Number of deliveries currently buffered server-side (rate-limited
63366
63343
  * notifiers may queue). 0 when the provider sends synchronously. */
63367
- queueDepth: number().int().nonnegative(),
63344
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63368
63345
  /** Per-feature capability matrix — authoritative source for the
63369
63346
  * compose-form field gating. */
63370
63347
  supports: NotifierSupportsSchema
@@ -63456,11 +63433,9 @@ onSent: { data: object({
63456
63433
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63457
63434
  * for: water flow, distance, weight, frequency, signal strength, …).
63458
63435
  *
63459
- * The `unit` and `precision` fields carry the live display metadata that
63460
- * the upstream source provides with each state push (HA
63461
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63462
- * The UI reads them directly from the slice — they are the single source
63463
- * 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).
63464
63439
  *
63465
63440
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63466
63441
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63469,15 +63444,8 @@ onSent: { data: object({
63469
63444
  * numeric values land here.
63470
63445
  */
63471
63446
  var NumericSensorStatusSchema = object({
63472
- value: number(),
63473
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63474
- * Populated live from the upstream source on each state push.
63475
- * Absent for unitless sensors. */
63476
- unit: string().optional(),
63477
- /** Suggested decimal places for numeric display.
63478
- * Populated live from the upstream source when provided.
63479
- * Falls back to auto-formatting when absent. */
63480
- 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")),
63481
63449
  /** Ms epoch when the slice was last updated. */
63482
63450
  lastFetchedAt: number()
63483
63451
  });
@@ -63501,11 +63469,11 @@ var numericSensorCapability = {
63501
63469
  */
63502
63470
  durability: "restored",
63503
63471
  /**
63504
- * 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.
63505
63474
  */
63506
63475
  history: { fields: { value: {
63507
63476
  kind: "gauge",
63508
- unitFrom: "unit",
63509
63477
  minIntervalMs: 3e4
63510
63478
  } } },
63511
63479
  /** Clock fields: written, but excluded from the compare that decides
@@ -63797,23 +63765,26 @@ var PetFeederStatusSchema = object({
63797
63765
  /** Food currently in the bowl (grams). Null when the device has not
63798
63766
  * reported a reading yet. On dual-hopper models this is the combined
63799
63767
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63800
- foodLevel: number().nullable(),
63768
+ foodLevel: number().nullable().meta(readingMeta("none")),
63801
63769
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63802
63770
  * single-hopper models. */
63803
- food1: number().nullable(),
63771
+ food1: number().nullable().meta(readingMeta("none")),
63804
63772
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63805
63773
  * single-hopper models. */
63806
- food2: number().nullable(),
63774
+ food2: number().nullable().meta(readingMeta("none")),
63807
63775
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63808
63776
  * (`device_class: problem`, on = low). True when the bowl is empty /
63809
63777
  * below the feeder's low threshold. */
63810
63778
  lowFood: boolean(),
63811
63779
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63812
63780
  * device has no battery reading. */
63813
- batteryPower: number().min(0).max(100).nullable(),
63814
- /** Days of desiccant life remaining. Null when the model has no
63815
- * desiccant sensor. */
63816
- 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")),
63817
63788
  /** True while a feed is in progress. */
63818
63789
  feeding: boolean(),
63819
63790
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63824,7 +63795,7 @@ var PetFeederStatusSchema = object({
63824
63795
  * with `errorCode` for consumers that want the raw integer. */
63825
63796
  error: string().nullable(),
63826
63797
  /** Raw device error code (0 / null = no error). */
63827
- errorCode: number().nullable(),
63798
+ errorCode: number().nullable().meta(readingMeta("none")),
63828
63799
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63829
63800
  isDualHopper: boolean(),
63830
63801
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63834,7 +63805,7 @@ var PetFeederStatusSchema = object({
63834
63805
  /** Play a chime when dispensing. */
63835
63806
  feedSound: boolean(),
63836
63807
  /** Speaker / prompt volume level (device-scaled integer). */
63837
- volume: number(),
63808
+ volume: number().meta(readingMeta("none")),
63838
63809
  /** Ms epoch when the slice was last refreshed from the cloud. */
63839
63810
  lastFetchedAt: number()
63840
63811
  });
@@ -63953,7 +63924,7 @@ var petFeederCapability = {
63953
63924
  },
63954
63925
  desiccantLeftDays: {
63955
63926
  kind: "gauge",
63956
- unit: "days"
63927
+ unit: "s"
63957
63928
  }
63958
63929
  } }
63959
63930
  };
@@ -64354,36 +64325,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64354
64325
  */
64355
64326
  var PowerMeterStatusSchema = object({
64356
64327
  /** Instantaneous power draw in watts. */
64357
- watts: number().optional().meta({
64358
- unit: "W",
64359
- label: "Power"
64360
- }),
64328
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64361
64329
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64362
- kwhTotal: number().optional().meta({
64363
- unit: "kWh",
64364
- label: "Energy"
64365
- }),
64330
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64366
64331
  /** Voltage in volts. */
64367
- volts: number().optional(),
64332
+ volts: number().optional().meta(readingMeta("voltage")),
64368
64333
  /** Current in amperes. */
64369
- amps: number().optional(),
64370
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64371
- va: number().optional(),
64334
+ amps: number().optional().meta(readingMeta("current")),
64335
+ /** Apparent power in volt-amperes. */
64336
+ va: number().optional().meta(readingMeta("apparent-power")),
64372
64337
  /** Power factor (0..1) if reported. */
64373
- powerFactor: number().min(0).max(1).optional(),
64338
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64374
64339
  /** Ms epoch when the slice was last updated. */
64375
- lastFetchedAt: number(),
64376
- /** Live display unit of the single metric this slice carries (e.g. HA
64377
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64378
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64379
- * unit per slice is unambiguous. The UI prefers this over the role's
64380
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64381
- unit: string().optional(),
64382
- /** Suggested decimal places for numeric display.
64383
- * Populated live from the upstream source when provided (e.g. HA
64384
- * `attributes.suggested_display_precision`). Falls back to
64385
- * auto-formatting when absent. */
64386
- precision: number().int().min(0).max(10).optional()
64340
+ lastFetchedAt: number()
64387
64341
  });
64388
64342
  var powerMeterCapability = {
64389
64343
  name: "power-meter",
@@ -64447,12 +64401,7 @@ var powerMeterCapability = {
64447
64401
  } },
64448
64402
  /** Clock fields: written, but excluded from the compare that decides
64449
64403
  * whether persisting is worth a SQLite commit. */
64450
- volatileStateFields: ["lastFetchedAt"],
64451
- customize: {
64452
- version: 1,
64453
- advanced: "derive",
64454
- targets: []
64455
- }
64404
+ volatileStateFields: ["lastFetchedAt"]
64456
64405
  };
64457
64406
  /**
64458
64407
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64472,11 +64421,11 @@ var powerMeterCapability = {
64472
64421
  */
64473
64422
  var GpsLocationSchema = object({
64474
64423
  /** Latitude in decimal degrees, -90..90. */
64475
- latitude: number().min(-90).max(90),
64424
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64476
64425
  /** Longitude in decimal degrees, -180..180. */
64477
- longitude: number().min(-180).max(180),
64426
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64478
64427
  /** Reported accuracy in meters (lower = better). */
64479
- accuracyMeters: number().nonnegative()
64428
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64480
64429
  });
64481
64430
  var PresenceStatusSchema = object({
64482
64431
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64488,7 +64437,7 @@ var PresenceStatusSchema = object({
64488
64437
  gps: GpsLocationSchema.nullable(),
64489
64438
  /** Optional battery level of the tracking device (0..100). Null
64490
64439
  * when not reported. */
64491
- batteryPercent: number().min(0).max(100).nullable(),
64440
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64492
64441
  /** Ms epoch when the slice was last updated. */
64493
64442
  lastChangedAt: number()
64494
64443
  });
@@ -64538,19 +64487,10 @@ var presenceCapability = {
64538
64487
  * `sourceInfo`.
64539
64488
  */
64540
64489
  var PressureSensorStatusSchema = object({
64541
- /** Current pressure in hPa. */
64542
- hpa: number(),
64490
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64491
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64543
64492
  /** Ms epoch when the slice was last updated. */
64544
- lastFetchedAt: number(),
64545
- /** Live display unit from the upstream source (e.g. HA
64546
- * `attributes.unit_of_measurement`). The UI prefers this over the
64547
- * role's canonical unit. Absent → fall back to the canonical unit. */
64548
- unit: string().optional(),
64549
- /** Suggested decimal places for numeric display.
64550
- * Populated live from the upstream source when provided (e.g. HA
64551
- * `attributes.suggested_display_precision`). Falls back to
64552
- * auto-formatting when absent. */
64553
- precision: number().int().min(0).max(10).optional()
64493
+ lastFetchedAt: number()
64554
64494
  });
64555
64495
  var pressureSensorCapability = {
64556
64496
  name: "pressure-sensor",
@@ -64964,8 +64904,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64964
64904
  }), _void(), { kind: "mutation" });
64965
64905
  var PtzAutotrackSettingsSchema = object({
64966
64906
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64967
- stopDelaySeconds: number().int().min(0).max(300),
64968
- 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"))
64969
64909
  });
64970
64910
  /**
64971
64911
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65127,7 +65067,7 @@ var RunningStateStatusSchema = object({
65127
65067
  /** null = unknown — never coerced to false (D393). */
65128
65068
  running: boolean().nullable(),
65129
65069
  /** Epoch ms the current boolean began; null while `running` is null. */
65130
- since: number().nullable(),
65070
+ since: number().nullable().meta(readingMeta("none")),
65131
65071
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65132
65072
  phase: string().nullable(),
65133
65073
  /** null until a reading has been published (D393). */
@@ -65142,7 +65082,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65142
65082
  /** Generic Customize target for the running boolean (D936). */
65143
65083
  var runningCustomize = {
65144
65084
  version: 1,
65145
- advanced: "derive",
65146
65085
  targets: [{
65147
65086
  id: "running",
65148
65087
  cardinality: "scalar",
@@ -65162,7 +65101,7 @@ var runningCustomize = {
65162
65101
  DeviceType.Thermostat,
65163
65102
  DeviceType.PetFeeder
65164
65103
  ],
65165
- apply: "add-or-replace",
65104
+ apply: "add",
65166
65105
  path: "running",
65167
65106
  sources: [
65168
65107
  {
@@ -65178,6 +65117,7 @@ var runningCustomize = {
65178
65117
  requiredUnit: "W",
65179
65118
  transforms: [{
65180
65119
  kind: "threshold",
65120
+ comparisons: ["gte", "lte"],
65181
65121
  comparison: "gte",
65182
65122
  defaultThreshold: 5,
65183
65123
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -66425,9 +66365,9 @@ var RecordTriggerSchema = _enum([
66425
66365
  */
66426
66366
  var RecordWindowSchema = object({
66427
66367
  trigger: RecordTriggerSchema,
66428
- day: number().int().min(0).max(6),
66429
- startMinute: number().int().min(0).max(1439),
66430
- 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"))
66431
66371
  });
66432
66372
  /** Status of one physical volume, as the camera itself describes it. */
66433
66373
  var OnboardStorageVolumeSchema = object({
@@ -66503,10 +66443,10 @@ var RecordingOnboardStatusSchema = object({
66503
66443
  /** Master "record to the card at all" switch. */
66504
66444
  enabled: boolean().nullable(),
66505
66445
  overwriteWhenFull: boolean().nullable(),
66506
- preRecordSec: number().nullable(),
66507
- postRecordSec: number().nullable(),
66446
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66447
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66508
66448
  /** Length of one recorded file, in minutes. */
66509
- segmentMinutes: number().nullable(),
66449
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66510
66450
  /** The primary track's weekly windows, flattened. */
66511
66451
  windows: array(RecordWindowSchema),
66512
66452
  /**
@@ -66522,7 +66462,7 @@ var RecordingOnboardStatusSchema = object({
66522
66462
  * provider reporting a non-zero count also reports the schedule as not
66523
66463
  * writable.
66524
66464
  */
66525
- unreadableWindows: number(),
66465
+ unreadableWindows: number().meta(readingMeta("count")),
66526
66466
  /**
66527
66467
  * The camera is scheduled to record and has NO usable storage.
66528
66468
  *
@@ -67367,7 +67307,7 @@ var CamStreamDescriptorSchema = object({
67367
67307
  url: string().optional(),
67368
67308
  codec: string().optional(),
67369
67309
  resolution: CamStreamResolutionSchema.optional(),
67370
- fps: number().positive().optional(),
67310
+ fps: number().positive().optional().meta(readingMeta("rate")),
67371
67311
  label: string().optional(),
67372
67312
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67373
67313
  deviceFeatures: array(string()).optional(),
@@ -67454,18 +67394,18 @@ var StreamProfileSchema = _enum([
67454
67394
  ]);
67455
67395
  /** The current encoding config of one stream profile. */
67456
67396
  var StreamProfileConfigSchema = object({
67457
- width: number(),
67458
- height: number(),
67397
+ width: number().meta(readingMeta("none")),
67398
+ height: number().meta(readingMeta("none")),
67459
67399
  codec: _enum(["h264", "h265"]),
67460
- framerate: number(),
67461
- bitrate: number(),
67400
+ framerate: number().meta(readingMeta("rate")),
67401
+ bitrate: number().meta(readingMeta("bitrate")),
67462
67402
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67463
67403
  encoderProfile: _enum([
67464
67404
  "high",
67465
67405
  "main",
67466
67406
  "baseline"
67467
67407
  ]).optional(),
67468
- gop: number().optional(),
67408
+ gop: number().optional().meta(readingMeta("none")),
67469
67409
  /**
67470
67410
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67471
67411
  *
@@ -67686,12 +67626,7 @@ var switchCapability = {
67686
67626
  label: "On",
67687
67627
  kind: "boolean"
67688
67628
  }
67689
- }] },
67690
- customize: {
67691
- version: 1,
67692
- advanced: "derive",
67693
- targets: []
67694
- }
67629
+ }] }
67695
67630
  };
67696
67631
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67697
67632
  function hasControlCharacter(value) {
@@ -68274,35 +68209,21 @@ var tamperCapability = {
68274
68209
  * entries with `device_class: temperature` and any future native
68275
68210
  * thermometer.
68276
68211
  *
68277
- * Unit normalisation: providers convert to Celsius before storing.
68278
- * The slice value is always Celsius so cross-cap aggregators
68279
- * (climate-control's `currentTemp`, energy analytics) can compose
68280
- * without per-source unit fixups. The canonical display unit (`°C`) is
68281
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68282
- * `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.
68283
68214
  *
68284
68215
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68285
68216
  * frozen feed (provider hung) distinct from a "temperature hasn't
68286
68217
  * changed" steady state.
68287
68218
  */
68288
68219
  var TemperatureSensorStatusSchema = object({
68289
- /** Current temperature in Celsius. */
68290
- celsius: number().meta({
68291
- unit: "°C",
68220
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68221
+ celsius: number().nullable().meta(readingMeta("temperature", {
68222
+ precision: 1,
68292
68223
  label: "Temperature"
68293
- }),
68224
+ })),
68294
68225
  /** Ms epoch when the slice was last updated (push or poll). */
68295
- lastFetchedAt: number(),
68296
- /** Live display unit from the upstream source (e.g. HA
68297
- * `attributes.unit_of_measurement`). The UI prefers this over the
68298
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68299
- * Absent → the UI falls back to the role's canonical unit. */
68300
- unit: string().optional(),
68301
- /** Suggested decimal places for numeric display.
68302
- * Populated live from the upstream source when provided (e.g. HA
68303
- * `attributes.suggested_display_precision`). Falls back to
68304
- * auto-formatting when absent. */
68305
- precision: number().int().min(0).max(10).optional()
68226
+ lastFetchedAt: number()
68306
68227
  });
68307
68228
  var temperatureSensorCapability = {
68308
68229
  name: "temperature-sensor",
@@ -68333,12 +68254,7 @@ var temperatureSensorCapability = {
68333
68254
  } } },
68334
68255
  /** Clock fields: written, but excluded from the compare that decides
68335
68256
  * whether persisting is worth a SQLite commit. */
68336
- volatileStateFields: ["lastFetchedAt"],
68337
- customize: {
68338
- version: 1,
68339
- advanced: "derive",
68340
- targets: []
68341
- }
68257
+ volatileStateFields: ["lastFetchedAt"]
68342
68258
  };
68343
68259
  /**
68344
68260
  * toast — system-scoped singleton capability that streams toast
@@ -68621,16 +68537,16 @@ var VacuumControlStatusSchema = object({
68621
68537
  ]),
68622
68538
  /** 0..100 battery percentage. Null when the device has no battery
68623
68539
  * reading. */
68624
- batteryLevel: number().min(0).max(100).nullable(),
68540
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68625
68541
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68626
68542
  * the vacuum has no speed control. */
68627
68543
  fanSpeed: string().nullable(),
68628
68544
  /** Speed tokens the hardware accepts — drives the UI selector. */
68629
68545
  availableFanSpeeds: array(string()),
68630
68546
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68631
- progressPercent: number().min(0).max(100).nullable(),
68547
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68632
68548
  /** Current error code (0 / null = no error). */
68633
- errorCode: number().nullable(),
68549
+ errorCode: number().nullable().meta(readingMeta("none")),
68634
68550
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68635
68551
  errorLabel: string().nullable(),
68636
68552
  /** Ms epoch when the slice was last updated. */
@@ -68708,7 +68624,7 @@ var ValveStatusSchema = object({
68708
68624
  ]),
68709
68625
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68710
68626
  * intermediate position surface. */
68711
- position: number().min(0).max(100).nullable(),
68627
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68712
68628
  /** Ms epoch when the slice was last updated. */
68713
68629
  lastChangedAt: number()
68714
68630
  });
@@ -68816,9 +68732,9 @@ var vibrationCapability = {
68816
68732
  */
68817
68733
  var WaterHeaterStatusSchema = object({
68818
68734
  /** Current measured temperature. Null when not reported. */
68819
- currentTemp: number().nullable(),
68735
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68820
68736
  /** Target temperature setpoint. Null when no setpoint surface. */
68821
- targetTemp: number().nullable(),
68737
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68822
68738
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68823
68739
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68824
68740
  * device reports an unknown state. */
@@ -68829,9 +68745,9 @@ var WaterHeaterStatusSchema = object({
68829
68745
  * has no away surface. */
68830
68746
  away: boolean().nullable(),
68831
68747
  /** HA `min_temp` attribute. Null when not reported. */
68832
- minTemp: number().nullable(),
68748
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68833
68749
  /** HA `max_temp` attribute. Null when not reported. */
68834
- maxTemp: number().nullable(),
68750
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68835
68751
  /** Ms epoch when the slice was last updated. */
68836
68752
  lastChangedAt: number()
68837
68753
  });
@@ -68916,25 +68832,18 @@ var waterHeaterCapability = {
68916
68832
  * Forecast deferred — current conditions only for v1.
68917
68833
  */
68918
68834
  var WeatherStatusSchema = object({
68919
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68920
- * when no condition has been reported yet. */
68835
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68921
68836
  condition: string().nullable(),
68922
- /** Current temperature in the reported unit. Null when not provided. */
68923
- temperature: number().nullable(),
68924
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68925
- temperatureUnit: string().nullable(),
68837
+ /** Current temperature in Celsius. Null when not provided or refused. */
68838
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68926
68839
  /** Relative humidity (0..100). Null when not provided. */
68927
- humidity: number().min(0).max(100).nullable(),
68928
- /** Barometric pressure in the reported unit. Null when not provided. */
68929
- pressure: number().nullable(),
68930
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68931
- pressureUnit: string().nullable(),
68932
- /** Wind speed in the reported unit. Null when not provided. */
68933
- windSpeed: number().nullable(),
68934
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68935
- 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")),
68936
68845
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68937
- windBearing: number().nullable(),
68846
+ windBearing: number().nullable().meta(readingMeta("angle")),
68938
68847
  /** Ms epoch when the slice was last updated. */
68939
68848
  lastFetchedAt: number()
68940
68849
  });
@@ -68962,12 +68871,12 @@ var weatherCapability = {
68962
68871
  */
68963
68872
  durability: "session",
68964
68873
  /**
68965
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68874
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68966
68875
  */
68967
68876
  history: { fields: {
68968
68877
  temperature: {
68969
68878
  kind: "gauge",
68970
- unitFrom: "temperatureUnit",
68879
+ unit: "°C",
68971
68880
  deadband: .2
68972
68881
  },
68973
68882
  humidity: {
@@ -68977,12 +68886,12 @@ var weatherCapability = {
68977
68886
  },
68978
68887
  pressure: {
68979
68888
  kind: "gauge",
68980
- unitFrom: "pressureUnit",
68889
+ unit: "hPa",
68981
68890
  deadband: .5
68982
68891
  },
68983
68892
  windSpeed: {
68984
68893
  kind: "gauge",
68985
- unitFrom: "windSpeedUnit",
68894
+ unit: "m/s",
68986
68895
  deadband: .5
68987
68896
  }
68988
68897
  } }
@@ -69920,6 +69829,404 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
69920
69829
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
69921
69830
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
69922
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
+ }
69923
70230
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69924
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;
69925
70232
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -84589,7 +84896,6 @@ var SENSOR_CAP_VOCABULARY = {
84589
84896
  field: "value",
84590
84897
  quantity: t("the reading", "il valore"),
84591
84898
  label: t("Reading", "Valore"),
84592
- unitField: "unit",
84593
84899
  icon: "gauge"
84594
84900
  }]
84595
84901
  },
@@ -84725,7 +85031,7 @@ var SENSOR_CAP_VOCABULARY = {
84725
85031
  field: "temperature",
84726
85032
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84727
85033
  label: t("Temperature", "Temperatura"),
84728
- unitField: "temperatureUnit",
85034
+ unit: "°C",
84729
85035
  icon: "thermometer"
84730
85036
  },
84731
85037
  {
@@ -84741,7 +85047,7 @@ var SENSOR_CAP_VOCABULARY = {
84741
85047
  field: "windSpeed",
84742
85048
  quantity: t("the wind speed", "la velocità del vento"),
84743
85049
  label: t("Wind", "Vento"),
84744
- unitField: "windSpeedUnit",
85050
+ unit: "m/s",
84745
85051
  icon: "wind"
84746
85052
  },
84747
85053
  {
@@ -84749,7 +85055,7 @@ var SENSOR_CAP_VOCABULARY = {
84749
85055
  field: "pressure",
84750
85056
  quantity: t("the air pressure", "la pressione atmosferica"),
84751
85057
  label: t("Pressure", "Pressione"),
84752
- unitField: "pressureUnit",
85058
+ unit: "hPa",
84753
85059
  icon: "gauge"
84754
85060
  }
84755
85061
  ]
@@ -87886,6 +88192,11 @@ var PulseOverrideDraftSchema = object({
87886
88192
  targetId: string().min(1),
87887
88193
  enabled: boolean(),
87888
88194
  debounceMs: number().nonnegative(),
88195
+ edges: array(_enum([
88196
+ "rising",
88197
+ "button",
88198
+ "native-event"
88199
+ ])).min(1).optional(),
87889
88200
  sources: array(OverrideSourceSchema).min(1)
87890
88201
  });
87891
88202
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -89317,6 +89628,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
89317
89628
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
89318
89629
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
89319
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
+ /**
89320
89721
  * Pure fuzzy matcher for adoption location import. Normalized
89321
89722
  * case-insensitive Levenshtein similarity, used to reuse an existing
89322
89723
  * CamStack location when an imported source location (e.g. an HA area) is
@@ -89545,6 +89946,12 @@ Object.defineProperty(exports, "automationControlCapability", {
89545
89946
  return automationControlCapability;
89546
89947
  }
89547
89948
  });
89949
+ Object.defineProperty(exports, "baseUnitOf", {
89950
+ enumerable: true,
89951
+ get: function() {
89952
+ return baseUnitOf;
89953
+ }
89954
+ });
89548
89955
  Object.defineProperty(exports, "batteryCapability", {
89549
89956
  enumerable: true,
89550
89957
  get: function() {
@@ -89761,6 +90168,18 @@ Object.defineProperty(exports, "imageCapability", {
89761
90168
  return imageCapability;
89762
90169
  }
89763
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
+ });
89764
90183
  Object.defineProperty(exports, "lawnMowerControlCapability", {
89765
90184
  enumerable: true,
89766
90185
  get: function() {
@@ -89869,6 +90288,12 @@ Object.defineProperty(exports, "pressureSensorCapability", {
89869
90288
  return pressureSensorCapability;
89870
90289
  }
89871
90290
  });
90291
+ Object.defineProperty(exports, "quantityFromUnit", {
90292
+ enumerable: true,
90293
+ get: function() {
90294
+ return quantityFromUnit;
90295
+ }
90296
+ });
89872
90297
  Object.defineProperty(exports, "record", {
89873
90298
  enumerable: true,
89874
90299
  get: function() {
@@ -89935,6 +90360,18 @@ Object.defineProperty(exports, "temperatureSensorCapability", {
89935
90360
  return temperatureSensorCapability;
89936
90361
  }
89937
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
+ });
89938
90375
  Object.defineProperty(exports, "union", {
89939
90376
  enumerable: true,
89940
90377
  get: function() {