@camstack/addon-provider-dreame 0.2.155 → 0.2.156

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 +1043 -522
  2. package/dist/addon.mjs +1043 -522
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5479,7 +5479,7 @@ var ZodIssueCode$1 = {
5479
5479
  var ZodFirstPartyTypeKind$1;
5480
5480
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5481
5481
  //#endregion
5482
- //#region ../types/dist/sleep-BErAxhul.mjs
5482
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5483
5483
  /**
5484
5484
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5485
5485
  * window to float samples (D455).
@@ -8033,6 +8033,99 @@ var RecordKindDeclarationSchema = object({
8033
8033
  }).strict();
8034
8034
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8035
8035
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8036
+ var READING_QUANTITIES = [
8037
+ "temperature",
8038
+ "pressure",
8039
+ "speed",
8040
+ "precipitation",
8041
+ "precipitation-rate",
8042
+ "length",
8043
+ "area",
8044
+ "illuminance",
8045
+ "irradiance",
8046
+ "power",
8047
+ "energy",
8048
+ "voltage",
8049
+ "current",
8050
+ "apparent-power",
8051
+ "humidity",
8052
+ "percentage",
8053
+ "concentration-volume",
8054
+ "concentration-mass",
8055
+ "concentration-ppb",
8056
+ "volume",
8057
+ "duration",
8058
+ "duration-ms",
8059
+ "information",
8060
+ "sound-level",
8061
+ "signal-level",
8062
+ "colour-temperature",
8063
+ "angle",
8064
+ "bitrate",
8065
+ "rate",
8066
+ "mass",
8067
+ "index",
8068
+ "ratio",
8069
+ "count",
8070
+ "none",
8071
+ "device"
8072
+ ];
8073
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8074
+ new Set(READING_QUANTITIES);
8075
+ /**
8076
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8077
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8078
+ */
8079
+ var QUANTITY_BASE = {
8080
+ temperature: "°C",
8081
+ pressure: "hPa",
8082
+ speed: "m/s",
8083
+ precipitation: "mm",
8084
+ "precipitation-rate": "mm/h",
8085
+ length: "m",
8086
+ area: "m²",
8087
+ illuminance: "lx",
8088
+ irradiance: "W/m²",
8089
+ power: "W",
8090
+ energy: "kWh",
8091
+ voltage: "V",
8092
+ current: "A",
8093
+ "apparent-power": "VA",
8094
+ humidity: "%",
8095
+ percentage: "%",
8096
+ "concentration-volume": "ppm",
8097
+ "concentration-mass": "µg/m³",
8098
+ "concentration-ppb": "ppb",
8099
+ volume: "L",
8100
+ duration: "s",
8101
+ "duration-ms": "ms",
8102
+ information: "B",
8103
+ "sound-level": "dBFS",
8104
+ "signal-level": "dBm",
8105
+ "colour-temperature": "mired",
8106
+ angle: "°",
8107
+ bitrate: "kbps",
8108
+ rate: "fps",
8109
+ mass: "kg",
8110
+ index: "none",
8111
+ ratio: "none",
8112
+ count: "none",
8113
+ none: "none",
8114
+ device: "device"
8115
+ };
8116
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8117
+ function baseUnitOf(quantity) {
8118
+ const symbol = QUANTITY_BASE[quantity];
8119
+ if (symbol === "none" || symbol === "device") return void 0;
8120
+ return symbol;
8121
+ }
8122
+ function readingMeta(quantity, extras) {
8123
+ return {
8124
+ quantity,
8125
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8126
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8127
+ };
8128
+ }
8036
8129
  /** Shared Zod schemas used across streaming capabilities. */
8037
8130
  var CamProfileSchema = _enum([
8038
8131
  "high",
@@ -8060,8 +8153,8 @@ var CamStreamKindSchema = _enum([
8060
8153
  "derived"
8061
8154
  ]);
8062
8155
  var CamStreamResolutionSchema = object({
8063
- width: number().int().positive(),
8064
- height: number().int().positive()
8156
+ width: number().int().positive().meta(readingMeta("none")),
8157
+ height: number().int().positive().meta(readingMeta("none"))
8065
8158
  });
8066
8159
  var CameraStreamSchema = object({
8067
8160
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8708,14 +8801,6 @@ Object.freeze({
8708
8801
  {
8709
8802
  "path": "aqi",
8710
8803
  "valueType": "number"
8711
- },
8712
- {
8713
- "path": "unit",
8714
- "valueType": "string"
8715
- },
8716
- {
8717
- "path": "precision",
8718
- "valueType": "number"
8719
8804
  }
8720
8805
  ],
8721
8806
  "alarm-panel": [
@@ -8751,20 +8836,10 @@ Object.freeze({
8751
8836
  "valueType": "boolean"
8752
8837
  }
8753
8838
  ],
8754
- "ambient-light-sensor": [
8755
- {
8756
- "path": "lux",
8757
- "valueType": "number"
8758
- },
8759
- {
8760
- "path": "unit",
8761
- "valueType": "string"
8762
- },
8763
- {
8764
- "path": "precision",
8765
- "valueType": "number"
8766
- }
8767
- ],
8839
+ "ambient-light-sensor": [{
8840
+ "path": "lux",
8841
+ "valueType": "number"
8842
+ }],
8768
8843
  "audio-metrics": [
8769
8844
  {
8770
8845
  "path": "ts",
@@ -8844,6 +8919,10 @@ Object.freeze({
8844
8919
  "path": "lastUpdated",
8845
8920
  "valueType": "number"
8846
8921
  },
8922
+ {
8923
+ "path": "volts",
8924
+ "valueType": "number"
8925
+ },
8847
8926
  {
8848
8927
  "path": "lastContactAt",
8849
8928
  "valueType": "number"
@@ -9086,10 +9165,6 @@ Object.freeze({
9086
9165
  "datetime"
9087
9166
  ]
9088
9167
  },
9089
- {
9090
- "path": "unit",
9091
- "valueType": "string"
9092
- },
9093
9168
  {
9094
9169
  "path": "min",
9095
9170
  "valueType": "number"
@@ -9101,19 +9176,6 @@ Object.freeze({
9101
9176
  {
9102
9177
  "path": "step",
9103
9178
  "valueType": "number"
9104
- },
9105
- {
9106
- "path": "precision",
9107
- "valueType": "number"
9108
- },
9109
- {
9110
- "path": "format",
9111
- "valueType": "enum",
9112
- "enumValues": [
9113
- "date",
9114
- "time",
9115
- "datetime"
9116
- ]
9117
9179
  }
9118
9180
  ],
9119
9181
  "cover": [
@@ -9354,13 +9416,8 @@ Object.freeze({
9354
9416
  "path": "value",
9355
9417
  "valueType": "string"
9356
9418
  }, {
9357
- "path": "format",
9358
- "valueType": "enum",
9359
- "enumValues": [
9360
- "date",
9361
- "time",
9362
- "datetime"
9363
- ]
9419
+ "path": "timestamp",
9420
+ "valueType": "boolean"
9364
9421
  }],
9365
9422
  "event-emitter": [
9366
9423
  {
@@ -9467,20 +9524,10 @@ Object.freeze({
9467
9524
  "valueType": "number"
9468
9525
  }
9469
9526
  ],
9470
- "humidity-sensor": [
9471
- {
9472
- "path": "percent",
9473
- "valueType": "number"
9474
- },
9475
- {
9476
- "path": "unit",
9477
- "valueType": "string"
9478
- },
9479
- {
9480
- "path": "precision",
9481
- "valueType": "number"
9482
- }
9483
- ],
9527
+ "humidity-sensor": [{
9528
+ "path": "percent",
9529
+ "valueType": "number"
9530
+ }],
9484
9531
  "image": [{
9485
9532
  "path": "url",
9486
9533
  "valueType": "string"
@@ -9819,20 +9866,10 @@ Object.freeze({
9819
9866
  "valueType": "boolean"
9820
9867
  }
9821
9868
  ],
9822
- "numeric-sensor": [
9823
- {
9824
- "path": "value",
9825
- "valueType": "number"
9826
- },
9827
- {
9828
- "path": "unit",
9829
- "valueType": "string"
9830
- },
9831
- {
9832
- "path": "precision",
9833
- "valueType": "number"
9834
- }
9835
- ],
9869
+ "numeric-sensor": [{
9870
+ "path": "value",
9871
+ "valueType": "number"
9872
+ }],
9836
9873
  "pet-feeder": [
9837
9874
  {
9838
9875
  "path": "foodLevel",
@@ -9924,14 +9961,6 @@ Object.freeze({
9924
9961
  {
9925
9962
  "path": "powerFactor",
9926
9963
  "valueType": "number"
9927
- },
9928
- {
9929
- "path": "unit",
9930
- "valueType": "string"
9931
- },
9932
- {
9933
- "path": "precision",
9934
- "valueType": "number"
9935
9964
  }
9936
9965
  ],
9937
9966
  "presence": [
@@ -9960,20 +9989,10 @@ Object.freeze({
9960
9989
  "valueType": "number"
9961
9990
  }
9962
9991
  ],
9963
- "pressure-sensor": [
9964
- {
9965
- "path": "hpa",
9966
- "valueType": "number"
9967
- },
9968
- {
9969
- "path": "unit",
9970
- "valueType": "string"
9971
- },
9972
- {
9973
- "path": "precision",
9974
- "valueType": "number"
9975
- }
9976
- ],
9992
+ "pressure-sensor": [{
9993
+ "path": "hpa",
9994
+ "valueType": "number"
9995
+ }],
9977
9996
  "privacy-mask": [{
9978
9997
  "path": "enabled",
9979
9998
  "valueType": "boolean"
@@ -10284,20 +10303,10 @@ Object.freeze({
10284
10303
  "path": "tampered",
10285
10304
  "valueType": "boolean"
10286
10305
  }],
10287
- "temperature-sensor": [
10288
- {
10289
- "path": "celsius",
10290
- "valueType": "number"
10291
- },
10292
- {
10293
- "path": "unit",
10294
- "valueType": "string"
10295
- },
10296
- {
10297
- "path": "precision",
10298
- "valueType": "number"
10299
- }
10300
- ],
10306
+ "temperature-sensor": [{
10307
+ "path": "celsius",
10308
+ "valueType": "number"
10309
+ }],
10301
10310
  "update": [
10302
10311
  {
10303
10312
  "path": "currentVersion",
@@ -10408,10 +10417,6 @@ Object.freeze({
10408
10417
  "path": "temperature",
10409
10418
  "valueType": "number"
10410
10419
  },
10411
- {
10412
- "path": "temperatureUnit",
10413
- "valueType": "string"
10414
- },
10415
10420
  {
10416
10421
  "path": "humidity",
10417
10422
  "valueType": "number"
@@ -10420,18 +10425,10 @@ Object.freeze({
10420
10425
  "path": "pressure",
10421
10426
  "valueType": "number"
10422
10427
  },
10423
- {
10424
- "path": "pressureUnit",
10425
- "valueType": "string"
10426
- },
10427
10428
  {
10428
10429
  "path": "windSpeed",
10429
10430
  "valueType": "number"
10430
10431
  },
10431
- {
10432
- "path": "windSpeedUnit",
10433
- "valueType": "string"
10434
- },
10435
10432
  {
10436
10433
  "path": "windBearing",
10437
10434
  "valueType": "number"
@@ -17909,7 +17906,8 @@ var CompositionProblemSchema = object({
17909
17906
  "item-field-reserved",
17910
17907
  "target-missing",
17911
17908
  "target-unreadable",
17912
- "not-replaceable"
17909
+ "not-replaceable",
17910
+ "native-capability-replace"
17913
17911
  ]),
17914
17912
  path: string(),
17915
17913
  message: string()
@@ -17967,9 +17965,11 @@ var CompositionFieldPlanSchema = object({
17967
17965
  reads: array(CompositionFieldReadSchema)
17968
17966
  });
17969
17967
  /**
17970
- * What a feature does to its target (D663): `new` builds a capability on a new
17971
- * device; on an existing device it `add`s a capability the device lacks, or
17972
- * `replace`s named fields of one it has natively.
17968
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17969
+ * new device; on an existing device it `add`s a capability the device lacks.
17970
+ * Replacing a capability the device already serves is refused
17971
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17972
+ * wire for a claim the hub already holds.
17973
17973
  */
17974
17974
  var CompositionFeatureModeSchema = _enum([
17975
17975
  "new",
@@ -19855,12 +19855,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19855
19855
  * providers detect renames by matching `uniqueId` and patch `id`
19856
19856
  * in-place so dispatch keeps working.
19857
19857
  *
19858
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19859
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19860
- * state push and are the single source of truth. The `device_class` tag is
19861
- * an HA-specific input that is consumed at the discovery boundary
19862
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19863
- * stored after that point.
19858
+ * **Display unit and precision are NOT stored here.** They are a read-time
19859
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
19860
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
19861
+ * neither is repeated on each sample. The `device_class` tag is an
19862
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
19863
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19864
19864
  *
19865
19865
  * The `raw` escape hatch carries provider-specific attributes that don't
19866
19866
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19874,6 +19874,14 @@ var SourceInfoSchema = object({
19874
19874
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19875
19875
  * Used to detect rename when `id` changes. */
19876
19876
  uniqueId: string().optional(),
19877
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
19878
+ quantity: ReadingQuantitySchema.optional(),
19879
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
19880
+ dateTimeShape: _enum([
19881
+ "date",
19882
+ "time",
19883
+ "datetime"
19884
+ ]).optional(),
19877
19885
  /** Provider-specific extras that don't fit the structured slots.
19878
19886
  * Whitelist what goes here per provider — do NOT dump entire HA
19879
19887
  * attribute blobs (would explode the meta JSON column). */
@@ -19919,6 +19927,80 @@ function mergeSourceInfo(prev, patch) {
19919
19927
  return next;
19920
19928
  }
19921
19929
  /**
19930
+ * Coerce common unit-of-measurement string variants to a canonical form
19931
+ * so the UI's display templates can rely on stable strings. Pure
19932
+ * passthrough for unknown units — kept conservative on purpose, the goal
19933
+ * is to normalise the long tail of HA `unit_of_measurement` values that
19934
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
19935
+ *
19936
+ * Extend the map as we encounter real-world data. Returning the raw
19937
+ * input for unknown values is intentional: providers should never
19938
+ * lose information silently here.
19939
+ */
19940
+ function normalizeUnit(raw) {
19941
+ if (raw == null) return void 0;
19942
+ const trimmed = raw.trim();
19943
+ if (trimmed === "") return void 0;
19944
+ const lower = trimmed.toLowerCase();
19945
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
19946
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
19947
+ if (lower === "k" || lower === "kelvin") return "K";
19948
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
19949
+ if (lower === "lx" || lower === "lux") return "lx";
19950
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
19951
+ if (lower === "kpa") return "kPa";
19952
+ if (lower === "pa") return "Pa";
19953
+ if (lower === "mbar" || lower === "millibar") return "mbar";
19954
+ if (lower === "w" || lower === "watt") return "W";
19955
+ if (lower === "kw" || lower === "kilowatt") return "kW";
19956
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
19957
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
19958
+ if (lower === "v" || lower === "volt") return "V";
19959
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
19960
+ if (lower === "ma" || lower === "milliampere") return "mA";
19961
+ if (lower === "va") return "VA";
19962
+ if (lower === "ppm") return "ppm";
19963
+ if (lower === "ppb") return "ppb";
19964
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
19965
+ if (lower === "km/h" || lower === "kph") return "km/h";
19966
+ if (lower === "mph") return "mph";
19967
+ if (lower === "m/s") return "m/s";
19968
+ if (lower === "m") return "m";
19969
+ if (lower === "m²" || lower === "m2") return "m²";
19970
+ if (lower === "mm") return "mm";
19971
+ if (lower === "cm") return "cm";
19972
+ if (lower === "km") return "km";
19973
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
19974
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
19975
+ if (lower === "kg") return "kg";
19976
+ if (lower === "g") return "g";
19977
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
19978
+ if (lower === "ml") return "mL";
19979
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
19980
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
19981
+ if (lower === "ms") return "ms";
19982
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
19983
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
19984
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
19985
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
19986
+ if (lower === "kib") return "KiB";
19987
+ if (lower === "mib") return "MiB";
19988
+ if (lower === "gib") return "GiB";
19989
+ if (lower === "tib") return "TiB";
19990
+ if (lower === "kb") return "kB";
19991
+ if (lower === "mb") return "MB";
19992
+ if (lower === "gb") return "GB";
19993
+ if (lower === "dbm") return "dBm";
19994
+ if (lower === "dbfs") return "dBFS";
19995
+ if (lower === "inhg") return "inHg";
19996
+ if (lower === "mm/h") return "mm/h";
19997
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
19998
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
19999
+ if (lower === "kbps") return "kbps";
20000
+ if (lower === "fps") return "fps";
20001
+ return trimmed;
20002
+ }
20003
+ /**
19922
20004
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
19923
20005
  * blob is a free-form `Record<string, unknown>` shared with hardware-
19924
20006
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -40995,7 +41077,8 @@ method(object({
40995
41077
  "enum"
40996
41078
  ]),
40997
41079
  enumValues: array(string()).optional(),
40998
- item: boolean().optional()
41080
+ item: boolean().optional(),
41081
+ quantity: ReadingQuantitySchema.optional()
40999
41082
  })).readonly(),
41000
41083
  itemArray: object({
41001
41084
  path: string(),
@@ -44851,8 +44934,8 @@ var MaskPolygonShapeSchema = object({
44851
44934
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44852
44935
  var MaskGridShapeSchema = object({
44853
44936
  kind: literal("grid"),
44854
- gridWidth: number(),
44855
- gridHeight: number(),
44937
+ gridWidth: number().meta(readingMeta("none")),
44938
+ gridHeight: number().meta(readingMeta("none")),
44856
44939
  cells: array(boolean())
44857
44940
  });
44858
44941
  discriminatedUnion("kind", [
@@ -56332,27 +56415,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56332
56415
  */
56333
56416
  var AirQualitySensorStatusSchema = object({
56334
56417
  /** Carbon dioxide concentration in ppm. */
56335
- co2Ppm: number().min(0).optional(),
56418
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56336
56419
  /** Total volatile organic compounds in ppb. */
56337
- vocPpb: number().min(0).optional(),
56420
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56338
56421
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56339
- pm25: number().min(0).optional(),
56422
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56340
56423
  /** Particulate matter ≤ 10 μm in µg/m³. */
56341
- pm10: number().min(0).optional(),
56424
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56342
56425
  /** Composite AQI value (typically 0..500). */
56343
- aqi: number().optional(),
56426
+ aqi: number().optional().meta(readingMeta("index")),
56344
56427
  /** Ms epoch when the slice was last updated. */
56345
- lastFetchedAt: number(),
56346
- /** Live display unit of the single metric this slice carries (e.g. HA
56347
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56348
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56349
- * per slice is unambiguous. */
56350
- unit: string().optional(),
56351
- /** Suggested decimal places for numeric display.
56352
- * Populated live from the upstream source when provided (e.g. HA
56353
- * `attributes.suggested_display_precision`). Falls back to
56354
- * auto-formatting when absent. */
56355
- precision: number().int().min(0).max(10).optional()
56428
+ lastFetchedAt: number()
56356
56429
  });
56357
56430
  var airQualitySensorCapability = {
56358
56431
  name: "air-quality-sensor",
@@ -56389,12 +56462,12 @@ var airQualitySensorCapability = {
56389
56462
  },
56390
56463
  pm25: {
56391
56464
  kind: "gauge",
56392
- unit: "ug/m3",
56465
+ unit: "µg/m³",
56393
56466
  deadband: 1
56394
56467
  },
56395
56468
  pm10: {
56396
56469
  kind: "gauge",
56397
- unit: "ug/m3",
56470
+ unit: "µg/m³",
56398
56471
  deadband: 1
56399
56472
  },
56400
56473
  aqi: {
@@ -56412,22 +56485,13 @@ var airQualitySensorCapability = {
56412
56485
  * entries with `device_class: illuminance`.
56413
56486
  */
56414
56487
  var AmbientLightSensorStatusSchema = object({
56415
- /** Current illuminance in lux (lx). */
56416
- lux: number().min(0).meta({
56417
- unit: "lx",
56488
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56489
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56490
+ precision: 0,
56418
56491
  label: "Illuminance"
56419
- }),
56492
+ })),
56420
56493
  /** Ms epoch when the slice was last updated. */
56421
- lastFetchedAt: number(),
56422
- /** Live display unit from the upstream source (e.g. HA
56423
- * `attributes.unit_of_measurement`). The UI prefers this over the
56424
- * role's canonical unit. Absent → fall back to the canonical unit. */
56425
- unit: string().optional(),
56426
- /** Suggested decimal places for numeric display.
56427
- * Populated live from the upstream source when provided (e.g. HA
56428
- * `attributes.suggested_display_precision`). Falls back to
56429
- * auto-formatting when absent. */
56430
- precision: number().int().min(0).max(10).optional()
56494
+ lastFetchedAt: number()
56431
56495
  });
56432
56496
  var ambientLightSensorCapability = {
56433
56497
  name: "ambient-light-sensor",
@@ -56459,12 +56523,7 @@ var ambientLightSensorCapability = {
56459
56523
  } } },
56460
56524
  /** Clock fields: written, but excluded from the compare that decides
56461
56525
  * whether persisting is worth a SQLite commit. */
56462
- volatileStateFields: ["lastFetchedAt"],
56463
- customize: {
56464
- version: 1,
56465
- advanced: "derive",
56466
- targets: []
56467
- }
56526
+ volatileStateFields: ["lastFetchedAt"]
56468
56527
  };
56469
56528
  /**
56470
56529
  * Per-class audio metrics aggregated over a sliding window.
@@ -56691,10 +56750,7 @@ var BatteryStatusSchema = object({
56691
56750
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56692
56751
  * declines to band a non-finite reading, which is the correct shape.
56693
56752
  */
56694
- percentage: number().min(0).max(100).nullable().meta({
56695
- unit: "%",
56696
- label: "Charge"
56697
- }),
56753
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56698
56754
  /**
56699
56755
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56700
56756
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56721,7 +56777,14 @@ var BatteryStatusSchema = object({
56721
56777
  */
56722
56778
  sleeping: boolean().meta({ label: "Sleeping" }),
56723
56779
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56724
- lastUpdated: number(),
56780
+ lastUpdated: number().meta(readingMeta("none")),
56781
+ /**
56782
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56783
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56784
+ * that only report charge. A second reading on this cap, not a second
56785
+ * `numeric-sensor` on the same device.
56786
+ */
56787
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56725
56788
  /**
56726
56789
  * Ms epoch of the last time the device PROVED it was reachable — a
56727
56790
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56773,7 +56836,6 @@ var BATTERY_DEVICE_TYPES = [
56773
56836
  /** Generic Customize target for the percentage reading (D936). */
56774
56837
  var batteryCustomize = {
56775
56838
  version: 1,
56776
- advanced: "derive",
56777
56839
  targets: [{
56778
56840
  id: "percentage",
56779
56841
  cardinality: "scalar",
@@ -56782,7 +56844,7 @@ var batteryCustomize = {
56782
56844
  stateField: "percentage"
56783
56845
  },
56784
56846
  deviceTypes: BATTERY_DEVICE_TYPES,
56785
- apply: "add-or-replace",
56847
+ apply: "add",
56786
56848
  path: "percentage",
56787
56849
  sources: [{
56788
56850
  kind: "field",
@@ -56903,10 +56965,16 @@ onStatusChanged: { data: object({
56903
56965
  /**
56904
56966
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56905
56967
  */
56906
- history: { fields: { percentage: {
56907
- kind: "gauge",
56908
- unit: "%"
56909
- } } },
56968
+ history: { fields: {
56969
+ percentage: {
56970
+ kind: "gauge",
56971
+ unit: "%"
56972
+ },
56973
+ volts: {
56974
+ kind: "gauge",
56975
+ unit: "V"
56976
+ }
56977
+ } },
56910
56978
  customize: batteryCustomize,
56911
56979
  /** Clock fields: written, but excluded from the compare that decides
56912
56980
  * whether persisting is worth a SQLite commit. */
@@ -56953,12 +57021,7 @@ var binaryCapability = {
56953
57021
  history: { fields: { on: { kind: "boolean" } } },
56954
57022
  /** Clock fields: written, but excluded from the compare that decides
56955
57023
  * whether persisting is worth a SQLite commit. */
56956
- volatileStateFields: ["lastChangedAt"],
56957
- customize: {
56958
- version: 1,
56959
- advanced: "derive",
56960
- targets: []
56961
- }
57024
+ volatileStateFields: ["lastChangedAt"]
56962
57025
  };
56963
57026
  /**
56964
57027
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -56974,7 +57037,7 @@ var binaryCapability = {
56974
57037
  */
56975
57038
  var BrightnessStatusSchema = object({
56976
57039
  /** Current level as 0..100 inclusive. Firmware-reported. */
56977
- percentage: number().min(0).max(100),
57040
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56978
57041
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56979
57042
  lastChangedAt: number()
56980
57043
  });
@@ -57445,21 +57508,21 @@ var ClimateControlStatusSchema = object({
57445
57508
  /** Single setpoint in Celsius. Used by single-target modes
57446
57509
  * (heat / cool / dry). Null when the device is in a range mode
57447
57510
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57448
- target: number().nullable(),
57511
+ target: number().nullable().meta(readingMeta("temperature")),
57449
57512
  /** Upper bound of the dual setpoint in Celsius. Populated only
57450
57513
  * in `heat_cool` mode. */
57451
- targetHigh: number().nullable(),
57514
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57452
57515
  /** Lower bound of the dual setpoint in Celsius. Populated only
57453
57516
  * in `heat_cool` mode. */
57454
- targetLow: number().nullable(),
57517
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57455
57518
  /** Target relative humidity (0..100). Null when the device has no
57456
57519
  * humidity control. */
57457
- targetHumidity: number().min(0).max(100).nullable(),
57520
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57458
57521
  /** Current measured temperature in Celsius. Read-only — pushed by
57459
57522
  * the device's internal sensor. */
57460
- currentTemp: number().nullable(),
57523
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57461
57524
  /** Current measured relative humidity (0..100). Read-only. */
57462
- currentHumidity: number().min(0).max(100).nullable(),
57525
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57463
57526
  /** Vertical louver swing on/off. Null when the device has no vertical
57464
57527
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57465
57528
  swingVertical: boolean().nullable(),
@@ -57467,11 +57530,11 @@ var ClimateControlStatusSchema = object({
57467
57530
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57468
57531
  swingHorizontal: boolean().nullable(),
57469
57532
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57470
- minTemp: number().nullable().optional(),
57533
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57471
57534
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57472
- maxTemp: number().nullable().optional(),
57535
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57473
57536
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57474
- tempStep: number().positive().nullable().optional(),
57537
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57475
57538
  /** Ms epoch when the slice was last updated (push or command). */
57476
57539
  lastFetchedAt: number()
57477
57540
  });
@@ -57613,17 +57676,17 @@ var climateControlCapability = {
57613
57676
  * modes to expose in the picker without a slice fetch.
57614
57677
  */
57615
57678
  var RgbTripletSchema = object({
57616
- r: number().int().min(0).max(255),
57617
- g: number().int().min(0).max(255),
57618
- b: number().int().min(0).max(255)
57679
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57680
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57681
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57619
57682
  });
57620
57683
  var HsvTripletSchema = object({
57621
57684
  /** Hue in degrees, 0..360. */
57622
- h: number().min(0).max(360),
57685
+ h: number().min(0).max(360).meta(readingMeta("none")),
57623
57686
  /** Saturation as 0..100 inclusive. */
57624
- s: number().min(0).max(100),
57687
+ s: number().min(0).max(100).meta(readingMeta("none")),
57625
57688
  /** Value/brightness as 0..100 inclusive. */
57626
- v: number().min(0).max(100)
57689
+ v: number().min(0).max(100).meta(readingMeta("none"))
57627
57690
  });
57628
57691
  var ColorInputSchema = discriminatedUnion("mode", [
57629
57692
  object({
@@ -57652,7 +57715,7 @@ var ColorStatusSchema = object({
57652
57715
  /** Populated when `mode === 'hsv'`. */
57653
57716
  hsv: HsvTripletSchema.optional(),
57654
57717
  /** Populated when `mode === 'mired'`. */
57655
- mireds: number().int().optional(),
57718
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57656
57719
  /** Ms epoch of the last operator-driven change. */
57657
57720
  lastChangedAt: number()
57658
57721
  });
@@ -57859,18 +57922,12 @@ _enum([
57859
57922
  function consumableKindOf(item) {
57860
57923
  return item.kind ?? "wear";
57861
57924
  }
57862
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57863
- * each. */
57864
- var ConsumableRemainingUnitSchema = _enum([
57865
- "days",
57866
- "hours",
57867
- "cycles"
57868
- ]);
57869
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57870
- * days). `value` null = unknown (D393). */
57925
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57926
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57927
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57871
57928
  var ConsumableRemainingSchema = object({
57872
- value: number().min(0).nullable(),
57873
- unit: ConsumableRemainingUnitSchema
57929
+ value: number().min(0).nullable().meta(readingMeta("device")),
57930
+ kind: ConsumableRemainingKindSchema
57874
57931
  });
57875
57932
  /** A single consumable item. Either a continuous `level` (remaining
57876
57933
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57883,7 +57940,7 @@ var ConsumableItemSchema = object({
57883
57940
  /** Display name. */
57884
57941
  label: string().min(1),
57885
57942
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57886
- level: number().min(0).max(100).nullable(),
57943
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57887
57944
  /** The provider's discrete verdict when known (binary mode). */
57888
57945
  status: ConsumableStatusSchema.nullable(),
57889
57946
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57965,8 +58022,8 @@ var consumablesCustomize = {
57965
58022
  transforms: [{ kind: "identity" }]
57966
58023
  }],
57967
58024
  literals: [{
57968
- path: "remaining.unit",
57969
- values: [...ConsumableRemainingUnitSchema.options]
58025
+ path: "remaining.kind",
58026
+ values: [...ConsumableRemainingKindSchema.options]
57970
58027
  }]
57971
58028
  },
57972
58029
  {
@@ -58095,7 +58152,6 @@ reset: method(object({
58095
58152
  },
58096
58153
  "remaining.value": {
58097
58154
  kind: "gauge",
58098
- unitFrom: "remaining.unit",
58099
58155
  deadband: 1,
58100
58156
  displayName: "Remaining"
58101
58157
  }
@@ -58165,10 +58221,9 @@ var contactCapability = {
58165
58221
  * (empty array for the other kinds). It lives in the slice because
58166
58222
  * HA `select.*` entities may update their options at runtime.
58167
58223
  *
58168
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58169
- * `kind === 'numeric'` and are absent (undefined) for all other
58170
- * kinds. They are populated live from HA attributes on each state
58171
- * push — consistent with `options` and the sensor-unit approach.
58224
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58225
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58226
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58172
58227
  */
58173
58228
  var ControlStatusSchema = object({
58174
58229
  kind: _enum([
@@ -58180,39 +58235,17 @@ var ControlStatusSchema = object({
58180
58235
  /** Current value. Numeric kind → number; select/text/datetime → string.
58181
58236
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58182
58237
  * or time-only — HA accepts all three forms). */
58183
- value: union([number(), string()]),
58238
+ value: union([number().meta(readingMeta("device")), string()]),
58184
58239
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58185
58240
  options: array(string()),
58186
58241
  /** Ms epoch when the slice was last updated. */
58187
58242
  lastChangedAt: number(),
58188
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58189
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58190
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58191
- unit: string().optional(),
58192
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58193
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58194
- min: number().optional(),
58195
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58196
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58197
- max: number().optional(),
58198
- /** Step increment. Populated live from HA `attributes.step`.
58199
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58200
- step: number().optional(),
58201
- /** Suggested decimal places for numeric display. Populated live from HA
58202
- * `attributes.suggested_display_precision`, or derived from `step` when the
58203
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58204
- * other kinds. Falls back to auto-formatting when absent. */
58205
- precision: number().int().min(0).max(10).optional(),
58206
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58207
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58208
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58209
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58210
- * `has_date`/`has_time` attributes on each push. */
58211
- format: _enum([
58212
- "date",
58213
- "time",
58214
- "datetime"
58215
- ]).optional()
58243
+ /** Minimum allowed value, in the device quantity's base unit. */
58244
+ min: number().optional().meta(readingMeta("device")),
58245
+ /** Maximum allowed value, in the device quantity's base unit. */
58246
+ max: number().optional().meta(readingMeta("device")),
58247
+ /** Step increment, in the device quantity's base unit. */
58248
+ step: number().optional().meta(readingMeta("device"))
58216
58249
  });
58217
58250
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58218
58251
  object({
@@ -58278,9 +58311,9 @@ var CoverStatusSchema = object({
58278
58311
  ]),
58279
58312
  /** 0 = fully closed, 100 = fully open. Null when the device has
58280
58313
  * no intermediate position surface. */
58281
- position: number().min(0).max(100).nullable(),
58314
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58282
58315
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58283
- tiltPosition: number().min(0).max(100).nullable(),
58316
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58284
58317
  /** Ms epoch when the slice was last updated. */
58285
58318
  lastChangedAt: number()
58286
58319
  });
@@ -58383,9 +58416,9 @@ var NormalizedRangeSchema$1 = object({
58383
58416
  var DayNightStatusSchema = object({
58384
58417
  mode: DayNightModeSchema,
58385
58418
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58386
- sensitivity: number().optional(),
58419
+ sensitivity: number().optional().meta(readingMeta("none")),
58387
58420
  /** Delay before the IR-cut filter flips, in seconds. */
58388
- switchDelaySec: number().optional(),
58421
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58389
58422
  lastFetchedAt: number()
58390
58423
  });
58391
58424
  /**
@@ -58603,11 +58636,11 @@ var DishwasherStatusSchema = object({
58603
58636
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58604
58637
  availablePrograms: array(string()),
58605
58638
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58606
- remainingSeconds: number().int().nonnegative().nullable(),
58639
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58607
58640
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58608
- delayedStartSeconds: number().int().nonnegative().nullable(),
58641
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58609
58642
  /** Cycle completion 0..100, or null when not applicable. */
58610
- progressPercent: number().min(0).max(100).nullable(),
58643
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58611
58644
  /** True when the door is reported open; null when the door state is not known. */
58612
58645
  doorOpen: boolean().nullable(),
58613
58646
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58631,7 +58664,7 @@ var DishwasherStatusSchema = object({
58631
58664
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58632
58665
  machineCareDue: boolean().nullable(),
58633
58666
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58634
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58667
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58635
58668
  /** Option states: null when the appliance does not expose the option or has not said. */
58636
58669
  halfLoad: boolean().nullable(),
58637
58670
  hygienePlus: boolean().nullable(),
@@ -58646,7 +58679,7 @@ var DishwasherStatusSchema = object({
58646
58679
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58647
58680
  activeEvents: array(string()),
58648
58681
  /** Numeric error code when the appliance reports one; null otherwise. */
58649
- errorCode: number().nullable(),
58682
+ errorCode: number().nullable().meta(readingMeta("none")),
58650
58683
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58651
58684
  errorLabel: string().nullable(),
58652
58685
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58877,7 +58910,7 @@ var DoorbellStatusSchema = object({
58877
58910
  /** Ms epoch of the last press. null = never observed since this provider started. */
58878
58911
  lastPressedAt: number().nullable(),
58879
58912
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58880
- pressCountSinceStart: number()
58913
+ pressCountSinceStart: number().meta(readingMeta("count"))
58881
58914
  });
58882
58915
  var DoorbellPressEventSchema = object({
58883
58916
  deviceId: number(),
@@ -58942,17 +58975,7 @@ onPressed: {
58942
58975
  history: "none",
58943
58976
  customize: doorbellCustomize
58944
58977
  };
58945
- /**
58946
- * Enum-state sensor — a string value picked from a finite option set.
58947
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58948
- * states, weather conditions, contact-source channels, mode strings).
58949
- * The option set is stable across observations; it is persisted in the
58950
- * device config blob (via the control slice at adoption) and is NOT
58951
- * stored in `sourceInfo`.
58952
- */
58953
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58954
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58955
- var EnumSensorDateTimeFormatSchema = _enum([
58978
+ _enum([
58956
58979
  "date",
58957
58980
  "time",
58958
58981
  "datetime"
@@ -58960,12 +58983,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58960
58983
  var EnumSensorStatusSchema = object({
58961
58984
  value: string(),
58962
58985
  /**
58963
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58964
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58965
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58966
- * a time-only sensor → `'time'`.
58986
+ * Set when `value` is a timestamp rather than an enum state, so history
58987
+ * does not learn ISO strings as states. The display shape (date / time /
58988
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58967
58989
  */
58968
- format: EnumSensorDateTimeFormatSchema.optional(),
58990
+ timestamp: boolean().optional(),
58969
58991
  /** Ms epoch when the slice was last updated. */
58970
58992
  lastFetchedAt: number()
58971
58993
  });
@@ -58992,11 +59014,11 @@ var enumSensorCapability = {
58992
59014
  * History (D850): the value as an `enum` series (an appliance's program or
58993
59015
  * operation state, an HVAC action). The option set is per device, so the
58994
59016
  * value list is LEARNED as values are first seen (bounded). A sensor with a
58995
- * `format` carries a timestamp, not a state, and is not recorded.
59017
+ * `timestamp` carries a clock, not a state, and is not recorded.
58996
59018
  */
58997
59019
  history: { fields: { value: {
58998
59020
  kind: "enum",
58999
- skipWhenSet: "format"
59021
+ skipWhenSet: "timestamp"
59000
59022
  } } },
59001
59023
  /** Clock fields: written, but excluded from the compare that decides
59002
59024
  * whether persisting is worth a SQLite commit. */
@@ -59016,13 +59038,13 @@ var EventFireSchema = object({
59016
59038
  deviceId: number(),
59017
59039
  eventType: string(),
59018
59040
  data: record(string(), unknown()).nullable(),
59019
- timestamp: number(),
59020
- seq: number()
59041
+ timestamp: number().meta(readingMeta("none")),
59042
+ seq: number().meta(readingMeta("count"))
59021
59043
  });
59022
59044
  var EventEmitterStatusSchema = object({
59023
59045
  eventTypes: array(string()),
59024
59046
  lastEvent: EventFireSchema.nullable(),
59025
- eventCountSinceStart: number()
59047
+ eventCountSinceStart: number().meta(readingMeta("count"))
59026
59048
  });
59027
59049
  var eventEmitterCapability = {
59028
59050
  name: "event-emitter",
@@ -59435,10 +59457,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59435
59457
  var FanControlStatusSchema = object({
59436
59458
  /** Active speed as 0..100 inclusive. Null when the device has no
59437
59459
  * speed surface (single-speed fan). */
59438
- percentage: number().min(0).max(100).nullable(),
59460
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59439
59461
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59440
59462
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59441
- percentageStep: number().positive().optional(),
59463
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59442
59464
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59443
59465
  * — empty string when no preset is active or supported. */
59444
59466
  preset: string(),
@@ -59559,7 +59581,7 @@ var FeatureProbeStatusSchema = object({
59559
59581
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59560
59582
  model: string().nullable(),
59561
59583
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59562
- channelCount: number().nullable(),
59584
+ channelCount: number().nullable().meta(readingMeta("count")),
59563
59585
  /**
59564
59586
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59565
59587
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59684,7 +59706,7 @@ var gasCapability = {
59684
59706
  };
59685
59707
  var HeatingValveStatusSchema = object({
59686
59708
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59687
- position: number().min(0).max(100).nullable(),
59709
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59688
59710
  /** Ms epoch when the slice was last updated (push or poll). */
59689
59711
  lastFetchedAt: number()
59690
59712
  });
@@ -59727,9 +59749,9 @@ var HumidifierStatusSchema = object({
59727
59749
  /** Whether the humidifier is currently on. */
59728
59750
  on: boolean(),
59729
59751
  /** Current measured relative humidity (0..100). Null when not reported. */
59730
- currentHumidity: number().min(0).max(100).nullable(),
59752
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59731
59753
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59732
- targetHumidity: number().min(0).max(100).nullable(),
59754
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59733
59755
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59734
59756
  * device has no mode surface. */
59735
59757
  mode: string().nullable(),
@@ -59739,9 +59761,9 @@ var HumidifierStatusSchema = object({
59739
59761
  * `idle` / `off`). Null when the device doesn't report it. */
59740
59762
  action: string().nullable(),
59741
59763
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59742
- minHumidity: number().nullable(),
59764
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59743
59765
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59744
- maxHumidity: number().nullable(),
59766
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59745
59767
  /** Ms epoch when the slice was last updated. */
59746
59768
  lastChangedAt: number()
59747
59769
  });
@@ -59811,22 +59833,13 @@ var humidifierCapability = {
59811
59833
  * `sourceInfo`.
59812
59834
  */
59813
59835
  var HumiditySensorStatusSchema = object({
59814
- /** Current relative humidity, 0..100. */
59815
- percent: number().min(0).max(100).meta({
59816
- unit: "%",
59836
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59837
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59838
+ precision: 0,
59817
59839
  label: "Humidity"
59818
- }),
59840
+ })),
59819
59841
  /** Ms epoch when the slice was last updated. */
59820
- lastFetchedAt: number(),
59821
- /** Live display unit from the upstream source (e.g. HA
59822
- * `attributes.unit_of_measurement`). The UI prefers this over the
59823
- * role's canonical unit. Absent → fall back to the canonical unit. */
59824
- unit: string().optional(),
59825
- /** Suggested decimal places for numeric display.
59826
- * Populated live from the upstream source when provided (e.g. HA
59827
- * `attributes.suggested_display_precision`). Falls back to
59828
- * auto-formatting when absent. */
59829
- precision: number().int().min(0).max(10).optional()
59842
+ lastFetchedAt: number()
59830
59843
  });
59831
59844
  var humiditySensorCapability = {
59832
59845
  name: "humidity-sensor",
@@ -59857,12 +59870,7 @@ var humiditySensorCapability = {
59857
59870
  } } },
59858
59871
  /** Clock fields: written, but excluded from the compare that decides
59859
59872
  * whether persisting is worth a SQLite commit. */
59860
- volatileStateFields: ["lastFetchedAt"],
59861
- customize: {
59862
- version: 1,
59863
- advanced: "derive",
59864
- targets: []
59865
- }
59873
+ volatileStateFields: ["lastFetchedAt"]
59866
59874
  };
59867
59875
  /**
59868
59876
  * Image display cap. Models a single still image exposed by an integration —
@@ -59883,7 +59891,7 @@ var ImageStatusSchema = object({
59883
59891
  * entity exposes no `entity_picture` (yet). */
59884
59892
  url: string().nullable(),
59885
59893
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59886
- lastUpdated: number().nullable()
59894
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59887
59895
  });
59888
59896
  var imageCapability = {
59889
59897
  name: "image",
@@ -59971,19 +59979,19 @@ var NormalizedRangeSchema = object({
59971
59979
  */
59972
59980
  var ImageSettingsStatusSchema = object({
59973
59981
  /** Normalized 0–100. */
59974
- brightness: number().optional(),
59982
+ brightness: number().optional().meta(readingMeta("none")),
59975
59983
  /** Normalized 0–100. */
59976
- contrast: number().optional(),
59984
+ contrast: number().optional().meta(readingMeta("none")),
59977
59985
  /** Normalized 0–100. */
59978
- saturation: number().optional(),
59986
+ saturation: number().optional().meta(readingMeta("none")),
59979
59987
  /** Normalized 0–100. */
59980
- sharpness: number().optional(),
59988
+ sharpness: number().optional().meta(readingMeta("none")),
59981
59989
  mirror: boolean().optional(),
59982
59990
  flip: boolean().optional(),
59983
59991
  rotate: ImageRotateSchema.optional(),
59984
59992
  whiteBalance: WhiteBalanceModeSchema.optional(),
59985
59993
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59986
- warmth: number().optional(),
59994
+ warmth: number().optional().meta(readingMeta("none")),
59987
59995
  exposureMode: ExposureModeSchema.optional(),
59988
59996
  backlightMode: BacklightModeSchema.optional(),
59989
59997
  lastFetchedAt: number()
@@ -60262,10 +60270,10 @@ var IntercomAbilitySchema = object({
60262
60270
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60263
60271
  codecs: array(string()),
60264
60272
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60265
- sampleRate: number(),
60273
+ sampleRate: number().meta(readingMeta("none")),
60266
60274
  duplex: _enum(["half", "full"]),
60267
60275
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60268
- maxBacklogMs: number()
60276
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60269
60277
  });
60270
60278
  var IntercomStatusSchema = object({
60271
60279
  /** True while a talk session is open. */
@@ -60492,13 +60500,13 @@ var LawnMowerControlStatusSchema = object({
60492
60500
  activity: LawnMowerActivitySchema,
60493
60501
  /** 0..100 battery percentage. Null when the device has no battery
60494
60502
  * reading. */
60495
- batteryLevel: number().min(0).max(100).nullable(),
60503
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60496
60504
  /** 0..100 mowing-completion percentage of the current task, or null when no
60497
60505
  * task is active / progress is unavailable. */
60498
- progressPercent: number().min(0).max(100).nullable(),
60506
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60499
60507
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60500
60508
  * or null when unknown. */
60501
- currentCode: number().nullable(),
60509
+ currentCode: number().nullable().meta(readingMeta("none")),
60502
60510
  /** Human label for {@link currentCode}, or null when undecodable. */
60503
60511
  currentCodeLabel: string().nullable(),
60504
60512
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61031,7 +61039,7 @@ var MediaPlayerStatusSchema = object({
61031
61039
  state: MediaPlayerStateSchema,
61032
61040
  /** Volume as 0..100 inclusive. Null when the device has no volume
61033
61041
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61034
- volumeLevel: number().min(0).max(100).nullable(),
61042
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61035
61043
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61036
61044
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61037
61045
  isMuted: boolean().nullable(),
@@ -61044,10 +61052,10 @@ var MediaPlayerStatusSchema = object({
61044
61052
  currentMedia: MediaInfoSchema.nullable(),
61045
61053
  /** Current playback position in ms. Null when not seekable or
61046
61054
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61047
- positionMs: number().int().nonnegative().nullable(),
61055
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61048
61056
  /** Total duration of the current media in ms. Null when unknown
61049
61057
  * (live stream) or nothing is playing. */
61050
- durationMs: number().int().nonnegative().nullable(),
61058
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61051
61059
  /** Shuffle toggle. Null when no shuffle surface.
61052
61060
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61053
61061
  shuffle: boolean().nullable(),
@@ -61356,7 +61364,7 @@ var motionCustomize = {
61356
61364
  eventName: "onMotionChanged"
61357
61365
  },
61358
61366
  deviceTypes: [DeviceType.Camera],
61359
- apply: "replace",
61367
+ apply: "add",
61360
61368
  applyAuthority: {
61361
61369
  kind: "camera-sensors",
61362
61370
  timeoutField: "motionCooldownMs"
@@ -61428,7 +61436,7 @@ var MotionStatusSchema = object({
61428
61436
  * it, every provider seeded it at boot, and every session left it
61429
61437
  * `detected: true` after its detach.
61430
61438
  */
61431
- autoClearAfterMs: number().nullable()
61439
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61432
61440
  });
61433
61441
  /**
61434
61442
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61610,7 +61618,7 @@ var MotionZoneRegionSchema = object({
61610
61618
  * the grid region(s). */
61611
61619
  var MotionZoneStatusSchema = object({
61612
61620
  enabled: boolean(),
61613
- sensitivity: number(),
61621
+ sensitivity: number().meta(readingMeta("none")),
61614
61622
  /** Grid region(s). Today exactly one `grid` shape. */
61615
61623
  regions: array(MotionZoneRegionSchema),
61616
61624
  lastFetchedAt: number()
@@ -62117,7 +62125,7 @@ var NavigationStatusSchema = object({
62117
62125
  following: boolean(),
62118
62126
  flash: boolean(),
62119
62127
  lightMode: NavigationLightModeSchema,
62120
- lightLevel: number().min(40).max(100),
62128
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62121
62129
  /** Ms epoch when the slice was last updated. */
62122
62130
  lastChangedAt: number()
62123
62131
  });
@@ -62251,13 +62259,13 @@ var NetworkLinkStatusSchema = object({
62251
62259
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62252
62260
  * SKIP a null rather than coerce it.
62253
62261
  */
62254
- signalPercent: number().min(0).max(100).nullable(),
62262
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62255
62263
  /** Raw received signal strength in dBm, when the firmware reports one. */
62256
- rssiDbm: number().optional(),
62264
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62257
62265
  /** Network name of a wireless link, when the firmware reports it. */
62258
62266
  ssid: string().optional(),
62259
62267
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62260
- lastUpdated: number()
62268
+ lastUpdated: number().meta(readingMeta("none"))
62261
62269
  });
62262
62270
  /** The slice a provider seeds before its first read: nothing is known yet. */
62263
62271
  var NETWORK_LINK_UNKNOWN = {
@@ -62653,7 +62661,7 @@ var NotifierStatusSchema = object({
62653
62661
  lastError: string().nullable(),
62654
62662
  /** Number of deliveries currently buffered server-side (rate-limited
62655
62663
  * notifiers may queue). 0 when the provider sends synchronously. */
62656
- queueDepth: number().int().nonnegative(),
62664
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62657
62665
  /** Per-feature capability matrix — authoritative source for the
62658
62666
  * compose-form field gating. */
62659
62667
  supports: NotifierSupportsSchema
@@ -62745,11 +62753,9 @@ onSent: { data: object({
62745
62753
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62746
62754
  * for: water flow, distance, weight, frequency, signal strength, …).
62747
62755
  *
62748
- * The `unit` and `precision` fields carry the live display metadata that
62749
- * the upstream source provides with each state push (HA
62750
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62751
- * The UI reads them directly from the slice — they are the single source
62752
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
62756
+ * The quantity is not on this slice. It is chosen when the device is
62757
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
62758
+ * that quantity's base unit. `null` is a refused sample (D393).
62753
62759
  *
62754
62760
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62755
62761
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62758,15 +62764,8 @@ onSent: { data: object({
62758
62764
  * numeric values land here.
62759
62765
  */
62760
62766
  var NumericSensorStatusSchema = object({
62761
- value: number(),
62762
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62763
- * Populated live from the upstream source on each state push.
62764
- * Absent for unitless sensors. */
62765
- unit: string().optional(),
62766
- /** Suggested decimal places for numeric display.
62767
- * Populated live from the upstream source when provided.
62768
- * Falls back to auto-formatting when absent. */
62769
- precision: number().int().min(0).max(10).optional(),
62767
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
62768
+ value: number().nullable().meta(readingMeta("device")),
62770
62769
  /** Ms epoch when the slice was last updated. */
62771
62770
  lastFetchedAt: number()
62772
62771
  });
@@ -62790,11 +62789,11 @@ var numericSensorCapability = {
62790
62789
  */
62791
62790
  durability: "restored",
62792
62791
  /**
62793
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
62792
+ * History (D850): recorded series. The unit is the device quantity's base,
62793
+ * resolved when the series is read — the cap has no single unit.
62794
62794
  */
62795
62795
  history: { fields: { value: {
62796
62796
  kind: "gauge",
62797
- unitFrom: "unit",
62798
62797
  minIntervalMs: 3e4
62799
62798
  } } },
62800
62799
  /** Clock fields: written, but excluded from the compare that decides
@@ -63086,23 +63085,26 @@ var PetFeederStatusSchema = object({
63086
63085
  /** Food currently in the bowl (grams). Null when the device has not
63087
63086
  * reported a reading yet. On dual-hopper models this is the combined
63088
63087
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63089
- foodLevel: number().nullable(),
63088
+ foodLevel: number().nullable().meta(readingMeta("none")),
63090
63089
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63091
63090
  * single-hopper models. */
63092
- food1: number().nullable(),
63091
+ food1: number().nullable().meta(readingMeta("none")),
63093
63092
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63094
63093
  * single-hopper models. */
63095
- food2: number().nullable(),
63094
+ food2: number().nullable().meta(readingMeta("none")),
63096
63095
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63097
63096
  * (`device_class: problem`, on = low). True when the bowl is empty /
63098
63097
  * below the feeder's low threshold. */
63099
63098
  lowFood: boolean(),
63100
63099
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63101
63100
  * device has no battery reading. */
63102
- batteryPower: number().min(0).max(100).nullable(),
63103
- /** Days of desiccant life remaining. Null when the model has no
63104
- * desiccant sensor. */
63105
- desiccantLeftDays: number().nullable(),
63101
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63102
+ /**
63103
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63104
+ * Providers convert from the days the feeder reports. Null when the
63105
+ * model has no desiccant sensor.
63106
+ */
63107
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63106
63108
  /** True while a feed is in progress. */
63107
63109
  feeding: boolean(),
63108
63110
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63113,7 +63115,7 @@ var PetFeederStatusSchema = object({
63113
63115
  * with `errorCode` for consumers that want the raw integer. */
63114
63116
  error: string().nullable(),
63115
63117
  /** Raw device error code (0 / null = no error). */
63116
- errorCode: number().nullable(),
63118
+ errorCode: number().nullable().meta(readingMeta("none")),
63117
63119
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63118
63120
  isDualHopper: boolean(),
63119
63121
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63123,7 +63125,7 @@ var PetFeederStatusSchema = object({
63123
63125
  /** Play a chime when dispensing. */
63124
63126
  feedSound: boolean(),
63125
63127
  /** Speaker / prompt volume level (device-scaled integer). */
63126
- volume: number(),
63128
+ volume: number().meta(readingMeta("none")),
63127
63129
  /** Ms epoch when the slice was last refreshed from the cloud. */
63128
63130
  lastFetchedAt: number()
63129
63131
  });
@@ -63242,7 +63244,7 @@ var petFeederCapability = {
63242
63244
  },
63243
63245
  desiccantLeftDays: {
63244
63246
  kind: "gauge",
63245
- unit: "days"
63247
+ unit: "s"
63246
63248
  }
63247
63249
  } }
63248
63250
  };
@@ -63643,36 +63645,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63643
63645
  */
63644
63646
  var PowerMeterStatusSchema = object({
63645
63647
  /** Instantaneous power draw in watts. */
63646
- watts: number().optional().meta({
63647
- unit: "W",
63648
- label: "Power"
63649
- }),
63648
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63650
63649
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63651
- kwhTotal: number().optional().meta({
63652
- unit: "kWh",
63653
- label: "Energy"
63654
- }),
63650
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63655
63651
  /** Voltage in volts. */
63656
- volts: number().optional(),
63652
+ volts: number().optional().meta(readingMeta("voltage")),
63657
63653
  /** Current in amperes. */
63658
- amps: number().optional(),
63659
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63660
- va: number().optional(),
63654
+ amps: number().optional().meta(readingMeta("current")),
63655
+ /** Apparent power in volt-amperes. */
63656
+ va: number().optional().meta(readingMeta("apparent-power")),
63661
63657
  /** Power factor (0..1) if reported. */
63662
- powerFactor: number().min(0).max(1).optional(),
63658
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63663
63659
  /** Ms epoch when the slice was last updated. */
63664
- lastFetchedAt: number(),
63665
- /** Live display unit of the single metric this slice carries (e.g. HA
63666
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63667
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63668
- * unit per slice is unambiguous. The UI prefers this over the role's
63669
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63670
- unit: string().optional(),
63671
- /** Suggested decimal places for numeric display.
63672
- * Populated live from the upstream source when provided (e.g. HA
63673
- * `attributes.suggested_display_precision`). Falls back to
63674
- * auto-formatting when absent. */
63675
- precision: number().int().min(0).max(10).optional()
63660
+ lastFetchedAt: number()
63676
63661
  });
63677
63662
  var powerMeterCapability = {
63678
63663
  name: "power-meter",
@@ -63736,12 +63721,7 @@ var powerMeterCapability = {
63736
63721
  } },
63737
63722
  /** Clock fields: written, but excluded from the compare that decides
63738
63723
  * whether persisting is worth a SQLite commit. */
63739
- volatileStateFields: ["lastFetchedAt"],
63740
- customize: {
63741
- version: 1,
63742
- advanced: "derive",
63743
- targets: []
63744
- }
63724
+ volatileStateFields: ["lastFetchedAt"]
63745
63725
  };
63746
63726
  /**
63747
63727
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63761,11 +63741,11 @@ var powerMeterCapability = {
63761
63741
  */
63762
63742
  var GpsLocationSchema = object({
63763
63743
  /** Latitude in decimal degrees, -90..90. */
63764
- latitude: number().min(-90).max(90),
63744
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63765
63745
  /** Longitude in decimal degrees, -180..180. */
63766
- longitude: number().min(-180).max(180),
63746
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63767
63747
  /** Reported accuracy in meters (lower = better). */
63768
- accuracyMeters: number().nonnegative()
63748
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63769
63749
  });
63770
63750
  var PresenceStatusSchema = object({
63771
63751
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63777,7 +63757,7 @@ var PresenceStatusSchema = object({
63777
63757
  gps: GpsLocationSchema.nullable(),
63778
63758
  /** Optional battery level of the tracking device (0..100). Null
63779
63759
  * when not reported. */
63780
- batteryPercent: number().min(0).max(100).nullable(),
63760
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63781
63761
  /** Ms epoch when the slice was last updated. */
63782
63762
  lastChangedAt: number()
63783
63763
  });
@@ -63827,19 +63807,10 @@ var presenceCapability = {
63827
63807
  * `sourceInfo`.
63828
63808
  */
63829
63809
  var PressureSensorStatusSchema = object({
63830
- /** Current pressure in hPa. */
63831
- hpa: number(),
63810
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
63811
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63832
63812
  /** Ms epoch when the slice was last updated. */
63833
- lastFetchedAt: number(),
63834
- /** Live display unit from the upstream source (e.g. HA
63835
- * `attributes.unit_of_measurement`). The UI prefers this over the
63836
- * role's canonical unit. Absent → fall back to the canonical unit. */
63837
- unit: string().optional(),
63838
- /** Suggested decimal places for numeric display.
63839
- * Populated live from the upstream source when provided (e.g. HA
63840
- * `attributes.suggested_display_precision`). Falls back to
63841
- * auto-formatting when absent. */
63842
- precision: number().int().min(0).max(10).optional()
63813
+ lastFetchedAt: number()
63843
63814
  });
63844
63815
  var pressureSensorCapability = {
63845
63816
  name: "pressure-sensor",
@@ -64253,8 +64224,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64253
64224
  }), _void(), { kind: "mutation" });
64254
64225
  var PtzAutotrackSettingsSchema = object({
64255
64226
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64256
- stopDelaySeconds: number().int().min(0).max(300),
64257
- disappearDelaySeconds: number().int().min(0).max(300)
64227
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64228
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64258
64229
  });
64259
64230
  /**
64260
64231
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64416,7 +64387,7 @@ var RunningStateStatusSchema = object({
64416
64387
  /** null = unknown — never coerced to false (D393). */
64417
64388
  running: boolean().nullable(),
64418
64389
  /** Epoch ms the current boolean began; null while `running` is null. */
64419
- since: number().nullable(),
64390
+ since: number().nullable().meta(readingMeta("none")),
64420
64391
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64421
64392
  phase: string().nullable(),
64422
64393
  /** null until a reading has been published (D393). */
@@ -64431,7 +64402,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64431
64402
  /** Generic Customize target for the running boolean (D936). */
64432
64403
  var runningCustomize = {
64433
64404
  version: 1,
64434
- advanced: "derive",
64435
64405
  targets: [{
64436
64406
  id: "running",
64437
64407
  cardinality: "scalar",
@@ -64451,7 +64421,7 @@ var runningCustomize = {
64451
64421
  DeviceType.Thermostat,
64452
64422
  DeviceType.PetFeeder
64453
64423
  ],
64454
- apply: "add-or-replace",
64424
+ apply: "add",
64455
64425
  path: "running",
64456
64426
  sources: [
64457
64427
  {
@@ -65715,9 +65685,9 @@ var RecordTriggerSchema = _enum([
65715
65685
  */
65716
65686
  var RecordWindowSchema = object({
65717
65687
  trigger: RecordTriggerSchema,
65718
- day: number().int().min(0).max(6),
65719
- startMinute: number().int().min(0).max(1439),
65720
- endMinute: number().int().min(1).max(1440)
65688
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65689
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65690
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65721
65691
  });
65722
65692
  /** Status of one physical volume, as the camera itself describes it. */
65723
65693
  var OnboardStorageVolumeSchema = object({
@@ -65793,10 +65763,10 @@ var RecordingOnboardStatusSchema = object({
65793
65763
  /** Master "record to the card at all" switch. */
65794
65764
  enabled: boolean().nullable(),
65795
65765
  overwriteWhenFull: boolean().nullable(),
65796
- preRecordSec: number().nullable(),
65797
- postRecordSec: number().nullable(),
65766
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
65767
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65798
65768
  /** Length of one recorded file, in minutes. */
65799
- segmentMinutes: number().nullable(),
65769
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65800
65770
  /** The primary track's weekly windows, flattened. */
65801
65771
  windows: array(RecordWindowSchema),
65802
65772
  /**
@@ -65812,7 +65782,7 @@ var RecordingOnboardStatusSchema = object({
65812
65782
  * provider reporting a non-zero count also reports the schedule as not
65813
65783
  * writable.
65814
65784
  */
65815
- unreadableWindows: number(),
65785
+ unreadableWindows: number().meta(readingMeta("count")),
65816
65786
  /**
65817
65787
  * The camera is scheduled to record and has NO usable storage.
65818
65788
  *
@@ -66657,7 +66627,7 @@ var CamStreamDescriptorSchema = object({
66657
66627
  url: string().optional(),
66658
66628
  codec: string().optional(),
66659
66629
  resolution: CamStreamResolutionSchema.optional(),
66660
- fps: number().positive().optional(),
66630
+ fps: number().positive().optional().meta(readingMeta("rate")),
66661
66631
  label: string().optional(),
66662
66632
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66663
66633
  deviceFeatures: array(string()).optional(),
@@ -66744,18 +66714,18 @@ var StreamProfileSchema = _enum([
66744
66714
  ]);
66745
66715
  /** The current encoding config of one stream profile. */
66746
66716
  var StreamProfileConfigSchema = object({
66747
- width: number(),
66748
- height: number(),
66717
+ width: number().meta(readingMeta("none")),
66718
+ height: number().meta(readingMeta("none")),
66749
66719
  codec: _enum(["h264", "h265"]),
66750
- framerate: number(),
66751
- bitrate: number(),
66720
+ framerate: number().meta(readingMeta("rate")),
66721
+ bitrate: number().meta(readingMeta("bitrate")),
66752
66722
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66753
66723
  encoderProfile: _enum([
66754
66724
  "high",
66755
66725
  "main",
66756
66726
  "baseline"
66757
66727
  ]).optional(),
66758
- gop: number().optional(),
66728
+ gop: number().optional().meta(readingMeta("none")),
66759
66729
  /**
66760
66730
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66761
66731
  *
@@ -66976,12 +66946,7 @@ var switchCapability = {
66976
66946
  label: "On",
66977
66947
  kind: "boolean"
66978
66948
  }
66979
- }] },
66980
- customize: {
66981
- version: 1,
66982
- advanced: "derive",
66983
- targets: []
66984
- }
66949
+ }] }
66985
66950
  };
66986
66951
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66987
66952
  function hasControlCharacter(value) {
@@ -67564,35 +67529,21 @@ var tamperCapability = {
67564
67529
  * entries with `device_class: temperature` and any future native
67565
67530
  * thermometer.
67566
67531
  *
67567
- * Unit normalisation: providers convert to Celsius before storing.
67568
- * The slice value is always Celsius so cross-cap aggregators
67569
- * (climate-control's `currentTemp`, energy analytics) can compose
67570
- * without per-source unit fixups. The canonical display unit (`°C`) is
67571
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67572
- * `sourceInfo`.
67532
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67533
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67573
67534
  *
67574
67535
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67575
67536
  * frozen feed (provider hung) distinct from a "temperature hasn't
67576
67537
  * changed" steady state.
67577
67538
  */
67578
67539
  var TemperatureSensorStatusSchema = object({
67579
- /** Current temperature in Celsius. */
67580
- celsius: number().meta({
67581
- unit: "°C",
67540
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67541
+ celsius: number().nullable().meta(readingMeta("temperature", {
67542
+ precision: 1,
67582
67543
  label: "Temperature"
67583
- }),
67544
+ })),
67584
67545
  /** Ms epoch when the slice was last updated (push or poll). */
67585
- lastFetchedAt: number(),
67586
- /** Live display unit from the upstream source (e.g. HA
67587
- * `attributes.unit_of_measurement`). The UI prefers this over the
67588
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67589
- * Absent → the UI falls back to the role's canonical unit. */
67590
- unit: string().optional(),
67591
- /** Suggested decimal places for numeric display.
67592
- * Populated live from the upstream source when provided (e.g. HA
67593
- * `attributes.suggested_display_precision`). Falls back to
67594
- * auto-formatting when absent. */
67595
- precision: number().int().min(0).max(10).optional()
67546
+ lastFetchedAt: number()
67596
67547
  });
67597
67548
  var temperatureSensorCapability = {
67598
67549
  name: "temperature-sensor",
@@ -67623,12 +67574,7 @@ var temperatureSensorCapability = {
67623
67574
  } } },
67624
67575
  /** Clock fields: written, but excluded from the compare that decides
67625
67576
  * whether persisting is worth a SQLite commit. */
67626
- volatileStateFields: ["lastFetchedAt"],
67627
- customize: {
67628
- version: 1,
67629
- advanced: "derive",
67630
- targets: []
67631
- }
67577
+ volatileStateFields: ["lastFetchedAt"]
67632
67578
  };
67633
67579
  /**
67634
67580
  * toast — system-scoped singleton capability that streams toast
@@ -67911,16 +67857,16 @@ var VacuumControlStatusSchema = object({
67911
67857
  ]),
67912
67858
  /** 0..100 battery percentage. Null when the device has no battery
67913
67859
  * reading. */
67914
- batteryLevel: number().min(0).max(100).nullable(),
67860
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67915
67861
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67916
67862
  * the vacuum has no speed control. */
67917
67863
  fanSpeed: string().nullable(),
67918
67864
  /** Speed tokens the hardware accepts — drives the UI selector. */
67919
67865
  availableFanSpeeds: array(string()),
67920
67866
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67921
- progressPercent: number().min(0).max(100).nullable(),
67867
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67922
67868
  /** Current error code (0 / null = no error). */
67923
- errorCode: number().nullable(),
67869
+ errorCode: number().nullable().meta(readingMeta("none")),
67924
67870
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67925
67871
  errorLabel: string().nullable(),
67926
67872
  /** Ms epoch when the slice was last updated. */
@@ -67998,7 +67944,7 @@ var ValveStatusSchema = object({
67998
67944
  ]),
67999
67945
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68000
67946
  * intermediate position surface. */
68001
- position: number().min(0).max(100).nullable(),
67947
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68002
67948
  /** Ms epoch when the slice was last updated. */
68003
67949
  lastChangedAt: number()
68004
67950
  });
@@ -68106,9 +68052,9 @@ var vibrationCapability = {
68106
68052
  */
68107
68053
  var WaterHeaterStatusSchema = object({
68108
68054
  /** Current measured temperature. Null when not reported. */
68109
- currentTemp: number().nullable(),
68055
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68110
68056
  /** Target temperature setpoint. Null when no setpoint surface. */
68111
- targetTemp: number().nullable(),
68057
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68112
68058
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68113
68059
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68114
68060
  * device reports an unknown state. */
@@ -68119,9 +68065,9 @@ var WaterHeaterStatusSchema = object({
68119
68065
  * has no away surface. */
68120
68066
  away: boolean().nullable(),
68121
68067
  /** HA `min_temp` attribute. Null when not reported. */
68122
- minTemp: number().nullable(),
68068
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68123
68069
  /** HA `max_temp` attribute. Null when not reported. */
68124
- maxTemp: number().nullable(),
68070
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68125
68071
  /** Ms epoch when the slice was last updated. */
68126
68072
  lastChangedAt: number()
68127
68073
  });
@@ -68206,25 +68152,18 @@ var waterHeaterCapability = {
68206
68152
  * Forecast deferred — current conditions only for v1.
68207
68153
  */
68208
68154
  var WeatherStatusSchema = object({
68209
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68210
- * when no condition has been reported yet. */
68155
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68211
68156
  condition: string().nullable(),
68212
- /** Current temperature in the reported unit. Null when not provided. */
68213
- temperature: number().nullable(),
68214
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68215
- temperatureUnit: string().nullable(),
68157
+ /** Current temperature in Celsius. Null when not provided or refused. */
68158
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68216
68159
  /** Relative humidity (0..100). Null when not provided. */
68217
- humidity: number().min(0).max(100).nullable(),
68218
- /** Barometric pressure in the reported unit. Null when not provided. */
68219
- pressure: number().nullable(),
68220
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68221
- pressureUnit: string().nullable(),
68222
- /** Wind speed in the reported unit. Null when not provided. */
68223
- windSpeed: number().nullable(),
68224
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68225
- windSpeedUnit: string().nullable(),
68160
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68161
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68162
+ pressure: number().nullable().meta(readingMeta("pressure")),
68163
+ /** Wind speed in metres per second. Null when not provided or refused. */
68164
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68226
68165
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68227
- windBearing: number().nullable(),
68166
+ windBearing: number().nullable().meta(readingMeta("angle")),
68228
68167
  /** Ms epoch when the slice was last updated. */
68229
68168
  lastFetchedAt: number()
68230
68169
  });
@@ -68252,12 +68191,12 @@ var weatherCapability = {
68252
68191
  */
68253
68192
  durability: "session",
68254
68193
  /**
68255
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68194
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68256
68195
  */
68257
68196
  history: { fields: {
68258
68197
  temperature: {
68259
68198
  kind: "gauge",
68260
- unitFrom: "temperatureUnit",
68199
+ unit: "°C",
68261
68200
  deadband: .2
68262
68201
  },
68263
68202
  humidity: {
@@ -68267,12 +68206,12 @@ var weatherCapability = {
68267
68206
  },
68268
68207
  pressure: {
68269
68208
  kind: "gauge",
68270
- unitFrom: "pressureUnit",
68209
+ unit: "hPa",
68271
68210
  deadband: .5
68272
68211
  },
68273
68212
  windSpeed: {
68274
68213
  kind: "gauge",
68275
- unitFrom: "windSpeedUnit",
68214
+ unit: "m/s",
68276
68215
  deadband: .5
68277
68216
  }
68278
68217
  } }
@@ -69203,6 +69142,404 @@ function runningFromLawnMower(activity, previous, fetchedAt, transitionAt) {
69203
69142
  if (activity === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
69204
69143
  return nativeSlice(false, activity, previous, transitionAtMs, fetchedAt, null);
69205
69144
  }
69145
+ /**
69146
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
69147
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
69148
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
69149
+ * decimals). K derives from `°C = K − 273.15`.
69150
+ */
69151
+ var UNIT_TABLE = {
69152
+ "°C": {
69153
+ dimension: "temperature",
69154
+ factor: 1,
69155
+ offset: 0
69156
+ },
69157
+ "°F": {
69158
+ dimension: "temperature",
69159
+ factor: 5 / 9,
69160
+ offset: -160 / 9
69161
+ },
69162
+ K: {
69163
+ dimension: "temperature",
69164
+ factor: 1,
69165
+ offset: -273.15
69166
+ },
69167
+ hPa: {
69168
+ dimension: "pressure",
69169
+ factor: 1,
69170
+ offset: 0
69171
+ },
69172
+ kPa: {
69173
+ dimension: "pressure",
69174
+ factor: 10,
69175
+ offset: 0
69176
+ },
69177
+ Pa: {
69178
+ dimension: "pressure",
69179
+ factor: .01,
69180
+ offset: 0
69181
+ },
69182
+ mbar: {
69183
+ dimension: "pressure",
69184
+ factor: 1,
69185
+ offset: 0
69186
+ },
69187
+ bar: {
69188
+ dimension: "pressure",
69189
+ factor: 1e3,
69190
+ offset: 0
69191
+ },
69192
+ inHg: {
69193
+ dimension: "pressure",
69194
+ factor: 33.8639,
69195
+ offset: 0
69196
+ },
69197
+ mmHg: {
69198
+ dimension: "pressure",
69199
+ factor: 1.33322,
69200
+ offset: 0
69201
+ },
69202
+ psi: {
69203
+ dimension: "pressure",
69204
+ factor: 68.9476,
69205
+ offset: 0
69206
+ },
69207
+ "m/s": {
69208
+ dimension: "speed",
69209
+ factor: 1,
69210
+ offset: 0
69211
+ },
69212
+ "km/h": {
69213
+ dimension: "speed",
69214
+ factor: 1 / 3.6,
69215
+ offset: 0
69216
+ },
69217
+ mph: {
69218
+ dimension: "speed",
69219
+ factor: .44704,
69220
+ offset: 0
69221
+ },
69222
+ kn: {
69223
+ dimension: "speed",
69224
+ factor: .514444,
69225
+ offset: 0
69226
+ },
69227
+ "mm/h": {
69228
+ dimension: "precipitation-rate",
69229
+ factor: 1,
69230
+ offset: 0
69231
+ },
69232
+ "in/h": {
69233
+ dimension: "precipitation-rate",
69234
+ factor: 25.4,
69235
+ offset: 0
69236
+ },
69237
+ m: {
69238
+ dimension: "length",
69239
+ factor: 1,
69240
+ offset: 0
69241
+ },
69242
+ mm: {
69243
+ dimension: "length",
69244
+ factor: .001,
69245
+ offset: 0
69246
+ },
69247
+ cm: {
69248
+ dimension: "length",
69249
+ factor: .01,
69250
+ offset: 0
69251
+ },
69252
+ km: {
69253
+ dimension: "length",
69254
+ factor: 1e3,
69255
+ offset: 0
69256
+ },
69257
+ in: {
69258
+ dimension: "length",
69259
+ factor: .0254,
69260
+ offset: 0
69261
+ },
69262
+ ft: {
69263
+ dimension: "length",
69264
+ factor: .3048,
69265
+ offset: 0
69266
+ },
69267
+ mi: {
69268
+ dimension: "length",
69269
+ factor: 1609.344,
69270
+ offset: 0
69271
+ },
69272
+ "m²": {
69273
+ dimension: "area",
69274
+ factor: 1,
69275
+ offset: 0
69276
+ },
69277
+ lx: {
69278
+ dimension: "illuminance",
69279
+ factor: 1,
69280
+ offset: 0
69281
+ },
69282
+ "W/m²": {
69283
+ dimension: "irradiance",
69284
+ factor: 1,
69285
+ offset: 0
69286
+ },
69287
+ W: {
69288
+ dimension: "power",
69289
+ factor: 1,
69290
+ offset: 0
69291
+ },
69292
+ kW: {
69293
+ dimension: "power",
69294
+ factor: 1e3,
69295
+ offset: 0
69296
+ },
69297
+ VA: {
69298
+ dimension: "apparent-power",
69299
+ factor: 1,
69300
+ offset: 0
69301
+ },
69302
+ Wh: {
69303
+ dimension: "energy",
69304
+ factor: 1,
69305
+ offset: 0
69306
+ },
69307
+ kWh: {
69308
+ dimension: "energy",
69309
+ factor: 1e3,
69310
+ offset: 0
69311
+ },
69312
+ MWh: {
69313
+ dimension: "energy",
69314
+ factor: 1e6,
69315
+ offset: 0
69316
+ },
69317
+ V: {
69318
+ dimension: "voltage",
69319
+ factor: 1,
69320
+ offset: 0
69321
+ },
69322
+ mV: {
69323
+ dimension: "voltage",
69324
+ factor: .001,
69325
+ offset: 0
69326
+ },
69327
+ A: {
69328
+ dimension: "current",
69329
+ factor: 1,
69330
+ offset: 0
69331
+ },
69332
+ mA: {
69333
+ dimension: "current",
69334
+ factor: .001,
69335
+ offset: 0
69336
+ },
69337
+ ppm: {
69338
+ dimension: "concentration-volume",
69339
+ factor: 1,
69340
+ offset: 0
69341
+ },
69342
+ ppb: {
69343
+ dimension: "concentration-volume",
69344
+ factor: .001,
69345
+ offset: 0
69346
+ },
69347
+ "µg/m³": {
69348
+ dimension: "concentration-mass",
69349
+ factor: 1,
69350
+ offset: 0
69351
+ },
69352
+ "mg/m³": {
69353
+ dimension: "concentration-mass",
69354
+ factor: 1e3,
69355
+ offset: 0
69356
+ },
69357
+ kg: {
69358
+ dimension: "mass",
69359
+ factor: 1,
69360
+ offset: 0
69361
+ },
69362
+ g: {
69363
+ dimension: "mass",
69364
+ factor: .001,
69365
+ offset: 0
69366
+ },
69367
+ lb: {
69368
+ dimension: "mass",
69369
+ factor: .453592,
69370
+ offset: 0
69371
+ },
69372
+ oz: {
69373
+ dimension: "mass",
69374
+ factor: .0283495,
69375
+ offset: 0
69376
+ },
69377
+ "%": {
69378
+ dimension: "percentage",
69379
+ factor: 1,
69380
+ offset: 0
69381
+ },
69382
+ L: {
69383
+ dimension: "volume",
69384
+ factor: 1,
69385
+ offset: 0
69386
+ },
69387
+ mL: {
69388
+ dimension: "volume",
69389
+ factor: .001,
69390
+ offset: 0
69391
+ },
69392
+ gal: {
69393
+ dimension: "volume",
69394
+ factor: 3.785411784,
69395
+ offset: 0
69396
+ },
69397
+ s: {
69398
+ dimension: "duration",
69399
+ factor: 1,
69400
+ offset: 0
69401
+ },
69402
+ ms: {
69403
+ dimension: "duration",
69404
+ factor: .001,
69405
+ offset: 0
69406
+ },
69407
+ min: {
69408
+ dimension: "duration",
69409
+ factor: 60,
69410
+ offset: 0
69411
+ },
69412
+ h: {
69413
+ dimension: "duration",
69414
+ factor: 3600,
69415
+ offset: 0
69416
+ },
69417
+ d: {
69418
+ dimension: "duration",
69419
+ factor: 86400,
69420
+ offset: 0
69421
+ },
69422
+ B: {
69423
+ dimension: "information",
69424
+ factor: 1,
69425
+ offset: 0
69426
+ },
69427
+ KiB: {
69428
+ dimension: "information",
69429
+ factor: 1024,
69430
+ offset: 0
69431
+ },
69432
+ MiB: {
69433
+ dimension: "information",
69434
+ factor: 1024 ** 2,
69435
+ offset: 0
69436
+ },
69437
+ GiB: {
69438
+ dimension: "information",
69439
+ factor: 1024 ** 3,
69440
+ offset: 0
69441
+ },
69442
+ TiB: {
69443
+ dimension: "information",
69444
+ factor: 1024 ** 4,
69445
+ offset: 0
69446
+ },
69447
+ kB: {
69448
+ dimension: "information",
69449
+ factor: 1e3,
69450
+ offset: 0
69451
+ },
69452
+ MB: {
69453
+ dimension: "information",
69454
+ factor: 1e6,
69455
+ offset: 0
69456
+ },
69457
+ GB: {
69458
+ dimension: "information",
69459
+ factor: 1e9,
69460
+ offset: 0
69461
+ },
69462
+ "°": {
69463
+ dimension: "angle",
69464
+ factor: 1,
69465
+ offset: 0
69466
+ },
69467
+ dBFS: {
69468
+ dimension: "sound-level",
69469
+ factor: 1,
69470
+ offset: 0
69471
+ },
69472
+ dBm: {
69473
+ dimension: "signal-level",
69474
+ factor: 1,
69475
+ offset: 0
69476
+ },
69477
+ mired: {
69478
+ dimension: "colour-temperature",
69479
+ factor: 1,
69480
+ offset: 0
69481
+ },
69482
+ kbps: {
69483
+ dimension: "bitrate",
69484
+ factor: 1,
69485
+ offset: 0
69486
+ },
69487
+ Mbps: {
69488
+ dimension: "bitrate",
69489
+ factor: 1e3,
69490
+ offset: 0
69491
+ },
69492
+ fps: {
69493
+ dimension: "rate",
69494
+ factor: 1,
69495
+ offset: 0
69496
+ }
69497
+ };
69498
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69499
+ function specFor(unit) {
69500
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
69501
+ }
69502
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
69503
+ function unitDimension(unit) {
69504
+ return specFor(unit)?.dimension;
69505
+ }
69506
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
69507
+ function toCanonical(value, spec) {
69508
+ return value * spec.factor + spec.offset;
69509
+ }
69510
+ /** Canonical-unit value → the unit described by `spec`. */
69511
+ function fromCanonical(canonical, spec) {
69512
+ return (canonical - spec.offset) / spec.factor;
69513
+ }
69514
+ /**
69515
+ * Graceful variant for the render path: returns the converted value, or
69516
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
69517
+ * Never throws — a render caller falls back to the raw value + source unit.
69518
+ */
69519
+ function tryConvertUnit(value, from, to) {
69520
+ if (from === to) return value;
69521
+ const fromSpec = specFor(from);
69522
+ if (!fromSpec) return void 0;
69523
+ const toSpec = specFor(to);
69524
+ if (!toSpec) return void 0;
69525
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
69526
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
69527
+ }
69528
+ /**
69529
+ * Every known unit spelling in a dimension (table insertion order — canonical
69530
+ * unit first). Powers the UI's same-dimension unit picker.
69531
+ */
69532
+ /**
69533
+ * Convert a difference, not a reading. Affine units (°F) shift an absolute
69534
+ * value by their offset; a step of 1°F is 5/9°C, so the offset is cancelled.
69535
+ */
69536
+ function tryConvertDelta(value, from, to) {
69537
+ if (from === to) return value;
69538
+ const origin = tryConvertUnit(0, from, to);
69539
+ const shifted = tryConvertUnit(value, from, to);
69540
+ if (origin === void 0 || shifted === void 0) return void 0;
69541
+ return shifted - origin;
69542
+ }
69206
69543
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69207
69544
  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;
69208
69545
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83888,7 +84225,6 @@ var SENSOR_CAP_VOCABULARY = {
83888
84225
  field: "value",
83889
84226
  quantity: t("the reading", "il valore"),
83890
84227
  label: t("Reading", "Valore"),
83891
- unitField: "unit",
83892
84228
  icon: "gauge"
83893
84229
  }]
83894
84230
  },
@@ -84024,7 +84360,7 @@ var SENSOR_CAP_VOCABULARY = {
84024
84360
  field: "temperature",
84025
84361
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84026
84362
  label: t("Temperature", "Temperatura"),
84027
- unitField: "temperatureUnit",
84363
+ unit: "°C",
84028
84364
  icon: "thermometer"
84029
84365
  },
84030
84366
  {
@@ -84040,7 +84376,7 @@ var SENSOR_CAP_VOCABULARY = {
84040
84376
  field: "windSpeed",
84041
84377
  quantity: t("the wind speed", "la velocità del vento"),
84042
84378
  label: t("Wind", "Vento"),
84043
- unitField: "windSpeedUnit",
84379
+ unit: "m/s",
84044
84380
  icon: "wind"
84045
84381
  },
84046
84382
  {
@@ -84048,7 +84384,7 @@ var SENSOR_CAP_VOCABULARY = {
84048
84384
  field: "pressure",
84049
84385
  quantity: t("the air pressure", "la pressione atmosferica"),
84050
84386
  label: t("Pressure", "Pressione"),
84051
- unitField: "pressureUnit",
84387
+ unit: "hPa",
84052
84388
  icon: "gauge"
84053
84389
  }
84054
84390
  ]
@@ -88413,6 +88749,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
88413
88749
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88414
88750
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88415
88751
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
88752
+ /**
88753
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
88754
+ * An unknown unit or a unit from another dimension is a refusal: the caller
88755
+ * logs it and publishes null (D393). This function never guesses.
88756
+ */
88757
+ var DIMENSION_QUANTITY = {
88758
+ temperature: "temperature",
88759
+ pressure: "pressure",
88760
+ speed: "speed",
88761
+ "precipitation-rate": "precipitation-rate",
88762
+ length: "length",
88763
+ area: "area",
88764
+ illuminance: "illuminance",
88765
+ irradiance: "irradiance",
88766
+ power: "power",
88767
+ "apparent-power": "apparent-power",
88768
+ energy: "energy",
88769
+ voltage: "voltage",
88770
+ current: "current",
88771
+ "concentration-volume": "concentration-volume",
88772
+ "concentration-mass": "concentration-mass",
88773
+ mass: "mass",
88774
+ percentage: "percentage",
88775
+ volume: "volume",
88776
+ duration: "duration",
88777
+ information: "information",
88778
+ angle: "angle",
88779
+ "sound-level": "sound-level",
88780
+ "signal-level": "signal-level",
88781
+ "colour-temperature": "colour-temperature",
88782
+ bitrate: "bitrate",
88783
+ rate: "rate"
88784
+ };
88785
+ /**
88786
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
88787
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
88788
+ * precipitation is chosen by the caller (a device class), not by the symbol.
88789
+ */
88790
+ function quantityFromUnit(rawUnit) {
88791
+ const unit = normalizeUnit(rawUnit);
88792
+ if (unit === void 0) return void 0;
88793
+ if (unit === "ppb") return "concentration-ppb";
88794
+ if (unit === "ms") return "duration-ms";
88795
+ const dimension = unitDimension(unit);
88796
+ if (dimension === void 0) return void 0;
88797
+ return DIMENSION_QUANTITY[dimension];
88798
+ }
88799
+ function ingestReading(input) {
88800
+ if (!Number.isFinite(input.value)) return {
88801
+ ok: false,
88802
+ reason: "non-finite"
88803
+ };
88804
+ if (input.quantity === "device") return {
88805
+ ok: false,
88806
+ reason: "unknown-unit"
88807
+ };
88808
+ const raw = input.rawUnit?.trim();
88809
+ if (raw === void 0 || raw.length === 0) return {
88810
+ ok: true,
88811
+ value: input.value
88812
+ };
88813
+ const unit = normalizeUnit(raw);
88814
+ if (unit === void 0) return {
88815
+ ok: false,
88816
+ reason: "unknown-unit"
88817
+ };
88818
+ const base = baseUnitOf(input.quantity);
88819
+ if (base === void 0) {
88820
+ if (unitDimension(unit) !== void 0) return {
88821
+ ok: false,
88822
+ reason: "cross-dimension"
88823
+ };
88824
+ return {
88825
+ ok: false,
88826
+ reason: "unknown-unit"
88827
+ };
88828
+ }
88829
+ const converted = tryConvertUnit(input.value, unit, base);
88830
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
88831
+ ok: false,
88832
+ reason: "unknown-unit"
88833
+ } : {
88834
+ ok: false,
88835
+ reason: "cross-dimension"
88836
+ };
88837
+ return {
88838
+ ok: true,
88839
+ value: converted
88840
+ };
88841
+ }
88416
88842
  var MB = 1024 * 1024;
88417
88843
  1024 * MB, 3072 * MB;
88418
88844
  //#endregion
@@ -191109,7 +191535,7 @@ var CONSUMABLES_COLD_START = {
191109
191535
  lastFetchedAt: 0
191110
191536
  };
191111
191537
  function itemEqual(a, b) {
191112
- return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.unit === b.remaining?.unit;
191538
+ return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable && a.kind === b.kind && a.remaining?.value === b.remaining?.value && a.remaining?.kind === b.remaining?.kind;
191113
191539
  }
191114
191540
  function itemsEqual(a, b) {
191115
191541
  return a.length === b.length && a.every((item, i) => {
@@ -191807,6 +192233,40 @@ var DreameMowerSubstateDevice = class extends DreameChildDevice {
191807
192233
  }
191808
192234
  };
191809
192235
  //#endregion
192236
+ //#region src/dreame-reading.ts
192237
+ /** Catalog unit → the quantity stored on the device. `times` is a count. */
192238
+ function dreameQuantity(unit) {
192239
+ if (unit === void 0 || unit.length === 0) return "none";
192240
+ if (unit === "times") return "count";
192241
+ const learned = quantityFromUnit(unit);
192242
+ if (learned === void 0 || learned === "device") return null;
192243
+ return learned;
192244
+ }
192245
+ /** Upstream number → base. A count ignores the word `times`. */
192246
+ function dreameToBase(value, unit, quantity) {
192247
+ const result = ingestReading({
192248
+ value,
192249
+ rawUnit: quantity === "count" ? void 0 : unit,
192250
+ quantity
192251
+ });
192252
+ return result.ok ? result.value : null;
192253
+ }
192254
+ /** Base number → the unit the robot still speaks. */
192255
+ function dreameToUpstream(value, unit, quantity) {
192256
+ const base = baseUnitOf(quantity);
192257
+ const upstream = unit === void 0 ? void 0 : normalizeUnit(unit);
192258
+ if (base === void 0 || upstream === void 0 || upstream === base) return value;
192259
+ return tryConvertUnit(value, base, upstream) ?? null;
192260
+ }
192261
+ /** A step is a difference, so an affine offset must not move it. */
192262
+ function dreameStep(step, unit, quantity) {
192263
+ if (step === void 0) return void 0;
192264
+ const base = baseUnitOf(quantity);
192265
+ const from = unit === void 0 ? void 0 : normalizeUnit(unit);
192266
+ if (base === void 0 || from === void 0 || from === base) return step;
192267
+ return tryConvertDelta(step, from, base);
192268
+ }
192269
+ //#endregion
191810
192270
  //#region src/devices/dreame-number-device.ts
191811
192271
  var CAP_NAME$5 = "control";
191812
192272
  /** Persisted config for a generic number child: the shared Dreame child config
@@ -191831,22 +192291,25 @@ var dreameNumberSchema = dreameChildSchema.extend({
191831
192291
  var DreameNumberDevice = class extends DreameChildDevice {
191832
192292
  #siid;
191833
192293
  #piid;
191834
- #min;
191835
- #max;
191836
- #step;
191837
192294
  #unit;
192295
+ #quantity;
192296
+ #baseMin;
192297
+ #baseMax;
192298
+ #baseStep;
191838
192299
  constructor(ctx) {
191839
192300
  super(ctx, DeviceRole.NumericControl);
191840
192301
  const cfg = dreameNumberSchema.parse(ctx.persistedConfig ?? {});
191841
192302
  this.#siid = cfg.siid;
191842
192303
  this.#piid = cfg.piid;
191843
- this.#min = cfg.min;
191844
- this.#max = cfg.max;
191845
- this.#step = cfg.step;
191846
192304
  this.#unit = cfg.unit;
192305
+ this.#quantity = dreameQuantity(cfg.unit);
192306
+ this.#baseMin = this.#toBase(cfg.min) ?? cfg.min;
192307
+ this.#baseMax = this.#toBase(cfg.max) ?? cfg.max;
192308
+ this.#baseStep = this.#quantity === null ? cfg.step : dreameStep(cfg.step, cfg.unit, this.#quantity);
191847
192309
  this.updateSourceInfo({
191848
192310
  id: `${this.dreameDeviceId}:number:${cfg.siid}-${cfg.piid}`,
191849
- system: "dreame"
192311
+ system: "dreame",
192312
+ ...this.#quantity !== null ? { quantity: this.#quantity } : {}
191850
192313
  });
191851
192314
  }
191852
192315
  /** Read the configured property's numeric value, or null when absent. */
@@ -191854,41 +192317,68 @@ var DreameNumberDevice = class extends DreameChildDevice {
191854
192317
  const v = this.resolveHandle()?.getProperty(this.#siid, this.#piid)?.value;
191855
192318
  return typeof v === "number" && Number.isFinite(v) ? v : null;
191856
192319
  }
191857
- #slice(value) {
192320
+ #toBase(value) {
192321
+ if (this.#quantity === null) return null;
192322
+ return dreameToBase(value, this.#unit, this.#quantity);
192323
+ }
192324
+ #slice(upstream) {
192325
+ const value = this.#toBase(upstream);
192326
+ if (value === null) {
192327
+ this.ctx.logger.warn("numeric reading refused", {
192328
+ tags: { deviceId: this.id },
192329
+ meta: {
192330
+ quantity: this.#quantity ?? "device",
192331
+ unit: this.#unit ?? "",
192332
+ reason: "unknown-unit"
192333
+ }
192334
+ });
192335
+ return null;
192336
+ }
191858
192337
  return {
191859
192338
  kind: "numeric",
191860
192339
  value,
191861
192340
  options: [],
191862
192341
  lastChangedAt: Date.now(),
191863
- min: this.#min,
191864
- max: this.#max,
191865
- step: this.#step,
191866
- ...this.#unit !== void 0 ? { unit: this.#unit } : {}
192342
+ min: this.#baseMin,
192343
+ max: this.#baseMax,
192344
+ ...this.#baseStep !== void 0 ? { step: this.#baseStep } : {}
191867
192345
  };
191868
192346
  }
191869
192347
  registerControlCap() {
191870
192348
  this.ctx.registerNativeCap(controlCapability, {
191871
- getStatus: async () => this.runtimeState.getCapState(CAP_NAME$5) ?? this.#slice(this.#read() ?? this.#min),
192349
+ getStatus: async () => this.runtimeState.getCapState(CAP_NAME$5) ?? {
192350
+ kind: "numeric",
192351
+ value: 0,
192352
+ options: [],
192353
+ lastChangedAt: 0
192354
+ },
191872
192355
  setValue: async ({ control }) => {
191873
192356
  if (control.kind !== "numeric") throw new Error(`DreameNumberDevice: expected a numeric value (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
192357
+ if (this.#quantity === null) throw new Error(`DreameNumberDevice: unknown unit (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191874
192358
  const handle = this.resolveHandle();
191875
192359
  if (handle === null) throw new Error(`DreameNumberDevice: handle unavailable (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191876
- const clamped = Math.min(this.#max, Math.max(this.#min, control.value));
192360
+ const upstream = dreameToUpstream(Math.min(this.#baseMax, Math.max(this.#baseMin, control.value)), this.#unit, this.#quantity);
192361
+ if (upstream === null) throw new Error(`DreameNumberDevice: cannot convert back (device=${this.dreameDeviceId}, ${this.#siid}/${this.#piid})`);
191877
192362
  await handle.setProperty({
191878
192363
  siid: this.#siid,
191879
192364
  piid: this.#piid,
191880
- value: clamped
192365
+ value: upstream
191881
192366
  });
191882
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(clamped));
192367
+ const slice = this.#slice(upstream);
192368
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$5, slice);
191883
192369
  }
191884
192370
  });
191885
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(this.#read() ?? this.#min));
192371
+ const initial = this.#read();
192372
+ const slice = initial === null ? null : this.#slice(initial);
192373
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$5, slice);
191886
192374
  }
191887
192375
  recomputeSlice() {
191888
192376
  const value = this.#read();
191889
192377
  if (value === null) return;
191890
- if (this.runtimeState.getCapState(CAP_NAME$5)?.value === value) return;
191891
- this.runtimeState.setCapState(CAP_NAME$5, this.#slice(value));
192378
+ const slice = this.#slice(value);
192379
+ if (slice === null) return;
192380
+ if (this.runtimeState.getCapState(CAP_NAME$5)?.value === slice.value) return;
192381
+ this.runtimeState.setCapState(CAP_NAME$5, slice);
191892
192382
  }
191893
192383
  };
191894
192384
  //#endregion
@@ -191919,9 +192409,11 @@ var DreameNumericSensorDevice = class extends DreameChildDevice {
191919
192409
  this.#siid = cfg.siid;
191920
192410
  this.#piid = cfg.piid;
191921
192411
  this.#unit = cfg.unit;
192412
+ const quantity = dreameQuantity(cfg.unit);
191922
192413
  this.updateSourceInfo({
191923
192414
  id: `${this.dreameDeviceId}:sensor:${cfg.siid}-${cfg.piid}`,
191924
- system: "dreame"
192415
+ system: "dreame",
192416
+ ...quantity !== null ? { quantity } : {}
191925
192417
  });
191926
192418
  }
191927
192419
  /** Read the configured property's numeric value from the shared handle, or null. */
@@ -191930,22 +192422,51 @@ var DreameNumericSensorDevice = class extends DreameChildDevice {
191930
192422
  return typeof v === "number" && Number.isFinite(v) ? v : null;
191931
192423
  }
191932
192424
  #slice(value) {
192425
+ const quantity = dreameQuantity(this.#unit);
192426
+ if (quantity === null) {
192427
+ this.ctx.logger.warn("numeric reading refused", {
192428
+ tags: { deviceId: this.id },
192429
+ meta: {
192430
+ quantity: "device",
192431
+ unit: this.#unit ?? "",
192432
+ reason: "unknown-unit"
192433
+ }
192434
+ });
192435
+ return null;
192436
+ }
192437
+ const stored = dreameToBase(value, this.#unit, quantity);
192438
+ if (stored === null) {
192439
+ this.ctx.logger.warn("numeric reading refused", {
192440
+ tags: { deviceId: this.id },
192441
+ meta: {
192442
+ quantity,
192443
+ unit: this.#unit ?? "",
192444
+ reason: "unknown-unit"
192445
+ }
192446
+ });
192447
+ return null;
192448
+ }
191933
192449
  return {
191934
- value,
191935
- ...this.#unit !== void 0 ? { unit: this.#unit } : {},
192450
+ value: stored,
191936
192451
  lastFetchedAt: Date.now()
191937
192452
  };
191938
192453
  }
191939
192454
  registerControlCap() {
191940
- this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(CAP_NAME$4) ?? this.#slice(0) });
192455
+ this.ctx.registerNativeCap(numericSensorCapability, { getStatus: async () => this.runtimeState.getCapState(CAP_NAME$4) ?? {
192456
+ value: 0,
192457
+ lastFetchedAt: 0
192458
+ } });
191941
192459
  const initial = this.#readValue();
191942
- this.runtimeState.setCapState(CAP_NAME$4, this.#slice(initial ?? 0));
192460
+ const slice = initial === null ? null : this.#slice(initial);
192461
+ if (slice !== null) this.runtimeState.setCapState(CAP_NAME$4, slice);
191943
192462
  }
191944
192463
  recomputeSlice() {
191945
192464
  const value = this.#readValue();
191946
192465
  if (value === null) return;
191947
- if (this.runtimeState.getCapState(CAP_NAME$4)?.value === value) return;
191948
- this.runtimeState.setCapState(CAP_NAME$4, this.#slice(value));
192466
+ const slice = this.#slice(value);
192467
+ if (slice === null) return;
192468
+ if (this.runtimeState.getCapState(CAP_NAME$4)?.value === slice.value) return;
192469
+ this.runtimeState.setCapState(CAP_NAME$4, slice);
191949
192470
  }
191950
192471
  };
191951
192472
  //#endregion