@camstack/addon-provider-ecowitt 0.2.146 → 0.2.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +994 -543
  2. package/dist/addon.mjs +994 -543
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5437,7 +5437,7 @@ var ZodIssueCode$1 = {
5437
5437
  var ZodFirstPartyTypeKind$1;
5438
5438
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5439
5439
  //#endregion
5440
- //#region ../types/dist/sleep-BErAxhul.mjs
5440
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5441
5441
  /**
5442
5442
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5443
5443
  * window to float samples (D455).
@@ -7991,6 +7991,99 @@ var RecordKindDeclarationSchema = object({
7991
7991
  }).strict();
7992
7992
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7993
7993
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7994
+ var READING_QUANTITIES = [
7995
+ "temperature",
7996
+ "pressure",
7997
+ "speed",
7998
+ "precipitation",
7999
+ "precipitation-rate",
8000
+ "length",
8001
+ "area",
8002
+ "illuminance",
8003
+ "irradiance",
8004
+ "power",
8005
+ "energy",
8006
+ "voltage",
8007
+ "current",
8008
+ "apparent-power",
8009
+ "humidity",
8010
+ "percentage",
8011
+ "concentration-volume",
8012
+ "concentration-mass",
8013
+ "concentration-ppb",
8014
+ "volume",
8015
+ "duration",
8016
+ "duration-ms",
8017
+ "information",
8018
+ "sound-level",
8019
+ "signal-level",
8020
+ "colour-temperature",
8021
+ "angle",
8022
+ "bitrate",
8023
+ "rate",
8024
+ "mass",
8025
+ "index",
8026
+ "ratio",
8027
+ "count",
8028
+ "none",
8029
+ "device"
8030
+ ];
8031
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8032
+ new Set(READING_QUANTITIES);
8033
+ /**
8034
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8035
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8036
+ */
8037
+ var QUANTITY_BASE = {
8038
+ temperature: "°C",
8039
+ pressure: "hPa",
8040
+ speed: "m/s",
8041
+ precipitation: "mm",
8042
+ "precipitation-rate": "mm/h",
8043
+ length: "m",
8044
+ area: "m²",
8045
+ illuminance: "lx",
8046
+ irradiance: "W/m²",
8047
+ power: "W",
8048
+ energy: "kWh",
8049
+ voltage: "V",
8050
+ current: "A",
8051
+ "apparent-power": "VA",
8052
+ humidity: "%",
8053
+ percentage: "%",
8054
+ "concentration-volume": "ppm",
8055
+ "concentration-mass": "µg/m³",
8056
+ "concentration-ppb": "ppb",
8057
+ volume: "L",
8058
+ duration: "s",
8059
+ "duration-ms": "ms",
8060
+ information: "B",
8061
+ "sound-level": "dBFS",
8062
+ "signal-level": "dBm",
8063
+ "colour-temperature": "mired",
8064
+ angle: "°",
8065
+ bitrate: "kbps",
8066
+ rate: "fps",
8067
+ mass: "kg",
8068
+ index: "none",
8069
+ ratio: "none",
8070
+ count: "none",
8071
+ none: "none",
8072
+ device: "device"
8073
+ };
8074
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8075
+ function baseUnitOf(quantity) {
8076
+ const symbol = QUANTITY_BASE[quantity];
8077
+ if (symbol === "none" || symbol === "device") return void 0;
8078
+ return symbol;
8079
+ }
8080
+ function readingMeta(quantity, extras) {
8081
+ return {
8082
+ quantity,
8083
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8084
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8085
+ };
8086
+ }
7994
8087
  /** Shared Zod schemas used across streaming capabilities. */
7995
8088
  var CamProfileSchema = _enum([
7996
8089
  "high",
@@ -8018,8 +8111,8 @@ var CamStreamKindSchema = _enum([
8018
8111
  "derived"
8019
8112
  ]);
8020
8113
  var CamStreamResolutionSchema = object({
8021
- width: number().int().positive(),
8022
- height: number().int().positive()
8114
+ width: number().int().positive().meta(readingMeta("none")),
8115
+ height: number().int().positive().meta(readingMeta("none"))
8023
8116
  });
8024
8117
  var CameraStreamSchema = object({
8025
8118
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8666,14 +8759,6 @@ Object.freeze({
8666
8759
  {
8667
8760
  "path": "aqi",
8668
8761
  "valueType": "number"
8669
- },
8670
- {
8671
- "path": "unit",
8672
- "valueType": "string"
8673
- },
8674
- {
8675
- "path": "precision",
8676
- "valueType": "number"
8677
8762
  }
8678
8763
  ],
8679
8764
  "alarm-panel": [
@@ -8709,20 +8794,10 @@ Object.freeze({
8709
8794
  "valueType": "boolean"
8710
8795
  }
8711
8796
  ],
8712
- "ambient-light-sensor": [
8713
- {
8714
- "path": "lux",
8715
- "valueType": "number"
8716
- },
8717
- {
8718
- "path": "unit",
8719
- "valueType": "string"
8720
- },
8721
- {
8722
- "path": "precision",
8723
- "valueType": "number"
8724
- }
8725
- ],
8797
+ "ambient-light-sensor": [{
8798
+ "path": "lux",
8799
+ "valueType": "number"
8800
+ }],
8726
8801
  "audio-metrics": [
8727
8802
  {
8728
8803
  "path": "ts",
@@ -8802,6 +8877,10 @@ Object.freeze({
8802
8877
  "path": "lastUpdated",
8803
8878
  "valueType": "number"
8804
8879
  },
8880
+ {
8881
+ "path": "volts",
8882
+ "valueType": "number"
8883
+ },
8805
8884
  {
8806
8885
  "path": "lastContactAt",
8807
8886
  "valueType": "number"
@@ -9044,10 +9123,6 @@ Object.freeze({
9044
9123
  "datetime"
9045
9124
  ]
9046
9125
  },
9047
- {
9048
- "path": "unit",
9049
- "valueType": "string"
9050
- },
9051
9126
  {
9052
9127
  "path": "min",
9053
9128
  "valueType": "number"
@@ -9059,19 +9134,6 @@ Object.freeze({
9059
9134
  {
9060
9135
  "path": "step",
9061
9136
  "valueType": "number"
9062
- },
9063
- {
9064
- "path": "precision",
9065
- "valueType": "number"
9066
- },
9067
- {
9068
- "path": "format",
9069
- "valueType": "enum",
9070
- "enumValues": [
9071
- "date",
9072
- "time",
9073
- "datetime"
9074
- ]
9075
9137
  }
9076
9138
  ],
9077
9139
  "cover": [
@@ -9312,13 +9374,8 @@ Object.freeze({
9312
9374
  "path": "value",
9313
9375
  "valueType": "string"
9314
9376
  }, {
9315
- "path": "format",
9316
- "valueType": "enum",
9317
- "enumValues": [
9318
- "date",
9319
- "time",
9320
- "datetime"
9321
- ]
9377
+ "path": "timestamp",
9378
+ "valueType": "boolean"
9322
9379
  }],
9323
9380
  "event-emitter": [
9324
9381
  {
@@ -9425,20 +9482,10 @@ Object.freeze({
9425
9482
  "valueType": "number"
9426
9483
  }
9427
9484
  ],
9428
- "humidity-sensor": [
9429
- {
9430
- "path": "percent",
9431
- "valueType": "number"
9432
- },
9433
- {
9434
- "path": "unit",
9435
- "valueType": "string"
9436
- },
9437
- {
9438
- "path": "precision",
9439
- "valueType": "number"
9440
- }
9441
- ],
9485
+ "humidity-sensor": [{
9486
+ "path": "percent",
9487
+ "valueType": "number"
9488
+ }],
9442
9489
  "image": [{
9443
9490
  "path": "url",
9444
9491
  "valueType": "string"
@@ -9777,20 +9824,10 @@ Object.freeze({
9777
9824
  "valueType": "boolean"
9778
9825
  }
9779
9826
  ],
9780
- "numeric-sensor": [
9781
- {
9782
- "path": "value",
9783
- "valueType": "number"
9784
- },
9785
- {
9786
- "path": "unit",
9787
- "valueType": "string"
9788
- },
9789
- {
9790
- "path": "precision",
9791
- "valueType": "number"
9792
- }
9793
- ],
9827
+ "numeric-sensor": [{
9828
+ "path": "value",
9829
+ "valueType": "number"
9830
+ }],
9794
9831
  "pet-feeder": [
9795
9832
  {
9796
9833
  "path": "foodLevel",
@@ -9882,14 +9919,6 @@ Object.freeze({
9882
9919
  {
9883
9920
  "path": "powerFactor",
9884
9921
  "valueType": "number"
9885
- },
9886
- {
9887
- "path": "unit",
9888
- "valueType": "string"
9889
- },
9890
- {
9891
- "path": "precision",
9892
- "valueType": "number"
9893
9922
  }
9894
9923
  ],
9895
9924
  "presence": [
@@ -9918,20 +9947,10 @@ Object.freeze({
9918
9947
  "valueType": "number"
9919
9948
  }
9920
9949
  ],
9921
- "pressure-sensor": [
9922
- {
9923
- "path": "hpa",
9924
- "valueType": "number"
9925
- },
9926
- {
9927
- "path": "unit",
9928
- "valueType": "string"
9929
- },
9930
- {
9931
- "path": "precision",
9932
- "valueType": "number"
9933
- }
9934
- ],
9950
+ "pressure-sensor": [{
9951
+ "path": "hpa",
9952
+ "valueType": "number"
9953
+ }],
9935
9954
  "privacy-mask": [{
9936
9955
  "path": "enabled",
9937
9956
  "valueType": "boolean"
@@ -10242,20 +10261,10 @@ Object.freeze({
10242
10261
  "path": "tampered",
10243
10262
  "valueType": "boolean"
10244
10263
  }],
10245
- "temperature-sensor": [
10246
- {
10247
- "path": "celsius",
10248
- "valueType": "number"
10249
- },
10250
- {
10251
- "path": "unit",
10252
- "valueType": "string"
10253
- },
10254
- {
10255
- "path": "precision",
10256
- "valueType": "number"
10257
- }
10258
- ],
10264
+ "temperature-sensor": [{
10265
+ "path": "celsius",
10266
+ "valueType": "number"
10267
+ }],
10259
10268
  "update": [
10260
10269
  {
10261
10270
  "path": "currentVersion",
@@ -10366,10 +10375,6 @@ Object.freeze({
10366
10375
  "path": "temperature",
10367
10376
  "valueType": "number"
10368
10377
  },
10369
- {
10370
- "path": "temperatureUnit",
10371
- "valueType": "string"
10372
- },
10373
10378
  {
10374
10379
  "path": "humidity",
10375
10380
  "valueType": "number"
@@ -10378,18 +10383,10 @@ Object.freeze({
10378
10383
  "path": "pressure",
10379
10384
  "valueType": "number"
10380
10385
  },
10381
- {
10382
- "path": "pressureUnit",
10383
- "valueType": "string"
10384
- },
10385
10386
  {
10386
10387
  "path": "windSpeed",
10387
10388
  "valueType": "number"
10388
10389
  },
10389
- {
10390
- "path": "windSpeedUnit",
10391
- "valueType": "string"
10392
- },
10393
10390
  {
10394
10391
  "path": "windBearing",
10395
10392
  "valueType": "number"
@@ -17867,7 +17864,8 @@ var CompositionProblemSchema = object({
17867
17864
  "item-field-reserved",
17868
17865
  "target-missing",
17869
17866
  "target-unreadable",
17870
- "not-replaceable"
17867
+ "not-replaceable",
17868
+ "native-capability-replace"
17871
17869
  ]),
17872
17870
  path: string(),
17873
17871
  message: string()
@@ -17925,9 +17923,11 @@ var CompositionFieldPlanSchema = object({
17925
17923
  reads: array(CompositionFieldReadSchema)
17926
17924
  });
17927
17925
  /**
17928
- * What a feature does to its target (D663): `new` builds a capability on a new
17929
- * device; on an existing device it `add`s a capability the device lacks, or
17930
- * `replace`s named fields of one it has natively.
17926
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17927
+ * new device; on an existing device it `add`s a capability the device lacks.
17928
+ * Replacing a capability the device already serves is refused
17929
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17930
+ * wire for a claim the hub already holds.
17931
17931
  */
17932
17932
  var CompositionFeatureModeSchema = _enum([
17933
17933
  "new",
@@ -19926,12 +19926,12 @@ function buildDeviceAdminUrl(parts) {
19926
19926
  * providers detect renames by matching `uniqueId` and patch `id`
19927
19927
  * in-place so dispatch keeps working.
19928
19928
  *
19929
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19930
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19931
- * state push and are the single source of truth. The `device_class` tag is
19932
- * an HA-specific input that is consumed at the discovery boundary
19933
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19934
- * stored after that point.
19929
+ * **Display unit and precision are NOT stored here.** They are a read-time
19930
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
19931
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
19932
+ * neither is repeated on each sample. The `device_class` tag is an
19933
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
19934
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19935
19935
  *
19936
19936
  * The `raw` escape hatch carries provider-specific attributes that don't
19937
19937
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19945,6 +19945,14 @@ var SourceInfoSchema = object({
19945
19945
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19946
19946
  * Used to detect rename when `id` changes. */
19947
19947
  uniqueId: string().optional(),
19948
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
19949
+ quantity: ReadingQuantitySchema.optional(),
19950
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
19951
+ dateTimeShape: _enum([
19952
+ "date",
19953
+ "time",
19954
+ "datetime"
19955
+ ]).optional(),
19948
19956
  /** Provider-specific extras that don't fit the structured slots.
19949
19957
  * Whitelist what goes here per provider — do NOT dump entire HA
19950
19958
  * attribute blobs (would explode the meta JSON column). */
@@ -19990,6 +19998,80 @@ function mergeSourceInfo(prev, patch) {
19990
19998
  return next;
19991
19999
  }
19992
20000
  /**
20001
+ * Coerce common unit-of-measurement string variants to a canonical form
20002
+ * so the UI's display templates can rely on stable strings. Pure
20003
+ * passthrough for unknown units — kept conservative on purpose, the goal
20004
+ * is to normalise the long tail of HA `unit_of_measurement` values that
20005
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
20006
+ *
20007
+ * Extend the map as we encounter real-world data. Returning the raw
20008
+ * input for unknown values is intentional: providers should never
20009
+ * lose information silently here.
20010
+ */
20011
+ function normalizeUnit(raw) {
20012
+ if (raw == null) return void 0;
20013
+ const trimmed = raw.trim();
20014
+ if (trimmed === "") return void 0;
20015
+ const lower = trimmed.toLowerCase();
20016
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
20017
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
20018
+ if (lower === "k" || lower === "kelvin") return "K";
20019
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
20020
+ if (lower === "lx" || lower === "lux") return "lx";
20021
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
20022
+ if (lower === "kpa") return "kPa";
20023
+ if (lower === "pa") return "Pa";
20024
+ if (lower === "mbar" || lower === "millibar") return "mbar";
20025
+ if (lower === "w" || lower === "watt") return "W";
20026
+ if (lower === "kw" || lower === "kilowatt") return "kW";
20027
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
20028
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
20029
+ if (lower === "v" || lower === "volt") return "V";
20030
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
20031
+ if (lower === "ma" || lower === "milliampere") return "mA";
20032
+ if (lower === "va") return "VA";
20033
+ if (lower === "ppm") return "ppm";
20034
+ if (lower === "ppb") return "ppb";
20035
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
20036
+ if (lower === "km/h" || lower === "kph") return "km/h";
20037
+ if (lower === "mph") return "mph";
20038
+ if (lower === "m/s") return "m/s";
20039
+ if (lower === "m") return "m";
20040
+ if (lower === "m²" || lower === "m2") return "m²";
20041
+ if (lower === "mm") return "mm";
20042
+ if (lower === "cm") return "cm";
20043
+ if (lower === "km") return "km";
20044
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
20045
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
20046
+ if (lower === "kg") return "kg";
20047
+ if (lower === "g") return "g";
20048
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
20049
+ if (lower === "ml") return "mL";
20050
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
20051
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
20052
+ if (lower === "ms") return "ms";
20053
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
20054
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
20055
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
20056
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
20057
+ if (lower === "kib") return "KiB";
20058
+ if (lower === "mib") return "MiB";
20059
+ if (lower === "gib") return "GiB";
20060
+ if (lower === "tib") return "TiB";
20061
+ if (lower === "kb") return "kB";
20062
+ if (lower === "mb") return "MB";
20063
+ if (lower === "gb") return "GB";
20064
+ if (lower === "dbm") return "dBm";
20065
+ if (lower === "dbfs") return "dBFS";
20066
+ if (lower === "inhg") return "inHg";
20067
+ if (lower === "mm/h") return "mm/h";
20068
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
20069
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
20070
+ if (lower === "kbps") return "kbps";
20071
+ if (lower === "fps") return "fps";
20072
+ return trimmed;
20073
+ }
20074
+ /**
19993
20075
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
19994
20076
  * blob is a free-form `Record<string, unknown>` shared with hardware-
19995
20077
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -41049,7 +41131,8 @@ method(object({
41049
41131
  "enum"
41050
41132
  ]),
41051
41133
  enumValues: array(string()).optional(),
41052
- item: boolean().optional()
41134
+ item: boolean().optional(),
41135
+ quantity: ReadingQuantitySchema.optional()
41053
41136
  })).readonly(),
41054
41137
  itemArray: object({
41055
41138
  path: string(),
@@ -44893,8 +44976,8 @@ var MaskPolygonShapeSchema = object({
44893
44976
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44894
44977
  var MaskGridShapeSchema = object({
44895
44978
  kind: literal("grid"),
44896
- gridWidth: number(),
44897
- gridHeight: number(),
44979
+ gridWidth: number().meta(readingMeta("none")),
44980
+ gridHeight: number().meta(readingMeta("none")),
44898
44981
  cells: array(boolean())
44899
44982
  });
44900
44983
  discriminatedUnion("kind", [
@@ -56374,27 +56457,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56374
56457
  */
56375
56458
  var AirQualitySensorStatusSchema = object({
56376
56459
  /** Carbon dioxide concentration in ppm. */
56377
- co2Ppm: number().min(0).optional(),
56460
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56378
56461
  /** Total volatile organic compounds in ppb. */
56379
- vocPpb: number().min(0).optional(),
56462
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56380
56463
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56381
- pm25: number().min(0).optional(),
56464
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56382
56465
  /** Particulate matter ≤ 10 μm in µg/m³. */
56383
- pm10: number().min(0).optional(),
56466
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56384
56467
  /** Composite AQI value (typically 0..500). */
56385
- aqi: number().optional(),
56468
+ aqi: number().optional().meta(readingMeta("index")),
56386
56469
  /** Ms epoch when the slice was last updated. */
56387
- lastFetchedAt: number(),
56388
- /** Live display unit of the single metric this slice carries (e.g. HA
56389
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56390
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56391
- * per slice is unambiguous. */
56392
- unit: string().optional(),
56393
- /** Suggested decimal places for numeric display.
56394
- * Populated live from the upstream source when provided (e.g. HA
56395
- * `attributes.suggested_display_precision`). Falls back to
56396
- * auto-formatting when absent. */
56397
- precision: number().int().min(0).max(10).optional()
56470
+ lastFetchedAt: number()
56398
56471
  });
56399
56472
  var airQualitySensorCapability = {
56400
56473
  name: "air-quality-sensor",
@@ -56431,12 +56504,12 @@ var airQualitySensorCapability = {
56431
56504
  },
56432
56505
  pm25: {
56433
56506
  kind: "gauge",
56434
- unit: "ug/m3",
56507
+ unit: "µg/m³",
56435
56508
  deadband: 1
56436
56509
  },
56437
56510
  pm10: {
56438
56511
  kind: "gauge",
56439
- unit: "ug/m3",
56512
+ unit: "µg/m³",
56440
56513
  deadband: 1
56441
56514
  },
56442
56515
  aqi: {
@@ -56454,22 +56527,13 @@ var airQualitySensorCapability = {
56454
56527
  * entries with `device_class: illuminance`.
56455
56528
  */
56456
56529
  var AmbientLightSensorStatusSchema = object({
56457
- /** Current illuminance in lux (lx). */
56458
- lux: number().min(0).meta({
56459
- unit: "lx",
56530
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56531
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56532
+ precision: 0,
56460
56533
  label: "Illuminance"
56461
- }),
56534
+ })),
56462
56535
  /** Ms epoch when the slice was last updated. */
56463
- lastFetchedAt: number(),
56464
- /** Live display unit from the upstream source (e.g. HA
56465
- * `attributes.unit_of_measurement`). The UI prefers this over the
56466
- * role's canonical unit. Absent → fall back to the canonical unit. */
56467
- unit: string().optional(),
56468
- /** Suggested decimal places for numeric display.
56469
- * Populated live from the upstream source when provided (e.g. HA
56470
- * `attributes.suggested_display_precision`). Falls back to
56471
- * auto-formatting when absent. */
56472
- precision: number().int().min(0).max(10).optional()
56536
+ lastFetchedAt: number()
56473
56537
  });
56474
56538
  var ambientLightSensorCapability = {
56475
56539
  name: "ambient-light-sensor",
@@ -56501,12 +56565,7 @@ var ambientLightSensorCapability = {
56501
56565
  } } },
56502
56566
  /** Clock fields: written, but excluded from the compare that decides
56503
56567
  * whether persisting is worth a SQLite commit. */
56504
- volatileStateFields: ["lastFetchedAt"],
56505
- customize: {
56506
- version: 1,
56507
- advanced: "derive",
56508
- targets: []
56509
- }
56568
+ volatileStateFields: ["lastFetchedAt"]
56510
56569
  };
56511
56570
  /**
56512
56571
  * Per-class audio metrics aggregated over a sliding window.
@@ -56733,10 +56792,7 @@ var BatteryStatusSchema = object({
56733
56792
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56734
56793
  * declines to band a non-finite reading, which is the correct shape.
56735
56794
  */
56736
- percentage: number().min(0).max(100).nullable().meta({
56737
- unit: "%",
56738
- label: "Charge"
56739
- }),
56795
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56740
56796
  /**
56741
56797
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56742
56798
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56763,7 +56819,14 @@ var BatteryStatusSchema = object({
56763
56819
  */
56764
56820
  sleeping: boolean().meta({ label: "Sleeping" }),
56765
56821
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56766
- lastUpdated: number(),
56822
+ lastUpdated: number().meta(readingMeta("none")),
56823
+ /**
56824
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56825
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56826
+ * that only report charge. A second reading on this cap, not a second
56827
+ * `numeric-sensor` on the same device.
56828
+ */
56829
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56767
56830
  /**
56768
56831
  * Ms epoch of the last time the device PROVED it was reachable — a
56769
56832
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56815,7 +56878,6 @@ var BATTERY_DEVICE_TYPES = [
56815
56878
  /** Generic Customize target for the percentage reading (D936). */
56816
56879
  var batteryCustomize = {
56817
56880
  version: 1,
56818
- advanced: "derive",
56819
56881
  targets: [{
56820
56882
  id: "percentage",
56821
56883
  cardinality: "scalar",
@@ -56824,7 +56886,7 @@ var batteryCustomize = {
56824
56886
  stateField: "percentage"
56825
56887
  },
56826
56888
  deviceTypes: BATTERY_DEVICE_TYPES,
56827
- apply: "add-or-replace",
56889
+ apply: "add",
56828
56890
  path: "percentage",
56829
56891
  sources: [{
56830
56892
  kind: "field",
@@ -56945,10 +57007,16 @@ onStatusChanged: { data: object({
56945
57007
  /**
56946
57008
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56947
57009
  */
56948
- history: { fields: { percentage: {
56949
- kind: "gauge",
56950
- unit: "%"
56951
- } } },
57010
+ history: { fields: {
57011
+ percentage: {
57012
+ kind: "gauge",
57013
+ unit: "%"
57014
+ },
57015
+ volts: {
57016
+ kind: "gauge",
57017
+ unit: "V"
57018
+ }
57019
+ } },
56952
57020
  customize: batteryCustomize,
56953
57021
  /** Clock fields: written, but excluded from the compare that decides
56954
57022
  * whether persisting is worth a SQLite commit. */
@@ -56995,12 +57063,7 @@ var binaryCapability = {
56995
57063
  history: { fields: { on: { kind: "boolean" } } },
56996
57064
  /** Clock fields: written, but excluded from the compare that decides
56997
57065
  * whether persisting is worth a SQLite commit. */
56998
- volatileStateFields: ["lastChangedAt"],
56999
- customize: {
57000
- version: 1,
57001
- advanced: "derive",
57002
- targets: []
57003
- }
57066
+ volatileStateFields: ["lastChangedAt"]
57004
57067
  };
57005
57068
  /**
57006
57069
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57016,7 +57079,7 @@ var binaryCapability = {
57016
57079
  */
57017
57080
  var BrightnessStatusSchema = object({
57018
57081
  /** Current level as 0..100 inclusive. Firmware-reported. */
57019
- percentage: number().min(0).max(100),
57082
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57020
57083
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57021
57084
  lastChangedAt: number()
57022
57085
  });
@@ -57470,21 +57533,21 @@ var ClimateControlStatusSchema = object({
57470
57533
  /** Single setpoint in Celsius. Used by single-target modes
57471
57534
  * (heat / cool / dry). Null when the device is in a range mode
57472
57535
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57473
- target: number().nullable(),
57536
+ target: number().nullable().meta(readingMeta("temperature")),
57474
57537
  /** Upper bound of the dual setpoint in Celsius. Populated only
57475
57538
  * in `heat_cool` mode. */
57476
- targetHigh: number().nullable(),
57539
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57477
57540
  /** Lower bound of the dual setpoint in Celsius. Populated only
57478
57541
  * in `heat_cool` mode. */
57479
- targetLow: number().nullable(),
57542
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57480
57543
  /** Target relative humidity (0..100). Null when the device has no
57481
57544
  * humidity control. */
57482
- targetHumidity: number().min(0).max(100).nullable(),
57545
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57483
57546
  /** Current measured temperature in Celsius. Read-only — pushed by
57484
57547
  * the device's internal sensor. */
57485
- currentTemp: number().nullable(),
57548
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57486
57549
  /** Current measured relative humidity (0..100). Read-only. */
57487
- currentHumidity: number().min(0).max(100).nullable(),
57550
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57488
57551
  /** Vertical louver swing on/off. Null when the device has no vertical
57489
57552
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57490
57553
  swingVertical: boolean().nullable(),
@@ -57492,11 +57555,11 @@ var ClimateControlStatusSchema = object({
57492
57555
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57493
57556
  swingHorizontal: boolean().nullable(),
57494
57557
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57495
- minTemp: number().nullable().optional(),
57558
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57496
57559
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57497
- maxTemp: number().nullable().optional(),
57560
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57498
57561
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57499
- tempStep: number().positive().nullable().optional(),
57562
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57500
57563
  /** Ms epoch when the slice was last updated (push or command). */
57501
57564
  lastFetchedAt: number()
57502
57565
  });
@@ -57638,17 +57701,17 @@ var climateControlCapability = {
57638
57701
  * modes to expose in the picker without a slice fetch.
57639
57702
  */
57640
57703
  var RgbTripletSchema = object({
57641
- r: number().int().min(0).max(255),
57642
- g: number().int().min(0).max(255),
57643
- b: number().int().min(0).max(255)
57704
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57705
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57706
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57644
57707
  });
57645
57708
  var HsvTripletSchema = object({
57646
57709
  /** Hue in degrees, 0..360. */
57647
- h: number().min(0).max(360),
57710
+ h: number().min(0).max(360).meta(readingMeta("none")),
57648
57711
  /** Saturation as 0..100 inclusive. */
57649
- s: number().min(0).max(100),
57712
+ s: number().min(0).max(100).meta(readingMeta("none")),
57650
57713
  /** Value/brightness as 0..100 inclusive. */
57651
- v: number().min(0).max(100)
57714
+ v: number().min(0).max(100).meta(readingMeta("none"))
57652
57715
  });
57653
57716
  var ColorInputSchema = discriminatedUnion("mode", [
57654
57717
  object({
@@ -57677,7 +57740,7 @@ var ColorStatusSchema = object({
57677
57740
  /** Populated when `mode === 'hsv'`. */
57678
57741
  hsv: HsvTripletSchema.optional(),
57679
57742
  /** Populated when `mode === 'mired'`. */
57680
- mireds: number().int().optional(),
57743
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57681
57744
  /** Ms epoch of the last operator-driven change. */
57682
57745
  lastChangedAt: number()
57683
57746
  });
@@ -57872,18 +57935,12 @@ _enum([
57872
57935
  "empty",
57873
57936
  "full"
57874
57937
  ]);
57875
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57876
- * each. */
57877
- var ConsumableRemainingUnitSchema = _enum([
57878
- "days",
57879
- "hours",
57880
- "cycles"
57881
- ]);
57882
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57883
- * days). `value` null = unknown (D393). */
57938
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57939
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57940
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57884
57941
  var ConsumableRemainingSchema = object({
57885
- value: number().min(0).nullable(),
57886
- unit: ConsumableRemainingUnitSchema
57942
+ value: number().min(0).nullable().meta(readingMeta("device")),
57943
+ kind: ConsumableRemainingKindSchema
57887
57944
  });
57888
57945
  /** A single consumable item. Either a continuous `level` (remaining
57889
57946
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57896,7 +57953,7 @@ var ConsumableItemSchema = object({
57896
57953
  /** Display name. */
57897
57954
  label: string().min(1),
57898
57955
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57899
- level: number().min(0).max(100).nullable(),
57956
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57900
57957
  /** The provider's discrete verdict when known (binary mode). */
57901
57958
  status: ConsumableStatusSchema.nullable(),
57902
57959
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57978,8 +58035,8 @@ var consumablesCustomize = {
57978
58035
  transforms: [{ kind: "identity" }]
57979
58036
  }],
57980
58037
  literals: [{
57981
- path: "remaining.unit",
57982
- values: [...ConsumableRemainingUnitSchema.options]
58038
+ path: "remaining.kind",
58039
+ values: [...ConsumableRemainingKindSchema.options]
57983
58040
  }]
57984
58041
  },
57985
58042
  {
@@ -58108,7 +58165,6 @@ reset: method(object({
58108
58165
  },
58109
58166
  "remaining.value": {
58110
58167
  kind: "gauge",
58111
- unitFrom: "remaining.unit",
58112
58168
  deadband: 1,
58113
58169
  displayName: "Remaining"
58114
58170
  }
@@ -58178,10 +58234,9 @@ var contactCapability = {
58178
58234
  * (empty array for the other kinds). It lives in the slice because
58179
58235
  * HA `select.*` entities may update their options at runtime.
58180
58236
  *
58181
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58182
- * `kind === 'numeric'` and are absent (undefined) for all other
58183
- * kinds. They are populated live from HA attributes on each state
58184
- * push — consistent with `options` and the sensor-unit approach.
58237
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58238
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58239
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58185
58240
  */
58186
58241
  var ControlStatusSchema = object({
58187
58242
  kind: _enum([
@@ -58193,39 +58248,17 @@ var ControlStatusSchema = object({
58193
58248
  /** Current value. Numeric kind → number; select/text/datetime → string.
58194
58249
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58195
58250
  * or time-only — HA accepts all three forms). */
58196
- value: union([number(), string()]),
58251
+ value: union([number().meta(readingMeta("device")), string()]),
58197
58252
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58198
58253
  options: array(string()),
58199
58254
  /** Ms epoch when the slice was last updated. */
58200
58255
  lastChangedAt: number(),
58201
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58202
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58203
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58204
- unit: string().optional(),
58205
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58206
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58207
- min: number().optional(),
58208
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58209
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58210
- max: number().optional(),
58211
- /** Step increment. Populated live from HA `attributes.step`.
58212
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58213
- step: number().optional(),
58214
- /** Suggested decimal places for numeric display. Populated live from HA
58215
- * `attributes.suggested_display_precision`, or derived from `step` when the
58216
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58217
- * other kinds. Falls back to auto-formatting when absent. */
58218
- precision: number().int().min(0).max(10).optional(),
58219
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58220
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58221
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58222
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58223
- * `has_date`/`has_time` attributes on each push. */
58224
- format: _enum([
58225
- "date",
58226
- "time",
58227
- "datetime"
58228
- ]).optional()
58256
+ /** Minimum allowed value, in the device quantity's base unit. */
58257
+ min: number().optional().meta(readingMeta("device")),
58258
+ /** Maximum allowed value, in the device quantity's base unit. */
58259
+ max: number().optional().meta(readingMeta("device")),
58260
+ /** Step increment, in the device quantity's base unit. */
58261
+ step: number().optional().meta(readingMeta("device"))
58229
58262
  });
58230
58263
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58231
58264
  object({
@@ -58291,9 +58324,9 @@ var CoverStatusSchema = object({
58291
58324
  ]),
58292
58325
  /** 0 = fully closed, 100 = fully open. Null when the device has
58293
58326
  * no intermediate position surface. */
58294
- position: number().min(0).max(100).nullable(),
58327
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58295
58328
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58296
- tiltPosition: number().min(0).max(100).nullable(),
58329
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58297
58330
  /** Ms epoch when the slice was last updated. */
58298
58331
  lastChangedAt: number()
58299
58332
  });
@@ -58396,9 +58429,9 @@ var NormalizedRangeSchema$1 = object({
58396
58429
  var DayNightStatusSchema = object({
58397
58430
  mode: DayNightModeSchema,
58398
58431
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58399
- sensitivity: number().optional(),
58432
+ sensitivity: number().optional().meta(readingMeta("none")),
58400
58433
  /** Delay before the IR-cut filter flips, in seconds. */
58401
- switchDelaySec: number().optional(),
58434
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58402
58435
  lastFetchedAt: number()
58403
58436
  });
58404
58437
  /**
@@ -58616,11 +58649,11 @@ var DishwasherStatusSchema = object({
58616
58649
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58617
58650
  availablePrograms: array(string()),
58618
58651
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58619
- remainingSeconds: number().int().nonnegative().nullable(),
58652
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58620
58653
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58621
- delayedStartSeconds: number().int().nonnegative().nullable(),
58654
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58622
58655
  /** Cycle completion 0..100, or null when not applicable. */
58623
- progressPercent: number().min(0).max(100).nullable(),
58656
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58624
58657
  /** True when the door is reported open; null when the door state is not known. */
58625
58658
  doorOpen: boolean().nullable(),
58626
58659
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58644,7 +58677,7 @@ var DishwasherStatusSchema = object({
58644
58677
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58645
58678
  machineCareDue: boolean().nullable(),
58646
58679
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58647
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58680
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58648
58681
  /** Option states: null when the appliance does not expose the option or has not said. */
58649
58682
  halfLoad: boolean().nullable(),
58650
58683
  hygienePlus: boolean().nullable(),
@@ -58659,7 +58692,7 @@ var DishwasherStatusSchema = object({
58659
58692
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58660
58693
  activeEvents: array(string()),
58661
58694
  /** Numeric error code when the appliance reports one; null otherwise. */
58662
- errorCode: number().nullable(),
58695
+ errorCode: number().nullable().meta(readingMeta("none")),
58663
58696
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58664
58697
  errorLabel: string().nullable(),
58665
58698
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58890,7 +58923,7 @@ var DoorbellStatusSchema = object({
58890
58923
  /** Ms epoch of the last press. null = never observed since this provider started. */
58891
58924
  lastPressedAt: number().nullable(),
58892
58925
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58893
- pressCountSinceStart: number()
58926
+ pressCountSinceStart: number().meta(readingMeta("count"))
58894
58927
  });
58895
58928
  var DoorbellPressEventSchema = object({
58896
58929
  deviceId: number(),
@@ -58955,17 +58988,7 @@ onPressed: {
58955
58988
  history: "none",
58956
58989
  customize: doorbellCustomize
58957
58990
  };
58958
- /**
58959
- * Enum-state sensor — a string value picked from a finite option set.
58960
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58961
- * states, weather conditions, contact-source channels, mode strings).
58962
- * The option set is stable across observations; it is persisted in the
58963
- * device config blob (via the control slice at adoption) and is NOT
58964
- * stored in `sourceInfo`.
58965
- */
58966
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58967
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58968
- var EnumSensorDateTimeFormatSchema = _enum([
58991
+ _enum([
58969
58992
  "date",
58970
58993
  "time",
58971
58994
  "datetime"
@@ -58973,12 +58996,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58973
58996
  var EnumSensorStatusSchema = object({
58974
58997
  value: string(),
58975
58998
  /**
58976
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58977
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58978
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58979
- * a time-only sensor → `'time'`.
58999
+ * Set when `value` is a timestamp rather than an enum state, so history
59000
+ * does not learn ISO strings as states. The display shape (date / time /
59001
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58980
59002
  */
58981
- format: EnumSensorDateTimeFormatSchema.optional(),
59003
+ timestamp: boolean().optional(),
58982
59004
  /** Ms epoch when the slice was last updated. */
58983
59005
  lastFetchedAt: number()
58984
59006
  });
@@ -59005,11 +59027,11 @@ var enumSensorCapability = {
59005
59027
  * History (D850): the value as an `enum` series (an appliance's program or
59006
59028
  * operation state, an HVAC action). The option set is per device, so the
59007
59029
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59008
- * `format` carries a timestamp, not a state, and is not recorded.
59030
+ * `timestamp` carries a clock, not a state, and is not recorded.
59009
59031
  */
59010
59032
  history: { fields: { value: {
59011
59033
  kind: "enum",
59012
- skipWhenSet: "format"
59034
+ skipWhenSet: "timestamp"
59013
59035
  } } },
59014
59036
  /** Clock fields: written, but excluded from the compare that decides
59015
59037
  * whether persisting is worth a SQLite commit. */
@@ -59029,13 +59051,13 @@ var EventFireSchema = object({
59029
59051
  deviceId: number(),
59030
59052
  eventType: string(),
59031
59053
  data: record(string(), unknown()).nullable(),
59032
- timestamp: number(),
59033
- seq: number()
59054
+ timestamp: number().meta(readingMeta("none")),
59055
+ seq: number().meta(readingMeta("count"))
59034
59056
  });
59035
59057
  var EventEmitterStatusSchema = object({
59036
59058
  eventTypes: array(string()),
59037
59059
  lastEvent: EventFireSchema.nullable(),
59038
- eventCountSinceStart: number()
59060
+ eventCountSinceStart: number().meta(readingMeta("count"))
59039
59061
  });
59040
59062
  var eventEmitterCapability = {
59041
59063
  name: "event-emitter",
@@ -59448,10 +59470,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59448
59470
  var FanControlStatusSchema = object({
59449
59471
  /** Active speed as 0..100 inclusive. Null when the device has no
59450
59472
  * speed surface (single-speed fan). */
59451
- percentage: number().min(0).max(100).nullable(),
59473
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59452
59474
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59453
59475
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59454
- percentageStep: number().positive().optional(),
59476
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59455
59477
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59456
59478
  * — empty string when no preset is active or supported. */
59457
59479
  preset: string(),
@@ -59572,7 +59594,7 @@ var FeatureProbeStatusSchema = object({
59572
59594
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59573
59595
  model: string().nullable(),
59574
59596
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59575
- channelCount: number().nullable(),
59597
+ channelCount: number().nullable().meta(readingMeta("count")),
59576
59598
  /**
59577
59599
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59578
59600
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59697,7 +59719,7 @@ var gasCapability = {
59697
59719
  };
59698
59720
  var HeatingValveStatusSchema = object({
59699
59721
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59700
- position: number().min(0).max(100).nullable(),
59722
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59701
59723
  /** Ms epoch when the slice was last updated (push or poll). */
59702
59724
  lastFetchedAt: number()
59703
59725
  });
@@ -59740,9 +59762,9 @@ var HumidifierStatusSchema = object({
59740
59762
  /** Whether the humidifier is currently on. */
59741
59763
  on: boolean(),
59742
59764
  /** Current measured relative humidity (0..100). Null when not reported. */
59743
- currentHumidity: number().min(0).max(100).nullable(),
59765
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59744
59766
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59745
- targetHumidity: number().min(0).max(100).nullable(),
59767
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59746
59768
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59747
59769
  * device has no mode surface. */
59748
59770
  mode: string().nullable(),
@@ -59752,9 +59774,9 @@ var HumidifierStatusSchema = object({
59752
59774
  * `idle` / `off`). Null when the device doesn't report it. */
59753
59775
  action: string().nullable(),
59754
59776
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59755
- minHumidity: number().nullable(),
59777
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59756
59778
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59757
- maxHumidity: number().nullable(),
59779
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59758
59780
  /** Ms epoch when the slice was last updated. */
59759
59781
  lastChangedAt: number()
59760
59782
  });
@@ -59824,22 +59846,13 @@ var humidifierCapability = {
59824
59846
  * `sourceInfo`.
59825
59847
  */
59826
59848
  var HumiditySensorStatusSchema = object({
59827
- /** Current relative humidity, 0..100. */
59828
- percent: number().min(0).max(100).meta({
59829
- unit: "%",
59849
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59850
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59851
+ precision: 0,
59830
59852
  label: "Humidity"
59831
- }),
59853
+ })),
59832
59854
  /** Ms epoch when the slice was last updated. */
59833
- lastFetchedAt: number(),
59834
- /** Live display unit from the upstream source (e.g. HA
59835
- * `attributes.unit_of_measurement`). The UI prefers this over the
59836
- * role's canonical unit. Absent → fall back to the canonical unit. */
59837
- unit: string().optional(),
59838
- /** Suggested decimal places for numeric display.
59839
- * Populated live from the upstream source when provided (e.g. HA
59840
- * `attributes.suggested_display_precision`). Falls back to
59841
- * auto-formatting when absent. */
59842
- precision: number().int().min(0).max(10).optional()
59855
+ lastFetchedAt: number()
59843
59856
  });
59844
59857
  var humiditySensorCapability = {
59845
59858
  name: "humidity-sensor",
@@ -59870,12 +59883,7 @@ var humiditySensorCapability = {
59870
59883
  } } },
59871
59884
  /** Clock fields: written, but excluded from the compare that decides
59872
59885
  * whether persisting is worth a SQLite commit. */
59873
- volatileStateFields: ["lastFetchedAt"],
59874
- customize: {
59875
- version: 1,
59876
- advanced: "derive",
59877
- targets: []
59878
- }
59886
+ volatileStateFields: ["lastFetchedAt"]
59879
59887
  };
59880
59888
  /**
59881
59889
  * Image display cap. Models a single still image exposed by an integration —
@@ -59896,7 +59904,7 @@ var ImageStatusSchema = object({
59896
59904
  * entity exposes no `entity_picture` (yet). */
59897
59905
  url: string().nullable(),
59898
59906
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59899
- lastUpdated: number().nullable()
59907
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59900
59908
  });
59901
59909
  var imageCapability = {
59902
59910
  name: "image",
@@ -59984,19 +59992,19 @@ var NormalizedRangeSchema = object({
59984
59992
  */
59985
59993
  var ImageSettingsStatusSchema = object({
59986
59994
  /** Normalized 0–100. */
59987
- brightness: number().optional(),
59995
+ brightness: number().optional().meta(readingMeta("none")),
59988
59996
  /** Normalized 0–100. */
59989
- contrast: number().optional(),
59997
+ contrast: number().optional().meta(readingMeta("none")),
59990
59998
  /** Normalized 0–100. */
59991
- saturation: number().optional(),
59999
+ saturation: number().optional().meta(readingMeta("none")),
59992
60000
  /** Normalized 0–100. */
59993
- sharpness: number().optional(),
60001
+ sharpness: number().optional().meta(readingMeta("none")),
59994
60002
  mirror: boolean().optional(),
59995
60003
  flip: boolean().optional(),
59996
60004
  rotate: ImageRotateSchema.optional(),
59997
60005
  whiteBalance: WhiteBalanceModeSchema.optional(),
59998
60006
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59999
- warmth: number().optional(),
60007
+ warmth: number().optional().meta(readingMeta("none")),
60000
60008
  exposureMode: ExposureModeSchema.optional(),
60001
60009
  backlightMode: BacklightModeSchema.optional(),
60002
60010
  lastFetchedAt: number()
@@ -60275,10 +60283,10 @@ var IntercomAbilitySchema = object({
60275
60283
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60276
60284
  codecs: array(string()),
60277
60285
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60278
- sampleRate: number(),
60286
+ sampleRate: number().meta(readingMeta("none")),
60279
60287
  duplex: _enum(["half", "full"]),
60280
60288
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60281
- maxBacklogMs: number()
60289
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60282
60290
  });
60283
60291
  var IntercomStatusSchema = object({
60284
60292
  /** True while a talk session is open. */
@@ -60505,13 +60513,13 @@ var LawnMowerControlStatusSchema = object({
60505
60513
  activity: LawnMowerActivitySchema,
60506
60514
  /** 0..100 battery percentage. Null when the device has no battery
60507
60515
  * reading. */
60508
- batteryLevel: number().min(0).max(100).nullable(),
60516
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60509
60517
  /** 0..100 mowing-completion percentage of the current task, or null when no
60510
60518
  * task is active / progress is unavailable. */
60511
- progressPercent: number().min(0).max(100).nullable(),
60519
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60512
60520
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60513
60521
  * or null when unknown. */
60514
- currentCode: number().nullable(),
60522
+ currentCode: number().nullable().meta(readingMeta("none")),
60515
60523
  /** Human label for {@link currentCode}, or null when undecodable. */
60516
60524
  currentCodeLabel: string().nullable(),
60517
60525
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61044,7 +61052,7 @@ var MediaPlayerStatusSchema = object({
61044
61052
  state: MediaPlayerStateSchema,
61045
61053
  /** Volume as 0..100 inclusive. Null when the device has no volume
61046
61054
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61047
- volumeLevel: number().min(0).max(100).nullable(),
61055
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61048
61056
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61049
61057
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61050
61058
  isMuted: boolean().nullable(),
@@ -61057,10 +61065,10 @@ var MediaPlayerStatusSchema = object({
61057
61065
  currentMedia: MediaInfoSchema.nullable(),
61058
61066
  /** Current playback position in ms. Null when not seekable or
61059
61067
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61060
- positionMs: number().int().nonnegative().nullable(),
61068
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61061
61069
  /** Total duration of the current media in ms. Null when unknown
61062
61070
  * (live stream) or nothing is playing. */
61063
- durationMs: number().int().nonnegative().nullable(),
61071
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61064
61072
  /** Shuffle toggle. Null when no shuffle surface.
61065
61073
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61066
61074
  shuffle: boolean().nullable(),
@@ -61369,7 +61377,7 @@ var motionCustomize = {
61369
61377
  eventName: "onMotionChanged"
61370
61378
  },
61371
61379
  deviceTypes: [DeviceType.Camera],
61372
- apply: "replace",
61380
+ apply: "add",
61373
61381
  applyAuthority: {
61374
61382
  kind: "camera-sensors",
61375
61383
  timeoutField: "motionCooldownMs"
@@ -61441,7 +61449,7 @@ var MotionStatusSchema = object({
61441
61449
  * it, every provider seeded it at boot, and every session left it
61442
61450
  * `detected: true` after its detach.
61443
61451
  */
61444
- autoClearAfterMs: number().nullable()
61452
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61445
61453
  });
61446
61454
  /**
61447
61455
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61623,7 +61631,7 @@ var MotionZoneRegionSchema = object({
61623
61631
  * the grid region(s). */
61624
61632
  var MotionZoneStatusSchema = object({
61625
61633
  enabled: boolean(),
61626
- sensitivity: number(),
61634
+ sensitivity: number().meta(readingMeta("none")),
61627
61635
  /** Grid region(s). Today exactly one `grid` shape. */
61628
61636
  regions: array(MotionZoneRegionSchema),
61629
61637
  lastFetchedAt: number()
@@ -61993,7 +62001,7 @@ var NavigationStatusSchema = object({
61993
62001
  following: boolean(),
61994
62002
  flash: boolean(),
61995
62003
  lightMode: NavigationLightModeSchema,
61996
- lightLevel: number().min(40).max(100),
62004
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
61997
62005
  /** Ms epoch when the slice was last updated. */
61998
62006
  lastChangedAt: number()
61999
62007
  });
@@ -62127,13 +62135,13 @@ var NetworkLinkStatusSchema = object({
62127
62135
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62128
62136
  * SKIP a null rather than coerce it.
62129
62137
  */
62130
- signalPercent: number().min(0).max(100).nullable(),
62138
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62131
62139
  /** Raw received signal strength in dBm, when the firmware reports one. */
62132
- rssiDbm: number().optional(),
62140
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62133
62141
  /** Network name of a wireless link, when the firmware reports it. */
62134
62142
  ssid: string().optional(),
62135
62143
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62136
- lastUpdated: number()
62144
+ lastUpdated: number().meta(readingMeta("none"))
62137
62145
  });
62138
62146
  var networkLinkCapability = {
62139
62147
  name: "network-link",
@@ -62518,7 +62526,7 @@ var NotifierStatusSchema = object({
62518
62526
  lastError: string().nullable(),
62519
62527
  /** Number of deliveries currently buffered server-side (rate-limited
62520
62528
  * notifiers may queue). 0 when the provider sends synchronously. */
62521
- queueDepth: number().int().nonnegative(),
62529
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62522
62530
  /** Per-feature capability matrix — authoritative source for the
62523
62531
  * compose-form field gating. */
62524
62532
  supports: NotifierSupportsSchema
@@ -62610,11 +62618,9 @@ onSent: { data: object({
62610
62618
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62611
62619
  * for: water flow, distance, weight, frequency, signal strength, …).
62612
62620
  *
62613
- * The `unit` and `precision` fields carry the live display metadata that
62614
- * the upstream source provides with each state push (HA
62615
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62616
- * The UI reads them directly from the slice — they are the single source
62617
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
62621
+ * The quantity is not on this slice. It is chosen when the device is
62622
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
62623
+ * that quantity's base unit. `null` is a refused sample (D393).
62618
62624
  *
62619
62625
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62620
62626
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62623,15 +62629,8 @@ onSent: { data: object({
62623
62629
  * numeric values land here.
62624
62630
  */
62625
62631
  var NumericSensorStatusSchema = object({
62626
- value: number(),
62627
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62628
- * Populated live from the upstream source on each state push.
62629
- * Absent for unitless sensors. */
62630
- unit: string().optional(),
62631
- /** Suggested decimal places for numeric display.
62632
- * Populated live from the upstream source when provided.
62633
- * Falls back to auto-formatting when absent. */
62634
- precision: number().int().min(0).max(10).optional(),
62632
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
62633
+ value: number().nullable().meta(readingMeta("device")),
62635
62634
  /** Ms epoch when the slice was last updated. */
62636
62635
  lastFetchedAt: number()
62637
62636
  });
@@ -62655,11 +62654,11 @@ var numericSensorCapability = {
62655
62654
  */
62656
62655
  durability: "restored",
62657
62656
  /**
62658
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
62657
+ * History (D850): recorded series. The unit is the device quantity's base,
62658
+ * resolved when the series is read — the cap has no single unit.
62659
62659
  */
62660
62660
  history: { fields: { value: {
62661
62661
  kind: "gauge",
62662
- unitFrom: "unit",
62663
62662
  minIntervalMs: 3e4
62664
62663
  } } },
62665
62664
  /** Clock fields: written, but excluded from the compare that decides
@@ -62951,23 +62950,26 @@ var PetFeederStatusSchema = object({
62951
62950
  /** Food currently in the bowl (grams). Null when the device has not
62952
62951
  * reported a reading yet. On dual-hopper models this is the combined
62953
62952
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62954
- foodLevel: number().nullable(),
62953
+ foodLevel: number().nullable().meta(readingMeta("none")),
62955
62954
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62956
62955
  * single-hopper models. */
62957
- food1: number().nullable(),
62956
+ food1: number().nullable().meta(readingMeta("none")),
62958
62957
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62959
62958
  * single-hopper models. */
62960
- food2: number().nullable(),
62959
+ food2: number().nullable().meta(readingMeta("none")),
62961
62960
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62962
62961
  * (`device_class: problem`, on = low). True when the bowl is empty /
62963
62962
  * below the feeder's low threshold. */
62964
62963
  lowFood: boolean(),
62965
62964
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62966
62965
  * device has no battery reading. */
62967
- batteryPower: number().min(0).max(100).nullable(),
62968
- /** Days of desiccant life remaining. Null when the model has no
62969
- * desiccant sensor. */
62970
- desiccantLeftDays: number().nullable(),
62966
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62967
+ /**
62968
+ * Desiccant life remaining, stored in seconds (duration base, D944).
62969
+ * Providers convert from the days the feeder reports. Null when the
62970
+ * model has no desiccant sensor.
62971
+ */
62972
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62971
62973
  /** True while a feed is in progress. */
62972
62974
  feeding: boolean(),
62973
62975
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62978,7 +62980,7 @@ var PetFeederStatusSchema = object({
62978
62980
  * with `errorCode` for consumers that want the raw integer. */
62979
62981
  error: string().nullable(),
62980
62982
  /** Raw device error code (0 / null = no error). */
62981
- errorCode: number().nullable(),
62983
+ errorCode: number().nullable().meta(readingMeta("none")),
62982
62984
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62983
62985
  isDualHopper: boolean(),
62984
62986
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -62988,7 +62990,7 @@ var PetFeederStatusSchema = object({
62988
62990
  /** Play a chime when dispensing. */
62989
62991
  feedSound: boolean(),
62990
62992
  /** Speaker / prompt volume level (device-scaled integer). */
62991
- volume: number(),
62993
+ volume: number().meta(readingMeta("none")),
62992
62994
  /** Ms epoch when the slice was last refreshed from the cloud. */
62993
62995
  lastFetchedAt: number()
62994
62996
  });
@@ -63107,7 +63109,7 @@ var petFeederCapability = {
63107
63109
  },
63108
63110
  desiccantLeftDays: {
63109
63111
  kind: "gauge",
63110
- unit: "days"
63112
+ unit: "s"
63111
63113
  }
63112
63114
  } }
63113
63115
  };
@@ -63508,36 +63510,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63508
63510
  */
63509
63511
  var PowerMeterStatusSchema = object({
63510
63512
  /** Instantaneous power draw in watts. */
63511
- watts: number().optional().meta({
63512
- unit: "W",
63513
- label: "Power"
63514
- }),
63513
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63515
63514
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63516
- kwhTotal: number().optional().meta({
63517
- unit: "kWh",
63518
- label: "Energy"
63519
- }),
63515
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63520
63516
  /** Voltage in volts. */
63521
- volts: number().optional(),
63517
+ volts: number().optional().meta(readingMeta("voltage")),
63522
63518
  /** Current in amperes. */
63523
- amps: number().optional(),
63524
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63525
- va: number().optional(),
63519
+ amps: number().optional().meta(readingMeta("current")),
63520
+ /** Apparent power in volt-amperes. */
63521
+ va: number().optional().meta(readingMeta("apparent-power")),
63526
63522
  /** Power factor (0..1) if reported. */
63527
- powerFactor: number().min(0).max(1).optional(),
63523
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63528
63524
  /** Ms epoch when the slice was last updated. */
63529
- lastFetchedAt: number(),
63530
- /** Live display unit of the single metric this slice carries (e.g. HA
63531
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63532
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63533
- * unit per slice is unambiguous. The UI prefers this over the role's
63534
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63535
- unit: string().optional(),
63536
- /** Suggested decimal places for numeric display.
63537
- * Populated live from the upstream source when provided (e.g. HA
63538
- * `attributes.suggested_display_precision`). Falls back to
63539
- * auto-formatting when absent. */
63540
- precision: number().int().min(0).max(10).optional()
63525
+ lastFetchedAt: number()
63541
63526
  });
63542
63527
  var powerMeterCapability = {
63543
63528
  name: "power-meter",
@@ -63601,12 +63586,7 @@ var powerMeterCapability = {
63601
63586
  } },
63602
63587
  /** Clock fields: written, but excluded from the compare that decides
63603
63588
  * whether persisting is worth a SQLite commit. */
63604
- volatileStateFields: ["lastFetchedAt"],
63605
- customize: {
63606
- version: 1,
63607
- advanced: "derive",
63608
- targets: []
63609
- }
63589
+ volatileStateFields: ["lastFetchedAt"]
63610
63590
  };
63611
63591
  /**
63612
63592
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63626,11 +63606,11 @@ var powerMeterCapability = {
63626
63606
  */
63627
63607
  var GpsLocationSchema = object({
63628
63608
  /** Latitude in decimal degrees, -90..90. */
63629
- latitude: number().min(-90).max(90),
63609
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63630
63610
  /** Longitude in decimal degrees, -180..180. */
63631
- longitude: number().min(-180).max(180),
63611
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63632
63612
  /** Reported accuracy in meters (lower = better). */
63633
- accuracyMeters: number().nonnegative()
63613
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63634
63614
  });
63635
63615
  var PresenceStatusSchema = object({
63636
63616
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63642,7 +63622,7 @@ var PresenceStatusSchema = object({
63642
63622
  gps: GpsLocationSchema.nullable(),
63643
63623
  /** Optional battery level of the tracking device (0..100). Null
63644
63624
  * when not reported. */
63645
- batteryPercent: number().min(0).max(100).nullable(),
63625
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63646
63626
  /** Ms epoch when the slice was last updated. */
63647
63627
  lastChangedAt: number()
63648
63628
  });
@@ -63692,19 +63672,10 @@ var presenceCapability = {
63692
63672
  * `sourceInfo`.
63693
63673
  */
63694
63674
  var PressureSensorStatusSchema = object({
63695
- /** Current pressure in hPa. */
63696
- hpa: number(),
63675
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
63676
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63697
63677
  /** Ms epoch when the slice was last updated. */
63698
- lastFetchedAt: number(),
63699
- /** Live display unit from the upstream source (e.g. HA
63700
- * `attributes.unit_of_measurement`). The UI prefers this over the
63701
- * role's canonical unit. Absent → fall back to the canonical unit. */
63702
- unit: string().optional(),
63703
- /** Suggested decimal places for numeric display.
63704
- * Populated live from the upstream source when provided (e.g. HA
63705
- * `attributes.suggested_display_precision`). Falls back to
63706
- * auto-formatting when absent. */
63707
- precision: number().int().min(0).max(10).optional()
63678
+ lastFetchedAt: number()
63708
63679
  });
63709
63680
  var pressureSensorCapability = {
63710
63681
  name: "pressure-sensor",
@@ -64118,8 +64089,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64118
64089
  }), _void(), { kind: "mutation" });
64119
64090
  var PtzAutotrackSettingsSchema = object({
64120
64091
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64121
- stopDelaySeconds: number().int().min(0).max(300),
64122
- disappearDelaySeconds: number().int().min(0).max(300)
64092
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64093
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64123
64094
  });
64124
64095
  /**
64125
64096
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64273,7 +64244,7 @@ var RunningStateStatusSchema = object({
64273
64244
  /** null = unknown — never coerced to false (D393). */
64274
64245
  running: boolean().nullable(),
64275
64246
  /** Epoch ms the current boolean began; null while `running` is null. */
64276
- since: number().nullable(),
64247
+ since: number().nullable().meta(readingMeta("none")),
64277
64248
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64278
64249
  phase: string().nullable(),
64279
64250
  /** null until a reading has been published (D393). */
@@ -64288,7 +64259,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64288
64259
  /** Generic Customize target for the running boolean (D936). */
64289
64260
  var runningCustomize = {
64290
64261
  version: 1,
64291
- advanced: "derive",
64292
64262
  targets: [{
64293
64263
  id: "running",
64294
64264
  cardinality: "scalar",
@@ -64308,7 +64278,7 @@ var runningCustomize = {
64308
64278
  DeviceType.Thermostat,
64309
64279
  DeviceType.PetFeeder
64310
64280
  ],
64311
- apply: "add-or-replace",
64281
+ apply: "add",
64312
64282
  path: "running",
64313
64283
  sources: [
64314
64284
  {
@@ -65572,9 +65542,9 @@ var RecordTriggerSchema = _enum([
65572
65542
  */
65573
65543
  var RecordWindowSchema = object({
65574
65544
  trigger: RecordTriggerSchema,
65575
- day: number().int().min(0).max(6),
65576
- startMinute: number().int().min(0).max(1439),
65577
- endMinute: number().int().min(1).max(1440)
65545
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65546
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65547
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65578
65548
  });
65579
65549
  /** Status of one physical volume, as the camera itself describes it. */
65580
65550
  var OnboardStorageVolumeSchema = object({
@@ -65650,10 +65620,10 @@ var RecordingOnboardStatusSchema = object({
65650
65620
  /** Master "record to the card at all" switch. */
65651
65621
  enabled: boolean().nullable(),
65652
65622
  overwriteWhenFull: boolean().nullable(),
65653
- preRecordSec: number().nullable(),
65654
- postRecordSec: number().nullable(),
65623
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
65624
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65655
65625
  /** Length of one recorded file, in minutes. */
65656
- segmentMinutes: number().nullable(),
65626
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65657
65627
  /** The primary track's weekly windows, flattened. */
65658
65628
  windows: array(RecordWindowSchema),
65659
65629
  /**
@@ -65669,7 +65639,7 @@ var RecordingOnboardStatusSchema = object({
65669
65639
  * provider reporting a non-zero count also reports the schedule as not
65670
65640
  * writable.
65671
65641
  */
65672
- unreadableWindows: number(),
65642
+ unreadableWindows: number().meta(readingMeta("count")),
65673
65643
  /**
65674
65644
  * The camera is scheduled to record and has NO usable storage.
65675
65645
  *
@@ -66514,7 +66484,7 @@ var CamStreamDescriptorSchema = object({
66514
66484
  url: string().optional(),
66515
66485
  codec: string().optional(),
66516
66486
  resolution: CamStreamResolutionSchema.optional(),
66517
- fps: number().positive().optional(),
66487
+ fps: number().positive().optional().meta(readingMeta("rate")),
66518
66488
  label: string().optional(),
66519
66489
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66520
66490
  deviceFeatures: array(string()).optional(),
@@ -66601,18 +66571,18 @@ var StreamProfileSchema = _enum([
66601
66571
  ]);
66602
66572
  /** The current encoding config of one stream profile. */
66603
66573
  var StreamProfileConfigSchema = object({
66604
- width: number(),
66605
- height: number(),
66574
+ width: number().meta(readingMeta("none")),
66575
+ height: number().meta(readingMeta("none")),
66606
66576
  codec: _enum(["h264", "h265"]),
66607
- framerate: number(),
66608
- bitrate: number(),
66577
+ framerate: number().meta(readingMeta("rate")),
66578
+ bitrate: number().meta(readingMeta("bitrate")),
66609
66579
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66610
66580
  encoderProfile: _enum([
66611
66581
  "high",
66612
66582
  "main",
66613
66583
  "baseline"
66614
66584
  ]).optional(),
66615
- gop: number().optional(),
66585
+ gop: number().optional().meta(readingMeta("none")),
66616
66586
  /**
66617
66587
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66618
66588
  *
@@ -66833,12 +66803,7 @@ var switchCapability = {
66833
66803
  label: "On",
66834
66804
  kind: "boolean"
66835
66805
  }
66836
- }] },
66837
- customize: {
66838
- version: 1,
66839
- advanced: "derive",
66840
- targets: []
66841
- }
66806
+ }] }
66842
66807
  };
66843
66808
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66844
66809
  function hasControlCharacter(value) {
@@ -67421,35 +67386,21 @@ var tamperCapability = {
67421
67386
  * entries with `device_class: temperature` and any future native
67422
67387
  * thermometer.
67423
67388
  *
67424
- * Unit normalisation: providers convert to Celsius before storing.
67425
- * The slice value is always Celsius so cross-cap aggregators
67426
- * (climate-control's `currentTemp`, energy analytics) can compose
67427
- * without per-source unit fixups. The canonical display unit (`°C`) is
67428
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67429
- * `sourceInfo`.
67389
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67390
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67430
67391
  *
67431
67392
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67432
67393
  * frozen feed (provider hung) distinct from a "temperature hasn't
67433
67394
  * changed" steady state.
67434
67395
  */
67435
67396
  var TemperatureSensorStatusSchema = object({
67436
- /** Current temperature in Celsius. */
67437
- celsius: number().meta({
67438
- unit: "°C",
67397
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67398
+ celsius: number().nullable().meta(readingMeta("temperature", {
67399
+ precision: 1,
67439
67400
  label: "Temperature"
67440
- }),
67401
+ })),
67441
67402
  /** Ms epoch when the slice was last updated (push or poll). */
67442
- lastFetchedAt: number(),
67443
- /** Live display unit from the upstream source (e.g. HA
67444
- * `attributes.unit_of_measurement`). The UI prefers this over the
67445
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67446
- * Absent → the UI falls back to the role's canonical unit. */
67447
- unit: string().optional(),
67448
- /** Suggested decimal places for numeric display.
67449
- * Populated live from the upstream source when provided (e.g. HA
67450
- * `attributes.suggested_display_precision`). Falls back to
67451
- * auto-formatting when absent. */
67452
- precision: number().int().min(0).max(10).optional()
67403
+ lastFetchedAt: number()
67453
67404
  });
67454
67405
  var temperatureSensorCapability = {
67455
67406
  name: "temperature-sensor",
@@ -67480,12 +67431,7 @@ var temperatureSensorCapability = {
67480
67431
  } } },
67481
67432
  /** Clock fields: written, but excluded from the compare that decides
67482
67433
  * whether persisting is worth a SQLite commit. */
67483
- volatileStateFields: ["lastFetchedAt"],
67484
- customize: {
67485
- version: 1,
67486
- advanced: "derive",
67487
- targets: []
67488
- }
67434
+ volatileStateFields: ["lastFetchedAt"]
67489
67435
  };
67490
67436
  /**
67491
67437
  * toast — system-scoped singleton capability that streams toast
@@ -67768,16 +67714,16 @@ var VacuumControlStatusSchema = object({
67768
67714
  ]),
67769
67715
  /** 0..100 battery percentage. Null when the device has no battery
67770
67716
  * reading. */
67771
- batteryLevel: number().min(0).max(100).nullable(),
67717
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67772
67718
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67773
67719
  * the vacuum has no speed control. */
67774
67720
  fanSpeed: string().nullable(),
67775
67721
  /** Speed tokens the hardware accepts — drives the UI selector. */
67776
67722
  availableFanSpeeds: array(string()),
67777
67723
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67778
- progressPercent: number().min(0).max(100).nullable(),
67724
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67779
67725
  /** Current error code (0 / null = no error). */
67780
- errorCode: number().nullable(),
67726
+ errorCode: number().nullable().meta(readingMeta("none")),
67781
67727
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67782
67728
  errorLabel: string().nullable(),
67783
67729
  /** Ms epoch when the slice was last updated. */
@@ -67855,7 +67801,7 @@ var ValveStatusSchema = object({
67855
67801
  ]),
67856
67802
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67857
67803
  * intermediate position surface. */
67858
- position: number().min(0).max(100).nullable(),
67804
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67859
67805
  /** Ms epoch when the slice was last updated. */
67860
67806
  lastChangedAt: number()
67861
67807
  });
@@ -67963,9 +67909,9 @@ var vibrationCapability = {
67963
67909
  */
67964
67910
  var WaterHeaterStatusSchema = object({
67965
67911
  /** Current measured temperature. Null when not reported. */
67966
- currentTemp: number().nullable(),
67912
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67967
67913
  /** Target temperature setpoint. Null when no setpoint surface. */
67968
- targetTemp: number().nullable(),
67914
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67969
67915
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67970
67916
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67971
67917
  * device reports an unknown state. */
@@ -67976,9 +67922,9 @@ var WaterHeaterStatusSchema = object({
67976
67922
  * has no away surface. */
67977
67923
  away: boolean().nullable(),
67978
67924
  /** HA `min_temp` attribute. Null when not reported. */
67979
- minTemp: number().nullable(),
67925
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67980
67926
  /** HA `max_temp` attribute. Null when not reported. */
67981
- maxTemp: number().nullable(),
67927
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67982
67928
  /** Ms epoch when the slice was last updated. */
67983
67929
  lastChangedAt: number()
67984
67930
  });
@@ -68063,25 +68009,18 @@ var waterHeaterCapability = {
68063
68009
  * Forecast deferred — current conditions only for v1.
68064
68010
  */
68065
68011
  var WeatherStatusSchema = object({
68066
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68067
- * when no condition has been reported yet. */
68012
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68068
68013
  condition: string().nullable(),
68069
- /** Current temperature in the reported unit. Null when not provided. */
68070
- temperature: number().nullable(),
68071
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68072
- temperatureUnit: string().nullable(),
68014
+ /** Current temperature in Celsius. Null when not provided or refused. */
68015
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68073
68016
  /** Relative humidity (0..100). Null when not provided. */
68074
- humidity: number().min(0).max(100).nullable(),
68075
- /** Barometric pressure in the reported unit. Null when not provided. */
68076
- pressure: number().nullable(),
68077
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68078
- pressureUnit: string().nullable(),
68079
- /** Wind speed in the reported unit. Null when not provided. */
68080
- windSpeed: number().nullable(),
68081
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68082
- windSpeedUnit: string().nullable(),
68017
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68018
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68019
+ pressure: number().nullable().meta(readingMeta("pressure")),
68020
+ /** Wind speed in metres per second. Null when not provided or refused. */
68021
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68083
68022
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68084
- windBearing: number().nullable(),
68023
+ windBearing: number().nullable().meta(readingMeta("angle")),
68085
68024
  /** Ms epoch when the slice was last updated. */
68086
68025
  lastFetchedAt: number()
68087
68026
  });
@@ -68109,12 +68048,12 @@ var weatherCapability = {
68109
68048
  */
68110
68049
  durability: "session",
68111
68050
  /**
68112
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68051
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68113
68052
  */
68114
68053
  history: { fields: {
68115
68054
  temperature: {
68116
68055
  kind: "gauge",
68117
- unitFrom: "temperatureUnit",
68056
+ unit: "°C",
68118
68057
  deadband: .2
68119
68058
  },
68120
68059
  humidity: {
@@ -68124,12 +68063,12 @@ var weatherCapability = {
68124
68063
  },
68125
68064
  pressure: {
68126
68065
  kind: "gauge",
68127
- unitFrom: "pressureUnit",
68066
+ unit: "hPa",
68128
68067
  deadband: .5
68129
68068
  },
68130
68069
  windSpeed: {
68131
68070
  kind: "gauge",
68132
- unitFrom: "windSpeedUnit",
68071
+ unit: "m/s",
68133
68072
  deadband: .5
68134
68073
  }
68135
68074
  } }
@@ -69030,6 +68969,389 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69030
68969
  kind: "mutation",
69031
68970
  auth: "admin"
69032
68971
  });
68972
+ /**
68973
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
68974
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
68975
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
68976
+ * decimals). K derives from `°C = K − 273.15`.
68977
+ */
68978
+ var UNIT_TABLE = {
68979
+ "°C": {
68980
+ dimension: "temperature",
68981
+ factor: 1,
68982
+ offset: 0
68983
+ },
68984
+ "°F": {
68985
+ dimension: "temperature",
68986
+ factor: 5 / 9,
68987
+ offset: -160 / 9
68988
+ },
68989
+ K: {
68990
+ dimension: "temperature",
68991
+ factor: 1,
68992
+ offset: -273.15
68993
+ },
68994
+ hPa: {
68995
+ dimension: "pressure",
68996
+ factor: 1,
68997
+ offset: 0
68998
+ },
68999
+ kPa: {
69000
+ dimension: "pressure",
69001
+ factor: 10,
69002
+ offset: 0
69003
+ },
69004
+ Pa: {
69005
+ dimension: "pressure",
69006
+ factor: .01,
69007
+ offset: 0
69008
+ },
69009
+ mbar: {
69010
+ dimension: "pressure",
69011
+ factor: 1,
69012
+ offset: 0
69013
+ },
69014
+ bar: {
69015
+ dimension: "pressure",
69016
+ factor: 1e3,
69017
+ offset: 0
69018
+ },
69019
+ inHg: {
69020
+ dimension: "pressure",
69021
+ factor: 33.8639,
69022
+ offset: 0
69023
+ },
69024
+ mmHg: {
69025
+ dimension: "pressure",
69026
+ factor: 1.33322,
69027
+ offset: 0
69028
+ },
69029
+ psi: {
69030
+ dimension: "pressure",
69031
+ factor: 68.9476,
69032
+ offset: 0
69033
+ },
69034
+ "m/s": {
69035
+ dimension: "speed",
69036
+ factor: 1,
69037
+ offset: 0
69038
+ },
69039
+ "km/h": {
69040
+ dimension: "speed",
69041
+ factor: 1 / 3.6,
69042
+ offset: 0
69043
+ },
69044
+ mph: {
69045
+ dimension: "speed",
69046
+ factor: .44704,
69047
+ offset: 0
69048
+ },
69049
+ kn: {
69050
+ dimension: "speed",
69051
+ factor: .514444,
69052
+ offset: 0
69053
+ },
69054
+ "mm/h": {
69055
+ dimension: "precipitation-rate",
69056
+ factor: 1,
69057
+ offset: 0
69058
+ },
69059
+ "in/h": {
69060
+ dimension: "precipitation-rate",
69061
+ factor: 25.4,
69062
+ offset: 0
69063
+ },
69064
+ m: {
69065
+ dimension: "length",
69066
+ factor: 1,
69067
+ offset: 0
69068
+ },
69069
+ mm: {
69070
+ dimension: "length",
69071
+ factor: .001,
69072
+ offset: 0
69073
+ },
69074
+ cm: {
69075
+ dimension: "length",
69076
+ factor: .01,
69077
+ offset: 0
69078
+ },
69079
+ km: {
69080
+ dimension: "length",
69081
+ factor: 1e3,
69082
+ offset: 0
69083
+ },
69084
+ in: {
69085
+ dimension: "length",
69086
+ factor: .0254,
69087
+ offset: 0
69088
+ },
69089
+ ft: {
69090
+ dimension: "length",
69091
+ factor: .3048,
69092
+ offset: 0
69093
+ },
69094
+ mi: {
69095
+ dimension: "length",
69096
+ factor: 1609.344,
69097
+ offset: 0
69098
+ },
69099
+ "m²": {
69100
+ dimension: "area",
69101
+ factor: 1,
69102
+ offset: 0
69103
+ },
69104
+ lx: {
69105
+ dimension: "illuminance",
69106
+ factor: 1,
69107
+ offset: 0
69108
+ },
69109
+ "W/m²": {
69110
+ dimension: "irradiance",
69111
+ factor: 1,
69112
+ offset: 0
69113
+ },
69114
+ W: {
69115
+ dimension: "power",
69116
+ factor: 1,
69117
+ offset: 0
69118
+ },
69119
+ kW: {
69120
+ dimension: "power",
69121
+ factor: 1e3,
69122
+ offset: 0
69123
+ },
69124
+ VA: {
69125
+ dimension: "apparent-power",
69126
+ factor: 1,
69127
+ offset: 0
69128
+ },
69129
+ Wh: {
69130
+ dimension: "energy",
69131
+ factor: 1,
69132
+ offset: 0
69133
+ },
69134
+ kWh: {
69135
+ dimension: "energy",
69136
+ factor: 1e3,
69137
+ offset: 0
69138
+ },
69139
+ MWh: {
69140
+ dimension: "energy",
69141
+ factor: 1e6,
69142
+ offset: 0
69143
+ },
69144
+ V: {
69145
+ dimension: "voltage",
69146
+ factor: 1,
69147
+ offset: 0
69148
+ },
69149
+ mV: {
69150
+ dimension: "voltage",
69151
+ factor: .001,
69152
+ offset: 0
69153
+ },
69154
+ A: {
69155
+ dimension: "current",
69156
+ factor: 1,
69157
+ offset: 0
69158
+ },
69159
+ mA: {
69160
+ dimension: "current",
69161
+ factor: .001,
69162
+ offset: 0
69163
+ },
69164
+ ppm: {
69165
+ dimension: "concentration-volume",
69166
+ factor: 1,
69167
+ offset: 0
69168
+ },
69169
+ ppb: {
69170
+ dimension: "concentration-volume",
69171
+ factor: .001,
69172
+ offset: 0
69173
+ },
69174
+ "µg/m³": {
69175
+ dimension: "concentration-mass",
69176
+ factor: 1,
69177
+ offset: 0
69178
+ },
69179
+ "mg/m³": {
69180
+ dimension: "concentration-mass",
69181
+ factor: 1e3,
69182
+ offset: 0
69183
+ },
69184
+ kg: {
69185
+ dimension: "mass",
69186
+ factor: 1,
69187
+ offset: 0
69188
+ },
69189
+ g: {
69190
+ dimension: "mass",
69191
+ factor: .001,
69192
+ offset: 0
69193
+ },
69194
+ lb: {
69195
+ dimension: "mass",
69196
+ factor: .453592,
69197
+ offset: 0
69198
+ },
69199
+ oz: {
69200
+ dimension: "mass",
69201
+ factor: .0283495,
69202
+ offset: 0
69203
+ },
69204
+ "%": {
69205
+ dimension: "percentage",
69206
+ factor: 1,
69207
+ offset: 0
69208
+ },
69209
+ L: {
69210
+ dimension: "volume",
69211
+ factor: 1,
69212
+ offset: 0
69213
+ },
69214
+ mL: {
69215
+ dimension: "volume",
69216
+ factor: .001,
69217
+ offset: 0
69218
+ },
69219
+ gal: {
69220
+ dimension: "volume",
69221
+ factor: 3.785411784,
69222
+ offset: 0
69223
+ },
69224
+ s: {
69225
+ dimension: "duration",
69226
+ factor: 1,
69227
+ offset: 0
69228
+ },
69229
+ ms: {
69230
+ dimension: "duration",
69231
+ factor: .001,
69232
+ offset: 0
69233
+ },
69234
+ min: {
69235
+ dimension: "duration",
69236
+ factor: 60,
69237
+ offset: 0
69238
+ },
69239
+ h: {
69240
+ dimension: "duration",
69241
+ factor: 3600,
69242
+ offset: 0
69243
+ },
69244
+ d: {
69245
+ dimension: "duration",
69246
+ factor: 86400,
69247
+ offset: 0
69248
+ },
69249
+ B: {
69250
+ dimension: "information",
69251
+ factor: 1,
69252
+ offset: 0
69253
+ },
69254
+ KiB: {
69255
+ dimension: "information",
69256
+ factor: 1024,
69257
+ offset: 0
69258
+ },
69259
+ MiB: {
69260
+ dimension: "information",
69261
+ factor: 1024 ** 2,
69262
+ offset: 0
69263
+ },
69264
+ GiB: {
69265
+ dimension: "information",
69266
+ factor: 1024 ** 3,
69267
+ offset: 0
69268
+ },
69269
+ TiB: {
69270
+ dimension: "information",
69271
+ factor: 1024 ** 4,
69272
+ offset: 0
69273
+ },
69274
+ kB: {
69275
+ dimension: "information",
69276
+ factor: 1e3,
69277
+ offset: 0
69278
+ },
69279
+ MB: {
69280
+ dimension: "information",
69281
+ factor: 1e6,
69282
+ offset: 0
69283
+ },
69284
+ GB: {
69285
+ dimension: "information",
69286
+ factor: 1e9,
69287
+ offset: 0
69288
+ },
69289
+ "°": {
69290
+ dimension: "angle",
69291
+ factor: 1,
69292
+ offset: 0
69293
+ },
69294
+ dBFS: {
69295
+ dimension: "sound-level",
69296
+ factor: 1,
69297
+ offset: 0
69298
+ },
69299
+ dBm: {
69300
+ dimension: "signal-level",
69301
+ factor: 1,
69302
+ offset: 0
69303
+ },
69304
+ mired: {
69305
+ dimension: "colour-temperature",
69306
+ factor: 1,
69307
+ offset: 0
69308
+ },
69309
+ kbps: {
69310
+ dimension: "bitrate",
69311
+ factor: 1,
69312
+ offset: 0
69313
+ },
69314
+ Mbps: {
69315
+ dimension: "bitrate",
69316
+ factor: 1e3,
69317
+ offset: 0
69318
+ },
69319
+ fps: {
69320
+ dimension: "rate",
69321
+ factor: 1,
69322
+ offset: 0
69323
+ }
69324
+ };
69325
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69326
+ function specFor(unit) {
69327
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
69328
+ }
69329
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
69330
+ function unitDimension(unit) {
69331
+ return specFor(unit)?.dimension;
69332
+ }
69333
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
69334
+ function toCanonical(value, spec) {
69335
+ return value * spec.factor + spec.offset;
69336
+ }
69337
+ /** Canonical-unit value → the unit described by `spec`. */
69338
+ function fromCanonical(canonical, spec) {
69339
+ return (canonical - spec.offset) / spec.factor;
69340
+ }
69341
+ /**
69342
+ * Graceful variant for the render path: returns the converted value, or
69343
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
69344
+ * Never throws — a render caller falls back to the raw value + source unit.
69345
+ */
69346
+ function tryConvertUnit(value, from, to) {
69347
+ if (from === to) return value;
69348
+ const fromSpec = specFor(from);
69349
+ if (!fromSpec) return void 0;
69350
+ const toSpec = specFor(to);
69351
+ if (!toSpec) return void 0;
69352
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
69353
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
69354
+ }
69033
69355
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69034
69356
  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;
69035
69357
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83699,7 +84021,6 @@ var SENSOR_CAP_VOCABULARY = {
83699
84021
  field: "value",
83700
84022
  quantity: t("the reading", "il valore"),
83701
84023
  label: t("Reading", "Valore"),
83702
- unitField: "unit",
83703
84024
  icon: "gauge"
83704
84025
  }]
83705
84026
  },
@@ -83835,7 +84156,7 @@ var SENSOR_CAP_VOCABULARY = {
83835
84156
  field: "temperature",
83836
84157
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83837
84158
  label: t("Temperature", "Temperatura"),
83838
- unitField: "temperatureUnit",
84159
+ unit: "°C",
83839
84160
  icon: "thermometer"
83840
84161
  },
83841
84162
  {
@@ -83851,7 +84172,7 @@ var SENSOR_CAP_VOCABULARY = {
83851
84172
  field: "windSpeed",
83852
84173
  quantity: t("the wind speed", "la velocità del vento"),
83853
84174
  label: t("Wind", "Vento"),
83854
- unitField: "windSpeedUnit",
84175
+ unit: "m/s",
83855
84176
  icon: "wind"
83856
84177
  },
83857
84178
  {
@@ -83859,7 +84180,7 @@ var SENSOR_CAP_VOCABULARY = {
83859
84180
  field: "pressure",
83860
84181
  quantity: t("the air pressure", "la pressione atmosferica"),
83861
84182
  label: t("Pressure", "Pressione"),
83862
- unitField: "pressureUnit",
84183
+ unit: "hPa",
83863
84184
  icon: "gauge"
83864
84185
  }
83865
84186
  ]
@@ -88224,6 +88545,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
88224
88545
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88225
88546
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88226
88547
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
88548
+ /**
88549
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
88550
+ * An unknown unit or a unit from another dimension is a refusal: the caller
88551
+ * logs it and publishes null (D393). This function never guesses.
88552
+ */
88553
+ var DIMENSION_QUANTITY = {
88554
+ temperature: "temperature",
88555
+ pressure: "pressure",
88556
+ speed: "speed",
88557
+ "precipitation-rate": "precipitation-rate",
88558
+ length: "length",
88559
+ area: "area",
88560
+ illuminance: "illuminance",
88561
+ irradiance: "irradiance",
88562
+ power: "power",
88563
+ "apparent-power": "apparent-power",
88564
+ energy: "energy",
88565
+ voltage: "voltage",
88566
+ current: "current",
88567
+ "concentration-volume": "concentration-volume",
88568
+ "concentration-mass": "concentration-mass",
88569
+ mass: "mass",
88570
+ percentage: "percentage",
88571
+ volume: "volume",
88572
+ duration: "duration",
88573
+ information: "information",
88574
+ angle: "angle",
88575
+ "sound-level": "sound-level",
88576
+ "signal-level": "signal-level",
88577
+ "colour-temperature": "colour-temperature",
88578
+ bitrate: "bitrate",
88579
+ rate: "rate"
88580
+ };
88581
+ /**
88582
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
88583
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
88584
+ * precipitation is chosen by the caller (a device class), not by the symbol.
88585
+ */
88586
+ function quantityFromUnit(rawUnit) {
88587
+ const unit = normalizeUnit(rawUnit);
88588
+ if (unit === void 0) return void 0;
88589
+ if (unit === "ppb") return "concentration-ppb";
88590
+ if (unit === "ms") return "duration-ms";
88591
+ const dimension = unitDimension(unit);
88592
+ if (dimension === void 0) return void 0;
88593
+ return DIMENSION_QUANTITY[dimension];
88594
+ }
88595
+ function ingestReading(input) {
88596
+ if (!Number.isFinite(input.value)) return {
88597
+ ok: false,
88598
+ reason: "non-finite"
88599
+ };
88600
+ if (input.quantity === "device") return {
88601
+ ok: false,
88602
+ reason: "unknown-unit"
88603
+ };
88604
+ const raw = input.rawUnit?.trim();
88605
+ if (raw === void 0 || raw.length === 0) return {
88606
+ ok: true,
88607
+ value: input.value
88608
+ };
88609
+ const unit = normalizeUnit(raw);
88610
+ if (unit === void 0) return {
88611
+ ok: false,
88612
+ reason: "unknown-unit"
88613
+ };
88614
+ const base = baseUnitOf(input.quantity);
88615
+ if (base === void 0) {
88616
+ if (unitDimension(unit) !== void 0) return {
88617
+ ok: false,
88618
+ reason: "cross-dimension"
88619
+ };
88620
+ return {
88621
+ ok: false,
88622
+ reason: "unknown-unit"
88623
+ };
88624
+ }
88625
+ const converted = tryConvertUnit(input.value, unit, base);
88626
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
88627
+ ok: false,
88628
+ reason: "unknown-unit"
88629
+ } : {
88630
+ ok: false,
88631
+ reason: "cross-dimension"
88632
+ };
88633
+ return {
88634
+ ok: true,
88635
+ value: converted
88636
+ };
88637
+ }
88227
88638
  var MB = 1024 * 1024;
88228
88639
  1024 * MB, 3072 * MB;
88229
88640
  //#endregion
@@ -94610,6 +95021,23 @@ function capForQuantity(quantity) {
94610
95021
  * the generic `numeric-sensor` cap but carry a typed role, so the UI renders a
94611
95022
  * wind / rain / UV / solar glyph instead of a bare hash.
94612
95023
  */
95024
+ /** Ecowitt quantity → the stored reading quantity. Unknown names return null. */
95025
+ function readingQuantityFor(quantity) {
95026
+ switch (quantity) {
95027
+ case "temperature": return "temperature";
95028
+ case "humidity": return "humidity";
95029
+ case "pressure": return "pressure";
95030
+ case "wind_speed": return "speed";
95031
+ case "wind_direction": return "angle";
95032
+ case "precipitation": return "precipitation";
95033
+ case "precipitation_rate": return "precipitation-rate";
95034
+ case "uv": return "index";
95035
+ case "irradiance": return "irradiance";
95036
+ case "lightning_distance": return "length";
95037
+ case "lightning_count": return "count";
95038
+ default: return null;
95039
+ }
95040
+ }
94613
95041
  function roleForQuantity(quantity) {
94614
95042
  switch (quantity) {
94615
95043
  case "temperature": return DeviceRole.TemperatureSensor;
@@ -94868,13 +95296,6 @@ var ecowittSensorSchema = object({
94868
95296
  system: literal("ecowitt").optional(),
94869
95297
  integrationId: string().optional()
94870
95298
  });
94871
- /**
94872
- * Slice key the V-battery `numeric-sensor` cap writes/reads. It is the cap's own
94873
- * name (`'numeric-sensor'`) — the same key the codegen `getStatus` reads — so
94874
- * the manual `setCapState` here lands in the slice the framework serves. Only
94875
- * registered when the PRIMARY cap is NOT itself `numeric-sensor` (no collision).
94876
- */
94877
- var BATTERY_VOLTAGE_CAP = numericSensorCapability.name;
94878
95299
  var SUPPORTED_CAPS = new Set([
94879
95300
  "temperature-sensor",
94880
95301
  "humidity-sensor",
@@ -94893,16 +95314,10 @@ var SUPPORTED_CAPS = new Set([
94893
95314
  * library discriminates two battery forms via `batteryUnit`:
94894
95315
  * - `'%'` (binary/bar batteries, already decoded to a percent) → the typed
94895
95316
  * `battery` cap (which models a 0..100 percentage).
94896
- * - `'V'` (raw voltage, e.g. WS90/WH90/WS85 capacitor + soil/leaf push) → a
94897
- * `numeric-sensor` cap carrying the voltage with `unit: 'V'`. A voltage has
94898
- * no honest percentage, so the percentage-shaped `battery` cap would be
94899
- * wrong; the generic numeric cap renders the volts verbatim.
94900
- *
94901
- * A child registers AT MOST one battery cap. When the child's PRIMARY sensor cap
94902
- * is itself `numeric-sensor` (e.g. a precipitation channel) a V-battery cannot
94903
- * also be hosted as a second `numeric-sensor` on the same device (the cap slice
94904
- * is keyed by cap name) — that rare case is skipped with a debug note rather
94905
- * than adding a new cap.
95317
+ * - `'V'` (raw voltage, e.g. WS90/WH90/WS85 capacitor + soil/leaf push) →
95318
+ * `battery.volts`. A voltage has no honest percentage, so `percentage`
95319
+ * stays null. The field lives on `battery`, so a numeric primary sensor
95320
+ * and a voltage battery do not share a slice.
94906
95321
  */
94907
95322
  var EcowittSensorDevice = class extends BaseDevice {
94908
95323
  features = [];
@@ -94910,12 +95325,12 @@ var EcowittSensorDevice = class extends BaseDevice {
94910
95325
  gatewayId;
94911
95326
  capName;
94912
95327
  valueKey;
95328
+ ecowittQuantity;
94913
95329
  /**
94914
95330
  * Which battery cap (if any) was registered for this sensor:
94915
95331
  * - `'percent'` → the typed `battery` cap (percentage),
94916
- * - `'voltage'` → a `numeric-sensor` cap carrying volts (unit `'V'`),
94917
- * - `'none'` → no battery cap (no battery, or a V-battery on a sensor
94918
- * whose primary cap is already `numeric-sensor`).
95332
+ * - `'voltage'` → `battery.volts` (percentage stays null),
95333
+ * - `'none'` → no battery cap.
94919
95334
  */
94920
95335
  batteryMode = "none";
94921
95336
  snapshotUnsub = null;
@@ -94927,10 +95342,13 @@ var EcowittSensorDevice = class extends BaseDevice {
94927
95342
  const cap = capForQuantity(persisted.quantity);
94928
95343
  this.capName = SUPPORTED_CAPS.has(cap) ? cap : "numeric-sensor";
94929
95344
  this.valueKey = valueFieldForCap(this.capName);
95345
+ this.ecowittQuantity = String(persisted.quantity);
94930
95346
  this.online = true;
95347
+ const named = readingQuantityFor(persisted.quantity);
94931
95348
  this.updateSourceInfo({
94932
95349
  id: this.sensorId,
94933
- system: "ecowitt"
95350
+ system: "ecowitt",
95351
+ ...this.capName === "numeric-sensor" && named !== null ? { quantity: named } : {}
94934
95352
  });
94935
95353
  }
94936
95354
  resolveSensor() {
@@ -94974,10 +95392,10 @@ var EcowittSensorDevice = class extends BaseDevice {
94974
95392
  const sensor = this.resolveSensor();
94975
95393
  if (sensor === null) return;
94976
95394
  const lastFetchedAt = sensor.lastUpdated || Date.now();
95395
+ const stored = this.toBase(sensor.value, sensor.unit);
94977
95396
  const slice = {
94978
- [this.valueKey]: clampForCap(this.capName, sensor.value),
94979
- lastFetchedAt,
94980
- ...sensor.unit.length > 0 ? { unit: sensor.unit } : {}
95397
+ [this.valueKey]: stored === null ? null : clampForCap(this.capName, stored),
95398
+ lastFetchedAt
94981
95399
  };
94982
95400
  this.runtimeState.setCapState(this.capName, slice);
94983
95401
  if (this.batteryMode === "percent" && sensor.batteryUnit === "%" && typeof sensor.battery === "number") {
@@ -94991,11 +95409,13 @@ var EcowittSensorDevice = class extends BaseDevice {
94991
95409
  }
94992
95410
  if (this.batteryMode === "voltage" && sensor.batteryUnit === "V" && typeof sensor.battery === "number" && Number.isFinite(sensor.battery)) {
94993
95411
  const next = {
94994
- value: sensor.battery,
94995
- unit: "V",
94996
- lastFetchedAt
95412
+ percentage: null,
95413
+ volts: sensor.battery,
95414
+ charging: "none",
95415
+ sleeping: false,
95416
+ lastUpdated: lastFetchedAt
94997
95417
  };
94998
- this.runtimeState.setCapState(BATTERY_VOLTAGE_CAP, next);
95418
+ this.runtimeState.setCapState("battery", next);
94999
95419
  }
95000
95420
  }
95001
95421
  registerSensorCap() {
@@ -95048,11 +95468,8 @@ var EcowittSensorDevice = class extends BaseDevice {
95048
95468
  }
95049
95469
  }
95050
95470
  /**
95051
- * Register the per-sensor battery cap matching the sensor's battery form:
95052
- * - `'%'` → typed `battery` cap (percentage),
95053
- * - `'V'` → `numeric-sensor` cap carrying volts (unit `'V'`), unless the
95054
- * sensor's primary cap is already `numeric-sensor` (slice-key collision),
95055
- * in which case it is skipped with a debug note.
95471
+ * Register the `battery` cap. Percent batteries fill `percentage`. Voltage
95472
+ * batteries fill `volts` and leave `percentage` null.
95056
95473
  */
95057
95474
  maybeRegisterBatteryCap() {
95058
95475
  const sensor = this.resolveSensor();
@@ -95079,29 +95496,63 @@ var EcowittSensorDevice = class extends BaseDevice {
95079
95496
  this.runtimeState.setCapState("battery", COLD);
95080
95497
  this.batteryMode = "percent";
95081
95498
  }
95082
- /**
95083
- * Register a `numeric-sensor` (unit `'V'`) cap for a raw-voltage battery. The
95084
- * voltage has no honest percentage, so the percentage-shaped `battery` cap is
95085
- * not used. Skipped when the primary cap already occupies the `numeric-sensor`
95086
- * slice on this device (a second instance would collide).
95087
- */
95499
+ /** Voltage batteries share the `battery` cap. `percentage` stays unknown. */
95088
95500
  registerVoltageBatteryCap() {
95089
- if (this.capName === "numeric-sensor") {
95090
- this.ctx.logger.debug("ecowitt sensor: V-battery not surfaced — primary cap already numeric-sensor", { meta: { sensorId: this.sensorId } });
95091
- return;
95092
- }
95093
95501
  const COLD = {
95094
- value: 0,
95095
- unit: "V",
95096
- lastFetchedAt: 0
95502
+ percentage: null,
95503
+ volts: null,
95504
+ charging: "unknown",
95505
+ sleeping: false,
95506
+ lastUpdated: 0
95097
95507
  };
95098
- this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(BATTERY_VOLTAGE_CAP) ?? COLD });
95099
- this.runtimeState.setCapState(BATTERY_VOLTAGE_CAP, COLD);
95508
+ this.ctx.registerNativeCap(batteryCapability, {
95509
+ getStatus: async () => this.runtimeState.getCapState("battery") ?? COLD,
95510
+ wakeForStream: async () => ({
95511
+ awoke: true,
95512
+ durationMs: 0
95513
+ })
95514
+ });
95515
+ this.runtimeState.setCapState("battery", COLD);
95100
95516
  this.batteryMode = "voltage";
95101
95517
  }
95102
95518
  readSlice(fallback) {
95103
95519
  return this.runtimeState.getCapState(this.capName) ?? fallback;
95104
95520
  }
95521
+ /** Station number → the quantity's base. An unknown unit publishes null. */
95522
+ toBase(value, unit) {
95523
+ const named = readingQuantityFor(this.ecowittQuantity);
95524
+ const fromUnit = unit.length === 0 ? void 0 : quantityFromUnit(unit);
95525
+ const quantity = named ?? (unit.length === 0 ? "none" : fromUnit ?? null);
95526
+ if (quantity === null || quantity === "device") {
95527
+ this.ctx.logger.warn("numeric reading refused", {
95528
+ tags: { deviceId: this.id },
95529
+ meta: {
95530
+ quantity: "device",
95531
+ unit,
95532
+ reason: "unknown-unit"
95533
+ }
95534
+ });
95535
+ return null;
95536
+ }
95537
+ const result = ingestReading({
95538
+ value,
95539
+ rawUnit: unit.length > 0 ? unit : void 0,
95540
+ quantity
95541
+ });
95542
+ if (!result.ok) {
95543
+ this.ctx.logger.warn("numeric reading refused", {
95544
+ tags: { deviceId: this.id },
95545
+ meta: {
95546
+ quantity,
95547
+ unit,
95548
+ reason: result.reason
95549
+ }
95550
+ });
95551
+ return null;
95552
+ }
95553
+ if (this.capName === "numeric-sensor" && this.sourceInfo.quantity !== quantity) this.updateSourceInfo({ quantity });
95554
+ return result.value;
95555
+ }
95105
95556
  };
95106
95557
  /** Clamp a raw SI value to the cap's slice constraints (humidity/lux are bounded). */
95107
95558
  function clampForCap(cap, value) {