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