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