@camstack/addon-provider-bosch 0.1.23 → 0.1.25

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.
@@ -5434,7 +5434,7 @@ var ZodIssueCode = {
5434
5434
  var ZodFirstPartyTypeKind;
5435
5435
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5436
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5437
+ //#region ../types/dist/sleep-CARd6wZF.mjs
5438
5438
  /**
5439
5439
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5440
  * window to float samples (D455).
@@ -7992,6 +7992,99 @@ var RecordKindDeclarationSchema = object({
7992
7992
  }).strict();
7993
7993
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7994
7994
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7995
+ var READING_QUANTITIES = [
7996
+ "temperature",
7997
+ "pressure",
7998
+ "speed",
7999
+ "precipitation",
8000
+ "precipitation-rate",
8001
+ "length",
8002
+ "area",
8003
+ "illuminance",
8004
+ "irradiance",
8005
+ "power",
8006
+ "energy",
8007
+ "voltage",
8008
+ "current",
8009
+ "apparent-power",
8010
+ "humidity",
8011
+ "percentage",
8012
+ "concentration-volume",
8013
+ "concentration-mass",
8014
+ "concentration-ppb",
8015
+ "volume",
8016
+ "duration",
8017
+ "duration-ms",
8018
+ "information",
8019
+ "sound-level",
8020
+ "signal-level",
8021
+ "colour-temperature",
8022
+ "angle",
8023
+ "bitrate",
8024
+ "rate",
8025
+ "mass",
8026
+ "index",
8027
+ "ratio",
8028
+ "count",
8029
+ "none",
8030
+ "device"
8031
+ ];
8032
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8033
+ new Set(READING_QUANTITIES);
8034
+ /**
8035
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8036
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8037
+ */
8038
+ var QUANTITY_BASE = {
8039
+ temperature: "°C",
8040
+ pressure: "hPa",
8041
+ speed: "m/s",
8042
+ precipitation: "mm",
8043
+ "precipitation-rate": "mm/h",
8044
+ length: "m",
8045
+ area: "m²",
8046
+ illuminance: "lx",
8047
+ irradiance: "W/m²",
8048
+ power: "W",
8049
+ energy: "kWh",
8050
+ voltage: "V",
8051
+ current: "A",
8052
+ "apparent-power": "VA",
8053
+ humidity: "%",
8054
+ percentage: "%",
8055
+ "concentration-volume": "ppm",
8056
+ "concentration-mass": "µg/m³",
8057
+ "concentration-ppb": "ppb",
8058
+ volume: "L",
8059
+ duration: "s",
8060
+ "duration-ms": "ms",
8061
+ information: "B",
8062
+ "sound-level": "dBFS",
8063
+ "signal-level": "dBm",
8064
+ "colour-temperature": "mired",
8065
+ angle: "°",
8066
+ bitrate: "kbps",
8067
+ rate: "fps",
8068
+ mass: "kg",
8069
+ index: "none",
8070
+ ratio: "none",
8071
+ count: "none",
8072
+ none: "none",
8073
+ device: "device"
8074
+ };
8075
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8076
+ function baseUnitOf(quantity) {
8077
+ const symbol = QUANTITY_BASE[quantity];
8078
+ if (symbol === "none" || symbol === "device") return void 0;
8079
+ return symbol;
8080
+ }
8081
+ function readingMeta(quantity, extras) {
8082
+ return {
8083
+ quantity,
8084
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8085
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8086
+ };
8087
+ }
7995
8088
  /** Shared Zod schemas used across streaming capabilities. */
7996
8089
  var CamProfileSchema = _enum([
7997
8090
  "high",
@@ -8019,8 +8112,8 @@ var CamStreamKindSchema = _enum([
8019
8112
  "derived"
8020
8113
  ]);
8021
8114
  var CamStreamResolutionSchema = object({
8022
- width: number().int().positive(),
8023
- height: number().int().positive()
8115
+ width: number().int().positive().meta(readingMeta("none")),
8116
+ height: number().int().positive().meta(readingMeta("none"))
8024
8117
  });
8025
8118
  var CameraStreamSchema = object({
8026
8119
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8689,14 +8782,6 @@ Object.freeze({
8689
8782
  {
8690
8783
  "path": "aqi",
8691
8784
  "valueType": "number"
8692
- },
8693
- {
8694
- "path": "unit",
8695
- "valueType": "string"
8696
- },
8697
- {
8698
- "path": "precision",
8699
- "valueType": "number"
8700
8785
  }
8701
8786
  ],
8702
8787
  "alarm-panel": [
@@ -8732,20 +8817,10 @@ Object.freeze({
8732
8817
  "valueType": "boolean"
8733
8818
  }
8734
8819
  ],
8735
- "ambient-light-sensor": [
8736
- {
8737
- "path": "lux",
8738
- "valueType": "number"
8739
- },
8740
- {
8741
- "path": "unit",
8742
- "valueType": "string"
8743
- },
8744
- {
8745
- "path": "precision",
8746
- "valueType": "number"
8747
- }
8748
- ],
8820
+ "ambient-light-sensor": [{
8821
+ "path": "lux",
8822
+ "valueType": "number"
8823
+ }],
8749
8824
  "audio-metrics": [
8750
8825
  {
8751
8826
  "path": "ts",
@@ -8825,6 +8900,10 @@ Object.freeze({
8825
8900
  "path": "lastUpdated",
8826
8901
  "valueType": "number"
8827
8902
  },
8903
+ {
8904
+ "path": "volts",
8905
+ "valueType": "number"
8906
+ },
8828
8907
  {
8829
8908
  "path": "lastContactAt",
8830
8909
  "valueType": "number"
@@ -9067,10 +9146,6 @@ Object.freeze({
9067
9146
  "datetime"
9068
9147
  ]
9069
9148
  },
9070
- {
9071
- "path": "unit",
9072
- "valueType": "string"
9073
- },
9074
9149
  {
9075
9150
  "path": "min",
9076
9151
  "valueType": "number"
@@ -9082,19 +9157,6 @@ Object.freeze({
9082
9157
  {
9083
9158
  "path": "step",
9084
9159
  "valueType": "number"
9085
- },
9086
- {
9087
- "path": "precision",
9088
- "valueType": "number"
9089
- },
9090
- {
9091
- "path": "format",
9092
- "valueType": "enum",
9093
- "enumValues": [
9094
- "date",
9095
- "time",
9096
- "datetime"
9097
- ]
9098
9160
  }
9099
9161
  ],
9100
9162
  "cover": [
@@ -9335,13 +9397,8 @@ Object.freeze({
9335
9397
  "path": "value",
9336
9398
  "valueType": "string"
9337
9399
  }, {
9338
- "path": "format",
9339
- "valueType": "enum",
9340
- "enumValues": [
9341
- "date",
9342
- "time",
9343
- "datetime"
9344
- ]
9400
+ "path": "timestamp",
9401
+ "valueType": "boolean"
9345
9402
  }],
9346
9403
  "event-emitter": [
9347
9404
  {
@@ -9448,20 +9505,10 @@ Object.freeze({
9448
9505
  "valueType": "number"
9449
9506
  }
9450
9507
  ],
9451
- "humidity-sensor": [
9452
- {
9453
- "path": "percent",
9454
- "valueType": "number"
9455
- },
9456
- {
9457
- "path": "unit",
9458
- "valueType": "string"
9459
- },
9460
- {
9461
- "path": "precision",
9462
- "valueType": "number"
9463
- }
9464
- ],
9508
+ "humidity-sensor": [{
9509
+ "path": "percent",
9510
+ "valueType": "number"
9511
+ }],
9465
9512
  "image": [{
9466
9513
  "path": "url",
9467
9514
  "valueType": "string"
@@ -9800,20 +9847,10 @@ Object.freeze({
9800
9847
  "valueType": "boolean"
9801
9848
  }
9802
9849
  ],
9803
- "numeric-sensor": [
9804
- {
9805
- "path": "value",
9806
- "valueType": "number"
9807
- },
9808
- {
9809
- "path": "unit",
9810
- "valueType": "string"
9811
- },
9812
- {
9813
- "path": "precision",
9814
- "valueType": "number"
9815
- }
9816
- ],
9850
+ "numeric-sensor": [{
9851
+ "path": "value",
9852
+ "valueType": "number"
9853
+ }],
9817
9854
  "pet-feeder": [
9818
9855
  {
9819
9856
  "path": "foodLevel",
@@ -9905,14 +9942,6 @@ Object.freeze({
9905
9942
  {
9906
9943
  "path": "powerFactor",
9907
9944
  "valueType": "number"
9908
- },
9909
- {
9910
- "path": "unit",
9911
- "valueType": "string"
9912
- },
9913
- {
9914
- "path": "precision",
9915
- "valueType": "number"
9916
9945
  }
9917
9946
  ],
9918
9947
  "presence": [
@@ -9941,20 +9970,10 @@ Object.freeze({
9941
9970
  "valueType": "number"
9942
9971
  }
9943
9972
  ],
9944
- "pressure-sensor": [
9945
- {
9946
- "path": "hpa",
9947
- "valueType": "number"
9948
- },
9949
- {
9950
- "path": "unit",
9951
- "valueType": "string"
9952
- },
9953
- {
9954
- "path": "precision",
9955
- "valueType": "number"
9956
- }
9957
- ],
9973
+ "pressure-sensor": [{
9974
+ "path": "hpa",
9975
+ "valueType": "number"
9976
+ }],
9958
9977
  "privacy-mask": [{
9959
9978
  "path": "enabled",
9960
9979
  "valueType": "boolean"
@@ -10265,20 +10284,10 @@ Object.freeze({
10265
10284
  "path": "tampered",
10266
10285
  "valueType": "boolean"
10267
10286
  }],
10268
- "temperature-sensor": [
10269
- {
10270
- "path": "celsius",
10271
- "valueType": "number"
10272
- },
10273
- {
10274
- "path": "unit",
10275
- "valueType": "string"
10276
- },
10277
- {
10278
- "path": "precision",
10279
- "valueType": "number"
10280
- }
10281
- ],
10287
+ "temperature-sensor": [{
10288
+ "path": "celsius",
10289
+ "valueType": "number"
10290
+ }],
10282
10291
  "update": [
10283
10292
  {
10284
10293
  "path": "currentVersion",
@@ -10389,10 +10398,6 @@ Object.freeze({
10389
10398
  "path": "temperature",
10390
10399
  "valueType": "number"
10391
10400
  },
10392
- {
10393
- "path": "temperatureUnit",
10394
- "valueType": "string"
10395
- },
10396
10401
  {
10397
10402
  "path": "humidity",
10398
10403
  "valueType": "number"
@@ -10401,18 +10406,10 @@ Object.freeze({
10401
10406
  "path": "pressure",
10402
10407
  "valueType": "number"
10403
10408
  },
10404
- {
10405
- "path": "pressureUnit",
10406
- "valueType": "string"
10407
- },
10408
10409
  {
10409
10410
  "path": "windSpeed",
10410
10411
  "valueType": "number"
10411
10412
  },
10412
- {
10413
- "path": "windSpeedUnit",
10414
- "valueType": "string"
10415
- },
10416
10413
  {
10417
10414
  "path": "windBearing",
10418
10415
  "valueType": "number"
@@ -17876,7 +17873,8 @@ var CompositionProblemSchema = object({
17876
17873
  "item-field-reserved",
17877
17874
  "target-missing",
17878
17875
  "target-unreadable",
17879
- "not-replaceable"
17876
+ "not-replaceable",
17877
+ "native-capability-replace"
17880
17878
  ]),
17881
17879
  path: string(),
17882
17880
  message: string()
@@ -17934,9 +17932,11 @@ var CompositionFieldPlanSchema = object({
17934
17932
  reads: array(CompositionFieldReadSchema)
17935
17933
  });
17936
17934
  /**
17937
- * What a feature does to its target (D663): `new` builds a capability on a new
17938
- * device; on an existing device it `add`s a capability the device lacks, or
17939
- * `replace`s named fields of one it has natively.
17935
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17936
+ * new device; on an existing device it `add`s a capability the device lacks.
17937
+ * Replacing a capability the device already serves is refused
17938
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17939
+ * wire for a claim the hub already holds.
17940
17940
  */
17941
17941
  var CompositionFeatureModeSchema = _enum([
17942
17942
  "new",
@@ -19822,12 +19822,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19822
19822
  * providers detect renames by matching `uniqueId` and patch `id`
19823
19823
  * in-place so dispatch keeps working.
19824
19824
  *
19825
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19826
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19827
- * state push and are the single source of truth. The `device_class` tag is
19828
- * an HA-specific input that is consumed at the discovery boundary
19829
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19830
- * stored after that point.
19825
+ * **Display unit and precision are NOT stored here.** They are a read-time
19826
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
19827
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
19828
+ * neither is repeated on each sample. The `device_class` tag is an
19829
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
19830
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19831
19831
  *
19832
19832
  * The `raw` escape hatch carries provider-specific attributes that don't
19833
19833
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19841,6 +19841,14 @@ var SourceInfoSchema = object({
19841
19841
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19842
19842
  * Used to detect rename when `id` changes. */
19843
19843
  uniqueId: string().optional(),
19844
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
19845
+ quantity: ReadingQuantitySchema.optional(),
19846
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
19847
+ dateTimeShape: _enum([
19848
+ "date",
19849
+ "time",
19850
+ "datetime"
19851
+ ]).optional(),
19844
19852
  /** Provider-specific extras that don't fit the structured slots.
19845
19853
  * Whitelist what goes here per provider — do NOT dump entire HA
19846
19854
  * attribute blobs (would explode the meta JSON column). */
@@ -19886,6 +19894,80 @@ function mergeSourceInfo(prev, patch) {
19886
19894
  return next;
19887
19895
  }
19888
19896
  /**
19897
+ * Coerce common unit-of-measurement string variants to a canonical form
19898
+ * so the UI's display templates can rely on stable strings. Pure
19899
+ * passthrough for unknown units — kept conservative on purpose, the goal
19900
+ * is to normalise the long tail of HA `unit_of_measurement` values that
19901
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
19902
+ *
19903
+ * Extend the map as we encounter real-world data. Returning the raw
19904
+ * input for unknown values is intentional: providers should never
19905
+ * lose information silently here.
19906
+ */
19907
+ function normalizeUnit(raw) {
19908
+ if (raw == null) return void 0;
19909
+ const trimmed = raw.trim();
19910
+ if (trimmed === "") return void 0;
19911
+ const lower = trimmed.toLowerCase();
19912
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
19913
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
19914
+ if (lower === "k" || lower === "kelvin") return "K";
19915
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
19916
+ if (lower === "lx" || lower === "lux") return "lx";
19917
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
19918
+ if (lower === "kpa") return "kPa";
19919
+ if (lower === "pa") return "Pa";
19920
+ if (lower === "mbar" || lower === "millibar") return "mbar";
19921
+ if (lower === "w" || lower === "watt") return "W";
19922
+ if (lower === "kw" || lower === "kilowatt") return "kW";
19923
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
19924
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
19925
+ if (lower === "v" || lower === "volt") return "V";
19926
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
19927
+ if (lower === "ma" || lower === "milliampere") return "mA";
19928
+ if (lower === "va") return "VA";
19929
+ if (lower === "ppm") return "ppm";
19930
+ if (lower === "ppb") return "ppb";
19931
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
19932
+ if (lower === "km/h" || lower === "kph") return "km/h";
19933
+ if (lower === "mph") return "mph";
19934
+ if (lower === "m/s") return "m/s";
19935
+ if (lower === "m") return "m";
19936
+ if (lower === "m²" || lower === "m2") return "m²";
19937
+ if (lower === "mm") return "mm";
19938
+ if (lower === "cm") return "cm";
19939
+ if (lower === "km") return "km";
19940
+ if (lower === "in" || lower === "inch" || lower === "inches") return "in";
19941
+ if (lower === "ft" || lower === "foot" || lower === "feet") return "ft";
19942
+ if (lower === "kg") return "kg";
19943
+ if (lower === "g") return "g";
19944
+ if (lower === "l" || lower === "liter" || lower === "litre" || lower === "liters" || lower === "litres") return "L";
19945
+ if (lower === "ml") return "mL";
19946
+ if (lower === "gal" || lower === "gallon" || lower === "gallons") return "gal";
19947
+ if (lower === "s" || lower === "sec" || lower === "second" || lower === "seconds") return "s";
19948
+ if (lower === "ms") return "ms";
19949
+ if (lower === "min" || lower === "minute" || lower === "minutes") return "min";
19950
+ if (lower === "h" || lower === "hr" || lower === "hour" || lower === "hours") return "h";
19951
+ if (lower === "d" || lower === "day" || lower === "days") return "d";
19952
+ if (lower === "b" || lower === "byte" || lower === "bytes") return "B";
19953
+ if (lower === "kib") return "KiB";
19954
+ if (lower === "mib") return "MiB";
19955
+ if (lower === "gib") return "GiB";
19956
+ if (lower === "tib") return "TiB";
19957
+ if (lower === "kb") return "kB";
19958
+ if (lower === "mb") return "MB";
19959
+ if (lower === "gb") return "GB";
19960
+ if (lower === "dbm") return "dBm";
19961
+ if (lower === "dbfs") return "dBFS";
19962
+ if (lower === "inhg") return "inHg";
19963
+ if (lower === "mm/h") return "mm/h";
19964
+ if (lower === "w/m²" || lower === "w/m2") return "W/m²";
19965
+ if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
19966
+ if (lower === "kbps") return "kbps";
19967
+ if (lower === "fps") return "fps";
19968
+ return trimmed;
19969
+ }
19970
+ /**
19889
19971
  * Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
19890
19972
  * blob is a free-form `Record<string, unknown>` shared with hardware-
19891
19973
  * identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
@@ -40962,7 +41044,8 @@ method(object({
40962
41044
  "enum"
40963
41045
  ]),
40964
41046
  enumValues: array(string()).optional(),
40965
- item: boolean().optional()
41047
+ item: boolean().optional(),
41048
+ quantity: ReadingQuantitySchema.optional()
40966
41049
  })).readonly(),
40967
41050
  itemArray: object({
40968
41051
  path: string(),
@@ -44806,8 +44889,8 @@ var MaskPolygonShapeSchema = object({
44806
44889
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44807
44890
  var MaskGridShapeSchema = object({
44808
44891
  kind: literal("grid"),
44809
- gridWidth: number(),
44810
- gridHeight: number(),
44892
+ gridWidth: number().meta(readingMeta("none")),
44893
+ gridHeight: number().meta(readingMeta("none")),
44811
44894
  cells: array(boolean())
44812
44895
  });
44813
44896
  discriminatedUnion("kind", [
@@ -56287,27 +56370,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56287
56370
  */
56288
56371
  var AirQualitySensorStatusSchema = object({
56289
56372
  /** Carbon dioxide concentration in ppm. */
56290
- co2Ppm: number().min(0).optional(),
56373
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56291
56374
  /** Total volatile organic compounds in ppb. */
56292
- vocPpb: number().min(0).optional(),
56375
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56293
56376
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56294
- pm25: number().min(0).optional(),
56377
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56295
56378
  /** Particulate matter ≤ 10 μm in µg/m³. */
56296
- pm10: number().min(0).optional(),
56379
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56297
56380
  /** Composite AQI value (typically 0..500). */
56298
- aqi: number().optional(),
56381
+ aqi: number().optional().meta(readingMeta("index")),
56299
56382
  /** Ms epoch when the slice was last updated. */
56300
- lastFetchedAt: number(),
56301
- /** Live display unit of the single metric this slice carries (e.g. HA
56302
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56303
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56304
- * per slice is unambiguous. */
56305
- unit: string().optional(),
56306
- /** Suggested decimal places for numeric display.
56307
- * Populated live from the upstream source when provided (e.g. HA
56308
- * `attributes.suggested_display_precision`). Falls back to
56309
- * auto-formatting when absent. */
56310
- precision: number().int().min(0).max(10).optional()
56383
+ lastFetchedAt: number()
56311
56384
  });
56312
56385
  var airQualitySensorCapability = {
56313
56386
  name: "air-quality-sensor",
@@ -56344,12 +56417,12 @@ var airQualitySensorCapability = {
56344
56417
  },
56345
56418
  pm25: {
56346
56419
  kind: "gauge",
56347
- unit: "ug/m3",
56420
+ unit: "µg/m³",
56348
56421
  deadband: 1
56349
56422
  },
56350
56423
  pm10: {
56351
56424
  kind: "gauge",
56352
- unit: "ug/m3",
56425
+ unit: "µg/m³",
56353
56426
  deadband: 1
56354
56427
  },
56355
56428
  aqi: {
@@ -56367,22 +56440,13 @@ var airQualitySensorCapability = {
56367
56440
  * entries with `device_class: illuminance`.
56368
56441
  */
56369
56442
  var AmbientLightSensorStatusSchema = object({
56370
- /** Current illuminance in lux (lx). */
56371
- lux: number().min(0).meta({
56372
- unit: "lx",
56443
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56444
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56445
+ precision: 0,
56373
56446
  label: "Illuminance"
56374
- }),
56447
+ })),
56375
56448
  /** Ms epoch when the slice was last updated. */
56376
- lastFetchedAt: number(),
56377
- /** Live display unit from the upstream source (e.g. HA
56378
- * `attributes.unit_of_measurement`). The UI prefers this over the
56379
- * role's canonical unit. Absent → fall back to the canonical unit. */
56380
- unit: string().optional(),
56381
- /** Suggested decimal places for numeric display.
56382
- * Populated live from the upstream source when provided (e.g. HA
56383
- * `attributes.suggested_display_precision`). Falls back to
56384
- * auto-formatting when absent. */
56385
- precision: number().int().min(0).max(10).optional()
56449
+ lastFetchedAt: number()
56386
56450
  });
56387
56451
  var ambientLightSensorCapability = {
56388
56452
  name: "ambient-light-sensor",
@@ -56414,12 +56478,7 @@ var ambientLightSensorCapability = {
56414
56478
  } } },
56415
56479
  /** Clock fields: written, but excluded from the compare that decides
56416
56480
  * whether persisting is worth a SQLite commit. */
56417
- volatileStateFields: ["lastFetchedAt"],
56418
- customize: {
56419
- version: 1,
56420
- advanced: "derive",
56421
- targets: []
56422
- }
56481
+ volatileStateFields: ["lastFetchedAt"]
56423
56482
  };
56424
56483
  /**
56425
56484
  * Per-class audio metrics aggregated over a sliding window.
@@ -56646,10 +56705,7 @@ var BatteryStatusSchema = object({
56646
56705
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56647
56706
  * declines to band a non-finite reading, which is the correct shape.
56648
56707
  */
56649
- percentage: number().min(0).max(100).nullable().meta({
56650
- unit: "%",
56651
- label: "Charge"
56652
- }),
56708
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56653
56709
  /**
56654
56710
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56655
56711
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56676,7 +56732,14 @@ var BatteryStatusSchema = object({
56676
56732
  */
56677
56733
  sleeping: boolean().meta({ label: "Sleeping" }),
56678
56734
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56679
- lastUpdated: number(),
56735
+ lastUpdated: number().meta(readingMeta("none")),
56736
+ /**
56737
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56738
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56739
+ * that only report charge. A second reading on this cap, not a second
56740
+ * `numeric-sensor` on the same device.
56741
+ */
56742
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56680
56743
  /**
56681
56744
  * Ms epoch of the last time the device PROVED it was reachable — a
56682
56745
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56728,7 +56791,6 @@ var BATTERY_DEVICE_TYPES = [
56728
56791
  /** Generic Customize target for the percentage reading (D936). */
56729
56792
  var batteryCustomize = {
56730
56793
  version: 1,
56731
- advanced: "derive",
56732
56794
  targets: [{
56733
56795
  id: "percentage",
56734
56796
  cardinality: "scalar",
@@ -56737,7 +56799,7 @@ var batteryCustomize = {
56737
56799
  stateField: "percentage"
56738
56800
  },
56739
56801
  deviceTypes: BATTERY_DEVICE_TYPES,
56740
- apply: "add-or-replace",
56802
+ apply: "add",
56741
56803
  path: "percentage",
56742
56804
  sources: [{
56743
56805
  kind: "field",
@@ -56858,10 +56920,16 @@ onStatusChanged: { data: object({
56858
56920
  /**
56859
56921
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56860
56922
  */
56861
- history: { fields: { percentage: {
56862
- kind: "gauge",
56863
- unit: "%"
56864
- } } },
56923
+ history: { fields: {
56924
+ percentage: {
56925
+ kind: "gauge",
56926
+ unit: "%"
56927
+ },
56928
+ volts: {
56929
+ kind: "gauge",
56930
+ unit: "V"
56931
+ }
56932
+ } },
56865
56933
  customize: batteryCustomize,
56866
56934
  /** Clock fields: written, but excluded from the compare that decides
56867
56935
  * whether persisting is worth a SQLite commit. */
@@ -56908,12 +56976,7 @@ var binaryCapability = {
56908
56976
  history: { fields: { on: { kind: "boolean" } } },
56909
56977
  /** Clock fields: written, but excluded from the compare that decides
56910
56978
  * whether persisting is worth a SQLite commit. */
56911
- volatileStateFields: ["lastChangedAt"],
56912
- customize: {
56913
- version: 1,
56914
- advanced: "derive",
56915
- targets: []
56916
- }
56979
+ volatileStateFields: ["lastChangedAt"]
56917
56980
  };
56918
56981
  /**
56919
56982
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -56929,7 +56992,7 @@ var binaryCapability = {
56929
56992
  */
56930
56993
  var BrightnessStatusSchema = object({
56931
56994
  /** Current level as 0..100 inclusive. Firmware-reported. */
56932
- percentage: number().min(0).max(100),
56995
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56933
56996
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56934
56997
  lastChangedAt: number()
56935
56998
  });
@@ -57383,21 +57446,21 @@ var ClimateControlStatusSchema = object({
57383
57446
  /** Single setpoint in Celsius. Used by single-target modes
57384
57447
  * (heat / cool / dry). Null when the device is in a range mode
57385
57448
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57386
- target: number().nullable(),
57449
+ target: number().nullable().meta(readingMeta("temperature")),
57387
57450
  /** Upper bound of the dual setpoint in Celsius. Populated only
57388
57451
  * in `heat_cool` mode. */
57389
- targetHigh: number().nullable(),
57452
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57390
57453
  /** Lower bound of the dual setpoint in Celsius. Populated only
57391
57454
  * in `heat_cool` mode. */
57392
- targetLow: number().nullable(),
57455
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57393
57456
  /** Target relative humidity (0..100). Null when the device has no
57394
57457
  * humidity control. */
57395
- targetHumidity: number().min(0).max(100).nullable(),
57458
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57396
57459
  /** Current measured temperature in Celsius. Read-only — pushed by
57397
57460
  * the device's internal sensor. */
57398
- currentTemp: number().nullable(),
57461
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57399
57462
  /** Current measured relative humidity (0..100). Read-only. */
57400
- currentHumidity: number().min(0).max(100).nullable(),
57463
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57401
57464
  /** Vertical louver swing on/off. Null when the device has no vertical
57402
57465
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57403
57466
  swingVertical: boolean().nullable(),
@@ -57405,11 +57468,11 @@ var ClimateControlStatusSchema = object({
57405
57468
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57406
57469
  swingHorizontal: boolean().nullable(),
57407
57470
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57408
- minTemp: number().nullable().optional(),
57471
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57409
57472
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57410
- maxTemp: number().nullable().optional(),
57473
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57411
57474
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57412
- tempStep: number().positive().nullable().optional(),
57475
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57413
57476
  /** Ms epoch when the slice was last updated (push or command). */
57414
57477
  lastFetchedAt: number()
57415
57478
  });
@@ -57551,17 +57614,17 @@ var climateControlCapability = {
57551
57614
  * modes to expose in the picker without a slice fetch.
57552
57615
  */
57553
57616
  var RgbTripletSchema = object({
57554
- r: number().int().min(0).max(255),
57555
- g: number().int().min(0).max(255),
57556
- b: number().int().min(0).max(255)
57617
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57618
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57619
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57557
57620
  });
57558
57621
  var HsvTripletSchema = object({
57559
57622
  /** Hue in degrees, 0..360. */
57560
- h: number().min(0).max(360),
57623
+ h: number().min(0).max(360).meta(readingMeta("none")),
57561
57624
  /** Saturation as 0..100 inclusive. */
57562
- s: number().min(0).max(100),
57625
+ s: number().min(0).max(100).meta(readingMeta("none")),
57563
57626
  /** Value/brightness as 0..100 inclusive. */
57564
- v: number().min(0).max(100)
57627
+ v: number().min(0).max(100).meta(readingMeta("none"))
57565
57628
  });
57566
57629
  var ColorInputSchema = discriminatedUnion("mode", [
57567
57630
  object({
@@ -57590,7 +57653,7 @@ var ColorStatusSchema = object({
57590
57653
  /** Populated when `mode === 'hsv'`. */
57591
57654
  hsv: HsvTripletSchema.optional(),
57592
57655
  /** Populated when `mode === 'mired'`. */
57593
- mireds: number().int().optional(),
57656
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57594
57657
  /** Ms epoch of the last operator-driven change. */
57595
57658
  lastChangedAt: number()
57596
57659
  });
@@ -57793,18 +57856,12 @@ _enum([
57793
57856
  "empty",
57794
57857
  "full"
57795
57858
  ]);
57796
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57797
- * each. */
57798
- var ConsumableRemainingUnitSchema = _enum([
57799
- "days",
57800
- "hours",
57801
- "cycles"
57802
- ]);
57803
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57804
- * days). `value` null = unknown (D393). */
57859
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57860
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57861
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57805
57862
  var ConsumableRemainingSchema = object({
57806
- value: number().min(0).nullable(),
57807
- unit: ConsumableRemainingUnitSchema
57863
+ value: number().min(0).nullable().meta(readingMeta("device")),
57864
+ kind: ConsumableRemainingKindSchema
57808
57865
  });
57809
57866
  /** A single consumable item. Either a continuous `level` (remaining
57810
57867
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57817,7 +57874,7 @@ var ConsumableItemSchema = object({
57817
57874
  /** Display name. */
57818
57875
  label: string().min(1),
57819
57876
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57820
- level: number().min(0).max(100).nullable(),
57877
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57821
57878
  /** The provider's discrete verdict when known (binary mode). */
57822
57879
  status: ConsumableStatusSchema.nullable(),
57823
57880
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57899,8 +57956,8 @@ var consumablesCustomize = {
57899
57956
  transforms: [{ kind: "identity" }]
57900
57957
  }],
57901
57958
  literals: [{
57902
- path: "remaining.unit",
57903
- values: [...ConsumableRemainingUnitSchema.options]
57959
+ path: "remaining.kind",
57960
+ values: [...ConsumableRemainingKindSchema.options]
57904
57961
  }]
57905
57962
  },
57906
57963
  {
@@ -58029,7 +58086,6 @@ reset: method(object({
58029
58086
  },
58030
58087
  "remaining.value": {
58031
58088
  kind: "gauge",
58032
- unitFrom: "remaining.unit",
58033
58089
  deadband: 1,
58034
58090
  displayName: "Remaining"
58035
58091
  }
@@ -58099,10 +58155,9 @@ var contactCapability = {
58099
58155
  * (empty array for the other kinds). It lives in the slice because
58100
58156
  * HA `select.*` entities may update their options at runtime.
58101
58157
  *
58102
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58103
- * `kind === 'numeric'` and are absent (undefined) for all other
58104
- * kinds. They are populated live from HA attributes on each state
58105
- * push — consistent with `options` and the sensor-unit approach.
58158
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58159
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58160
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58106
58161
  */
58107
58162
  var ControlStatusSchema = object({
58108
58163
  kind: _enum([
@@ -58114,39 +58169,17 @@ var ControlStatusSchema = object({
58114
58169
  /** Current value. Numeric kind → number; select/text/datetime → string.
58115
58170
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58116
58171
  * or time-only — HA accepts all three forms). */
58117
- value: union([number(), string()]),
58172
+ value: union([number().meta(readingMeta("device")), string()]),
58118
58173
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58119
58174
  options: array(string()),
58120
58175
  /** Ms epoch when the slice was last updated. */
58121
58176
  lastChangedAt: number(),
58122
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58123
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58124
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58125
- unit: string().optional(),
58126
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58127
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58128
- min: number().optional(),
58129
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58130
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58131
- max: number().optional(),
58132
- /** Step increment. Populated live from HA `attributes.step`.
58133
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58134
- step: number().optional(),
58135
- /** Suggested decimal places for numeric display. Populated live from HA
58136
- * `attributes.suggested_display_precision`, or derived from `step` when the
58137
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58138
- * other kinds. Falls back to auto-formatting when absent. */
58139
- precision: number().int().min(0).max(10).optional(),
58140
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58141
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58142
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58143
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58144
- * `has_date`/`has_time` attributes on each push. */
58145
- format: _enum([
58146
- "date",
58147
- "time",
58148
- "datetime"
58149
- ]).optional()
58177
+ /** Minimum allowed value, in the device quantity's base unit. */
58178
+ min: number().optional().meta(readingMeta("device")),
58179
+ /** Maximum allowed value, in the device quantity's base unit. */
58180
+ max: number().optional().meta(readingMeta("device")),
58181
+ /** Step increment, in the device quantity's base unit. */
58182
+ step: number().optional().meta(readingMeta("device"))
58150
58183
  });
58151
58184
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58152
58185
  object({
@@ -58212,9 +58245,9 @@ var CoverStatusSchema = object({
58212
58245
  ]),
58213
58246
  /** 0 = fully closed, 100 = fully open. Null when the device has
58214
58247
  * no intermediate position surface. */
58215
- position: number().min(0).max(100).nullable(),
58248
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58216
58249
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58217
- tiltPosition: number().min(0).max(100).nullable(),
58250
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58218
58251
  /** Ms epoch when the slice was last updated. */
58219
58252
  lastChangedAt: number()
58220
58253
  });
@@ -58317,9 +58350,9 @@ var NormalizedRangeSchema$1 = object({
58317
58350
  var DayNightStatusSchema = object({
58318
58351
  mode: DayNightModeSchema,
58319
58352
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58320
- sensitivity: number().optional(),
58353
+ sensitivity: number().optional().meta(readingMeta("none")),
58321
58354
  /** Delay before the IR-cut filter flips, in seconds. */
58322
- switchDelaySec: number().optional(),
58355
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58323
58356
  lastFetchedAt: number()
58324
58357
  });
58325
58358
  /**
@@ -58537,11 +58570,11 @@ var DishwasherStatusSchema = object({
58537
58570
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58538
58571
  availablePrograms: array(string()),
58539
58572
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58540
- remainingSeconds: number().int().nonnegative().nullable(),
58573
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58541
58574
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58542
- delayedStartSeconds: number().int().nonnegative().nullable(),
58575
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58543
58576
  /** Cycle completion 0..100, or null when not applicable. */
58544
- progressPercent: number().min(0).max(100).nullable(),
58577
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58545
58578
  /** True when the door is reported open; null when the door state is not known. */
58546
58579
  doorOpen: boolean().nullable(),
58547
58580
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58565,7 +58598,7 @@ var DishwasherStatusSchema = object({
58565
58598
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58566
58599
  machineCareDue: boolean().nullable(),
58567
58600
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58568
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58601
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58569
58602
  /** Option states: null when the appliance does not expose the option or has not said. */
58570
58603
  halfLoad: boolean().nullable(),
58571
58604
  hygienePlus: boolean().nullable(),
@@ -58580,7 +58613,7 @@ var DishwasherStatusSchema = object({
58580
58613
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58581
58614
  activeEvents: array(string()),
58582
58615
  /** Numeric error code when the appliance reports one; null otherwise. */
58583
- errorCode: number().nullable(),
58616
+ errorCode: number().nullable().meta(readingMeta("none")),
58584
58617
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58585
58618
  errorLabel: string().nullable(),
58586
58619
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58792,6 +58825,7 @@ var doorbellCustomize = {
58792
58825
  applyAuthority: { kind: "composition-claim" },
58793
58826
  eventName: "onPressed",
58794
58827
  edges: ["rising"],
58828
+ defaultEdge: "rising",
58795
58829
  timestampPath: "lastPressedAt",
58796
58830
  counterPath: "pressCountSinceStart",
58797
58831
  highWater: "composer-advance",
@@ -58810,7 +58844,7 @@ var DoorbellStatusSchema = object({
58810
58844
  /** Ms epoch of the last press. null = never observed since this provider started. */
58811
58845
  lastPressedAt: number().nullable(),
58812
58846
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58813
- pressCountSinceStart: number()
58847
+ pressCountSinceStart: number().meta(readingMeta("count"))
58814
58848
  });
58815
58849
  var DoorbellPressEventSchema = object({
58816
58850
  deviceId: number(),
@@ -58875,17 +58909,7 @@ onPressed: {
58875
58909
  history: "none",
58876
58910
  customize: doorbellCustomize
58877
58911
  };
58878
- /**
58879
- * Enum-state sensor — a string value picked from a finite option set.
58880
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58881
- * states, weather conditions, contact-source channels, mode strings).
58882
- * The option set is stable across observations; it is persisted in the
58883
- * device config blob (via the control slice at adoption) and is NOT
58884
- * stored in `sourceInfo`.
58885
- */
58886
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58887
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58888
- var EnumSensorDateTimeFormatSchema = _enum([
58912
+ _enum([
58889
58913
  "date",
58890
58914
  "time",
58891
58915
  "datetime"
@@ -58893,12 +58917,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58893
58917
  var EnumSensorStatusSchema = object({
58894
58918
  value: string(),
58895
58919
  /**
58896
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58897
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58898
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58899
- * a time-only sensor → `'time'`.
58920
+ * Set when `value` is a timestamp rather than an enum state, so history
58921
+ * does not learn ISO strings as states. The display shape (date / time /
58922
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58900
58923
  */
58901
- format: EnumSensorDateTimeFormatSchema.optional(),
58924
+ timestamp: boolean().optional(),
58902
58925
  /** Ms epoch when the slice was last updated. */
58903
58926
  lastFetchedAt: number()
58904
58927
  });
@@ -58925,11 +58948,11 @@ var enumSensorCapability = {
58925
58948
  * History (D850): the value as an `enum` series (an appliance's program or
58926
58949
  * operation state, an HVAC action). The option set is per device, so the
58927
58950
  * value list is LEARNED as values are first seen (bounded). A sensor with a
58928
- * `format` carries a timestamp, not a state, and is not recorded.
58951
+ * `timestamp` carries a clock, not a state, and is not recorded.
58929
58952
  */
58930
58953
  history: { fields: { value: {
58931
58954
  kind: "enum",
58932
- skipWhenSet: "format"
58955
+ skipWhenSet: "timestamp"
58933
58956
  } } },
58934
58957
  /** Clock fields: written, but excluded from the compare that decides
58935
58958
  * whether persisting is worth a SQLite commit. */
@@ -58949,13 +58972,13 @@ var EventFireSchema = object({
58949
58972
  deviceId: number(),
58950
58973
  eventType: string(),
58951
58974
  data: record(string(), unknown()).nullable(),
58952
- timestamp: number(),
58953
- seq: number()
58975
+ timestamp: number().meta(readingMeta("none")),
58976
+ seq: number().meta(readingMeta("count"))
58954
58977
  });
58955
58978
  var EventEmitterStatusSchema = object({
58956
58979
  eventTypes: array(string()),
58957
58980
  lastEvent: EventFireSchema.nullable(),
58958
- eventCountSinceStart: number()
58981
+ eventCountSinceStart: number().meta(readingMeta("count"))
58959
58982
  });
58960
58983
  var eventEmitterCapability = {
58961
58984
  name: "event-emitter",
@@ -59368,10 +59391,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59368
59391
  var FanControlStatusSchema = object({
59369
59392
  /** Active speed as 0..100 inclusive. Null when the device has no
59370
59393
  * speed surface (single-speed fan). */
59371
- percentage: number().min(0).max(100).nullable(),
59394
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59372
59395
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59373
59396
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59374
- percentageStep: number().positive().optional(),
59397
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59375
59398
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59376
59399
  * — empty string when no preset is active or supported. */
59377
59400
  preset: string(),
@@ -59492,7 +59515,7 @@ var FeatureProbeStatusSchema = object({
59492
59515
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59493
59516
  model: string().nullable(),
59494
59517
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59495
- channelCount: number().nullable(),
59518
+ channelCount: number().nullable().meta(readingMeta("count")),
59496
59519
  /**
59497
59520
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59498
59521
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59617,7 +59640,7 @@ var gasCapability = {
59617
59640
  };
59618
59641
  var HeatingValveStatusSchema = object({
59619
59642
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59620
- position: number().min(0).max(100).nullable(),
59643
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59621
59644
  /** Ms epoch when the slice was last updated (push or poll). */
59622
59645
  lastFetchedAt: number()
59623
59646
  });
@@ -59660,9 +59683,9 @@ var HumidifierStatusSchema = object({
59660
59683
  /** Whether the humidifier is currently on. */
59661
59684
  on: boolean(),
59662
59685
  /** Current measured relative humidity (0..100). Null when not reported. */
59663
- currentHumidity: number().min(0).max(100).nullable(),
59686
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59664
59687
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59665
- targetHumidity: number().min(0).max(100).nullable(),
59688
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59666
59689
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59667
59690
  * device has no mode surface. */
59668
59691
  mode: string().nullable(),
@@ -59672,9 +59695,9 @@ var HumidifierStatusSchema = object({
59672
59695
  * `idle` / `off`). Null when the device doesn't report it. */
59673
59696
  action: string().nullable(),
59674
59697
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59675
- minHumidity: number().nullable(),
59698
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59676
59699
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59677
- maxHumidity: number().nullable(),
59700
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59678
59701
  /** Ms epoch when the slice was last updated. */
59679
59702
  lastChangedAt: number()
59680
59703
  });
@@ -59744,22 +59767,13 @@ var humidifierCapability = {
59744
59767
  * `sourceInfo`.
59745
59768
  */
59746
59769
  var HumiditySensorStatusSchema = object({
59747
- /** Current relative humidity, 0..100. */
59748
- percent: number().min(0).max(100).meta({
59749
- unit: "%",
59770
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59771
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59772
+ precision: 0,
59750
59773
  label: "Humidity"
59751
- }),
59774
+ })),
59752
59775
  /** Ms epoch when the slice was last updated. */
59753
- lastFetchedAt: number(),
59754
- /** Live display unit from the upstream source (e.g. HA
59755
- * `attributes.unit_of_measurement`). The UI prefers this over the
59756
- * role's canonical unit. Absent → fall back to the canonical unit. */
59757
- unit: string().optional(),
59758
- /** Suggested decimal places for numeric display.
59759
- * Populated live from the upstream source when provided (e.g. HA
59760
- * `attributes.suggested_display_precision`). Falls back to
59761
- * auto-formatting when absent. */
59762
- precision: number().int().min(0).max(10).optional()
59776
+ lastFetchedAt: number()
59763
59777
  });
59764
59778
  var humiditySensorCapability = {
59765
59779
  name: "humidity-sensor",
@@ -59790,12 +59804,7 @@ var humiditySensorCapability = {
59790
59804
  } } },
59791
59805
  /** Clock fields: written, but excluded from the compare that decides
59792
59806
  * whether persisting is worth a SQLite commit. */
59793
- volatileStateFields: ["lastFetchedAt"],
59794
- customize: {
59795
- version: 1,
59796
- advanced: "derive",
59797
- targets: []
59798
- }
59807
+ volatileStateFields: ["lastFetchedAt"]
59799
59808
  };
59800
59809
  /**
59801
59810
  * Image display cap. Models a single still image exposed by an integration —
@@ -59816,7 +59825,7 @@ var ImageStatusSchema = object({
59816
59825
  * entity exposes no `entity_picture` (yet). */
59817
59826
  url: string().nullable(),
59818
59827
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59819
- lastUpdated: number().nullable()
59828
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59820
59829
  });
59821
59830
  var imageCapability = {
59822
59831
  name: "image",
@@ -59904,19 +59913,19 @@ var NormalizedRangeSchema = object({
59904
59913
  */
59905
59914
  var ImageSettingsStatusSchema = object({
59906
59915
  /** Normalized 0–100. */
59907
- brightness: number().optional(),
59916
+ brightness: number().optional().meta(readingMeta("none")),
59908
59917
  /** Normalized 0–100. */
59909
- contrast: number().optional(),
59918
+ contrast: number().optional().meta(readingMeta("none")),
59910
59919
  /** Normalized 0–100. */
59911
- saturation: number().optional(),
59920
+ saturation: number().optional().meta(readingMeta("none")),
59912
59921
  /** Normalized 0–100. */
59913
- sharpness: number().optional(),
59922
+ sharpness: number().optional().meta(readingMeta("none")),
59914
59923
  mirror: boolean().optional(),
59915
59924
  flip: boolean().optional(),
59916
59925
  rotate: ImageRotateSchema.optional(),
59917
59926
  whiteBalance: WhiteBalanceModeSchema.optional(),
59918
59927
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59919
- warmth: number().optional(),
59928
+ warmth: number().optional().meta(readingMeta("none")),
59920
59929
  exposureMode: ExposureModeSchema.optional(),
59921
59930
  backlightMode: BacklightModeSchema.optional(),
59922
59931
  lastFetchedAt: number()
@@ -60195,10 +60204,10 @@ var IntercomAbilitySchema = object({
60195
60204
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60196
60205
  codecs: array(string()),
60197
60206
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60198
- sampleRate: number(),
60207
+ sampleRate: number().meta(readingMeta("none")),
60199
60208
  duplex: _enum(["half", "full"]),
60200
60209
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60201
- maxBacklogMs: number()
60210
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60202
60211
  });
60203
60212
  var IntercomStatusSchema = object({
60204
60213
  /** True while a talk session is open. */
@@ -60425,13 +60434,13 @@ var LawnMowerControlStatusSchema = object({
60425
60434
  activity: LawnMowerActivitySchema,
60426
60435
  /** 0..100 battery percentage. Null when the device has no battery
60427
60436
  * reading. */
60428
- batteryLevel: number().min(0).max(100).nullable(),
60437
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60429
60438
  /** 0..100 mowing-completion percentage of the current task, or null when no
60430
60439
  * task is active / progress is unavailable. */
60431
- progressPercent: number().min(0).max(100).nullable(),
60440
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60432
60441
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60433
60442
  * or null when unknown. */
60434
- currentCode: number().nullable(),
60443
+ currentCode: number().nullable().meta(readingMeta("none")),
60435
60444
  /** Human label for {@link currentCode}, or null when undecodable. */
60436
60445
  currentCodeLabel: string().nullable(),
60437
60446
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -60964,7 +60973,7 @@ var MediaPlayerStatusSchema = object({
60964
60973
  state: MediaPlayerStateSchema,
60965
60974
  /** Volume as 0..100 inclusive. Null when the device has no volume
60966
60975
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
60967
- volumeLevel: number().min(0).max(100).nullable(),
60976
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60968
60977
  /** Mute toggle distinct from volume=0. Null when no mute surface.
60969
60978
  * Pair with `DeviceFeature.MediaPlayerMute`. */
60970
60979
  isMuted: boolean().nullable(),
@@ -60977,10 +60986,10 @@ var MediaPlayerStatusSchema = object({
60977
60986
  currentMedia: MediaInfoSchema.nullable(),
60978
60987
  /** Current playback position in ms. Null when not seekable or
60979
60988
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
60980
- positionMs: number().int().nonnegative().nullable(),
60989
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
60981
60990
  /** Total duration of the current media in ms. Null when unknown
60982
60991
  * (live stream) or nothing is playing. */
60983
- durationMs: number().int().nonnegative().nullable(),
60992
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
60984
60993
  /** Shuffle toggle. Null when no shuffle surface.
60985
60994
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
60986
60995
  shuffle: boolean().nullable(),
@@ -61289,7 +61298,7 @@ var motionCustomize = {
61289
61298
  eventName: "onMotionChanged"
61290
61299
  },
61291
61300
  deviceTypes: [DeviceType.Camera],
61292
- apply: "replace",
61301
+ apply: "add",
61293
61302
  applyAuthority: {
61294
61303
  kind: "camera-sensors",
61295
61304
  timeoutField: "motionCooldownMs"
@@ -61361,7 +61370,7 @@ var MotionStatusSchema = object({
61361
61370
  * it, every provider seeded it at boot, and every session left it
61362
61371
  * `detected: true` after its detach.
61363
61372
  */
61364
- autoClearAfterMs: number().nullable()
61373
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61365
61374
  });
61366
61375
  /**
61367
61376
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61543,7 +61552,7 @@ var MotionZoneRegionSchema = object({
61543
61552
  * the grid region(s). */
61544
61553
  var MotionZoneStatusSchema = object({
61545
61554
  enabled: boolean(),
61546
- sensitivity: number(),
61555
+ sensitivity: number().meta(readingMeta("none")),
61547
61556
  /** Grid region(s). Today exactly one `grid` shape. */
61548
61557
  regions: array(MotionZoneRegionSchema),
61549
61558
  lastFetchedAt: number()
@@ -61913,7 +61922,7 @@ var NavigationStatusSchema = object({
61913
61922
  following: boolean(),
61914
61923
  flash: boolean(),
61915
61924
  lightMode: NavigationLightModeSchema,
61916
- lightLevel: number().min(40).max(100),
61925
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
61917
61926
  /** Ms epoch when the slice was last updated. */
61918
61927
  lastChangedAt: number()
61919
61928
  });
@@ -62047,13 +62056,13 @@ var NetworkLinkStatusSchema = object({
62047
62056
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62048
62057
  * SKIP a null rather than coerce it.
62049
62058
  */
62050
- signalPercent: number().min(0).max(100).nullable(),
62059
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62051
62060
  /** Raw received signal strength in dBm, when the firmware reports one. */
62052
- rssiDbm: number().optional(),
62061
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62053
62062
  /** Network name of a wireless link, when the firmware reports it. */
62054
62063
  ssid: string().optional(),
62055
62064
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62056
- lastUpdated: number()
62065
+ lastUpdated: number().meta(readingMeta("none"))
62057
62066
  });
62058
62067
  var networkLinkCapability = {
62059
62068
  name: "network-link",
@@ -62438,7 +62447,7 @@ var NotifierStatusSchema = object({
62438
62447
  lastError: string().nullable(),
62439
62448
  /** Number of deliveries currently buffered server-side (rate-limited
62440
62449
  * notifiers may queue). 0 when the provider sends synchronously. */
62441
- queueDepth: number().int().nonnegative(),
62450
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62442
62451
  /** Per-feature capability matrix — authoritative source for the
62443
62452
  * compose-form field gating. */
62444
62453
  supports: NotifierSupportsSchema
@@ -62530,11 +62539,9 @@ onSent: { data: object({
62530
62539
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62531
62540
  * for: water flow, distance, weight, frequency, signal strength, …).
62532
62541
  *
62533
- * The `unit` and `precision` fields carry the live display metadata that
62534
- * the upstream source provides with each state push (HA
62535
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62536
- * The UI reads them directly from the slice — they are the single source
62537
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
62542
+ * The quantity is not on this slice. It is chosen when the device is
62543
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
62544
+ * that quantity's base unit. `null` is a refused sample (D393).
62538
62545
  *
62539
62546
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62540
62547
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62543,15 +62550,8 @@ onSent: { data: object({
62543
62550
  * numeric values land here.
62544
62551
  */
62545
62552
  var NumericSensorStatusSchema = object({
62546
- value: number(),
62547
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62548
- * Populated live from the upstream source on each state push.
62549
- * Absent for unitless sensors. */
62550
- unit: string().optional(),
62551
- /** Suggested decimal places for numeric display.
62552
- * Populated live from the upstream source when provided.
62553
- * Falls back to auto-formatting when absent. */
62554
- precision: number().int().min(0).max(10).optional(),
62553
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
62554
+ value: number().nullable().meta(readingMeta("device")),
62555
62555
  /** Ms epoch when the slice was last updated. */
62556
62556
  lastFetchedAt: number()
62557
62557
  });
@@ -62575,11 +62575,11 @@ var numericSensorCapability = {
62575
62575
  */
62576
62576
  durability: "restored",
62577
62577
  /**
62578
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
62578
+ * History (D850): recorded series. The unit is the device quantity's base,
62579
+ * resolved when the series is read — the cap has no single unit.
62579
62580
  */
62580
62581
  history: { fields: { value: {
62581
62582
  kind: "gauge",
62582
- unitFrom: "unit",
62583
62583
  minIntervalMs: 3e4
62584
62584
  } } },
62585
62585
  /** Clock fields: written, but excluded from the compare that decides
@@ -62871,23 +62871,26 @@ var PetFeederStatusSchema = object({
62871
62871
  /** Food currently in the bowl (grams). Null when the device has not
62872
62872
  * reported a reading yet. On dual-hopper models this is the combined
62873
62873
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62874
- foodLevel: number().nullable(),
62874
+ foodLevel: number().nullable().meta(readingMeta("none")),
62875
62875
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62876
62876
  * single-hopper models. */
62877
- food1: number().nullable(),
62877
+ food1: number().nullable().meta(readingMeta("none")),
62878
62878
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62879
62879
  * single-hopper models. */
62880
- food2: number().nullable(),
62880
+ food2: number().nullable().meta(readingMeta("none")),
62881
62881
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62882
62882
  * (`device_class: problem`, on = low). True when the bowl is empty /
62883
62883
  * below the feeder's low threshold. */
62884
62884
  lowFood: boolean(),
62885
62885
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62886
62886
  * device has no battery reading. */
62887
- batteryPower: number().min(0).max(100).nullable(),
62888
- /** Days of desiccant life remaining. Null when the model has no
62889
- * desiccant sensor. */
62890
- desiccantLeftDays: number().nullable(),
62887
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62888
+ /**
62889
+ * Desiccant life remaining, stored in seconds (duration base, D944).
62890
+ * Providers convert from the days the feeder reports. Null when the
62891
+ * model has no desiccant sensor.
62892
+ */
62893
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62891
62894
  /** True while a feed is in progress. */
62892
62895
  feeding: boolean(),
62893
62896
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62898,7 +62901,7 @@ var PetFeederStatusSchema = object({
62898
62901
  * with `errorCode` for consumers that want the raw integer. */
62899
62902
  error: string().nullable(),
62900
62903
  /** Raw device error code (0 / null = no error). */
62901
- errorCode: number().nullable(),
62904
+ errorCode: number().nullable().meta(readingMeta("none")),
62902
62905
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62903
62906
  isDualHopper: boolean(),
62904
62907
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -62908,7 +62911,7 @@ var PetFeederStatusSchema = object({
62908
62911
  /** Play a chime when dispensing. */
62909
62912
  feedSound: boolean(),
62910
62913
  /** Speaker / prompt volume level (device-scaled integer). */
62911
- volume: number(),
62914
+ volume: number().meta(readingMeta("none")),
62912
62915
  /** Ms epoch when the slice was last refreshed from the cloud. */
62913
62916
  lastFetchedAt: number()
62914
62917
  });
@@ -63027,7 +63030,7 @@ var petFeederCapability = {
63027
63030
  },
63028
63031
  desiccantLeftDays: {
63029
63032
  kind: "gauge",
63030
- unit: "days"
63033
+ unit: "s"
63031
63034
  }
63032
63035
  } }
63033
63036
  };
@@ -63428,36 +63431,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63428
63431
  */
63429
63432
  var PowerMeterStatusSchema = object({
63430
63433
  /** Instantaneous power draw in watts. */
63431
- watts: number().optional().meta({
63432
- unit: "W",
63433
- label: "Power"
63434
- }),
63434
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63435
63435
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63436
- kwhTotal: number().optional().meta({
63437
- unit: "kWh",
63438
- label: "Energy"
63439
- }),
63436
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63440
63437
  /** Voltage in volts. */
63441
- volts: number().optional(),
63438
+ volts: number().optional().meta(readingMeta("voltage")),
63442
63439
  /** Current in amperes. */
63443
- amps: number().optional(),
63444
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63445
- va: number().optional(),
63440
+ amps: number().optional().meta(readingMeta("current")),
63441
+ /** Apparent power in volt-amperes. */
63442
+ va: number().optional().meta(readingMeta("apparent-power")),
63446
63443
  /** Power factor (0..1) if reported. */
63447
- powerFactor: number().min(0).max(1).optional(),
63444
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63448
63445
  /** Ms epoch when the slice was last updated. */
63449
- lastFetchedAt: number(),
63450
- /** Live display unit of the single metric this slice carries (e.g. HA
63451
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63452
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63453
- * unit per slice is unambiguous. The UI prefers this over the role's
63454
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63455
- unit: string().optional(),
63456
- /** Suggested decimal places for numeric display.
63457
- * Populated live from the upstream source when provided (e.g. HA
63458
- * `attributes.suggested_display_precision`). Falls back to
63459
- * auto-formatting when absent. */
63460
- precision: number().int().min(0).max(10).optional()
63446
+ lastFetchedAt: number()
63461
63447
  });
63462
63448
  var powerMeterCapability = {
63463
63449
  name: "power-meter",
@@ -63521,12 +63507,7 @@ var powerMeterCapability = {
63521
63507
  } },
63522
63508
  /** Clock fields: written, but excluded from the compare that decides
63523
63509
  * whether persisting is worth a SQLite commit. */
63524
- volatileStateFields: ["lastFetchedAt"],
63525
- customize: {
63526
- version: 1,
63527
- advanced: "derive",
63528
- targets: []
63529
- }
63510
+ volatileStateFields: ["lastFetchedAt"]
63530
63511
  };
63531
63512
  /**
63532
63513
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63546,11 +63527,11 @@ var powerMeterCapability = {
63546
63527
  */
63547
63528
  var GpsLocationSchema = object({
63548
63529
  /** Latitude in decimal degrees, -90..90. */
63549
- latitude: number().min(-90).max(90),
63530
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63550
63531
  /** Longitude in decimal degrees, -180..180. */
63551
- longitude: number().min(-180).max(180),
63532
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63552
63533
  /** Reported accuracy in meters (lower = better). */
63553
- accuracyMeters: number().nonnegative()
63534
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63554
63535
  });
63555
63536
  var PresenceStatusSchema = object({
63556
63537
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63562,7 +63543,7 @@ var PresenceStatusSchema = object({
63562
63543
  gps: GpsLocationSchema.nullable(),
63563
63544
  /** Optional battery level of the tracking device (0..100). Null
63564
63545
  * when not reported. */
63565
- batteryPercent: number().min(0).max(100).nullable(),
63546
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63566
63547
  /** Ms epoch when the slice was last updated. */
63567
63548
  lastChangedAt: number()
63568
63549
  });
@@ -63612,19 +63593,10 @@ var presenceCapability = {
63612
63593
  * `sourceInfo`.
63613
63594
  */
63614
63595
  var PressureSensorStatusSchema = object({
63615
- /** Current pressure in hPa. */
63616
- hpa: number(),
63596
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
63597
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63617
63598
  /** Ms epoch when the slice was last updated. */
63618
- lastFetchedAt: number(),
63619
- /** Live display unit from the upstream source (e.g. HA
63620
- * `attributes.unit_of_measurement`). The UI prefers this over the
63621
- * role's canonical unit. Absent → fall back to the canonical unit. */
63622
- unit: string().optional(),
63623
- /** Suggested decimal places for numeric display.
63624
- * Populated live from the upstream source when provided (e.g. HA
63625
- * `attributes.suggested_display_precision`). Falls back to
63626
- * auto-formatting when absent. */
63627
- precision: number().int().min(0).max(10).optional()
63599
+ lastFetchedAt: number()
63628
63600
  });
63629
63601
  var pressureSensorCapability = {
63630
63602
  name: "pressure-sensor",
@@ -64038,8 +64010,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64038
64010
  }), _void(), { kind: "mutation" });
64039
64011
  var PtzAutotrackSettingsSchema = object({
64040
64012
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64041
- stopDelaySeconds: number().int().min(0).max(300),
64042
- disappearDelaySeconds: number().int().min(0).max(300)
64013
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64014
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64043
64015
  });
64044
64016
  /**
64045
64017
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64201,7 +64173,7 @@ var RunningStateStatusSchema = object({
64201
64173
  /** null = unknown — never coerced to false (D393). */
64202
64174
  running: boolean().nullable(),
64203
64175
  /** Epoch ms the current boolean began; null while `running` is null. */
64204
- since: number().nullable(),
64176
+ since: number().nullable().meta(readingMeta("none")),
64205
64177
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64206
64178
  phase: string().nullable(),
64207
64179
  /** null until a reading has been published (D393). */
@@ -64216,7 +64188,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64216
64188
  /** Generic Customize target for the running boolean (D936). */
64217
64189
  var runningCustomize = {
64218
64190
  version: 1,
64219
- advanced: "derive",
64220
64191
  targets: [{
64221
64192
  id: "running",
64222
64193
  cardinality: "scalar",
@@ -64236,7 +64207,7 @@ var runningCustomize = {
64236
64207
  DeviceType.Thermostat,
64237
64208
  DeviceType.PetFeeder
64238
64209
  ],
64239
- apply: "add-or-replace",
64210
+ apply: "add",
64240
64211
  path: "running",
64241
64212
  sources: [
64242
64213
  {
@@ -64252,6 +64223,7 @@ var runningCustomize = {
64252
64223
  requiredUnit: "W",
64253
64224
  transforms: [{
64254
64225
  kind: "threshold",
64226
+ comparisons: ["gte", "lte"],
64255
64227
  comparison: "gte",
64256
64228
  defaultThreshold: 5,
64257
64229
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -65499,9 +65471,9 @@ var RecordTriggerSchema = _enum([
65499
65471
  */
65500
65472
  var RecordWindowSchema = object({
65501
65473
  trigger: RecordTriggerSchema,
65502
- day: number().int().min(0).max(6),
65503
- startMinute: number().int().min(0).max(1439),
65504
- endMinute: number().int().min(1).max(1440)
65474
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
65475
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
65476
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65505
65477
  });
65506
65478
  /** Status of one physical volume, as the camera itself describes it. */
65507
65479
  var OnboardStorageVolumeSchema = object({
@@ -65577,10 +65549,10 @@ var RecordingOnboardStatusSchema = object({
65577
65549
  /** Master "record to the card at all" switch. */
65578
65550
  enabled: boolean().nullable(),
65579
65551
  overwriteWhenFull: boolean().nullable(),
65580
- preRecordSec: number().nullable(),
65581
- postRecordSec: number().nullable(),
65552
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
65553
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65582
65554
  /** Length of one recorded file, in minutes. */
65583
- segmentMinutes: number().nullable(),
65555
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65584
65556
  /** The primary track's weekly windows, flattened. */
65585
65557
  windows: array(RecordWindowSchema),
65586
65558
  /**
@@ -65596,7 +65568,7 @@ var RecordingOnboardStatusSchema = object({
65596
65568
  * provider reporting a non-zero count also reports the schedule as not
65597
65569
  * writable.
65598
65570
  */
65599
- unreadableWindows: number(),
65571
+ unreadableWindows: number().meta(readingMeta("count")),
65600
65572
  /**
65601
65573
  * The camera is scheduled to record and has NO usable storage.
65602
65574
  *
@@ -66441,7 +66413,7 @@ var CamStreamDescriptorSchema = object({
66441
66413
  url: string().optional(),
66442
66414
  codec: string().optional(),
66443
66415
  resolution: CamStreamResolutionSchema.optional(),
66444
- fps: number().positive().optional(),
66416
+ fps: number().positive().optional().meta(readingMeta("rate")),
66445
66417
  label: string().optional(),
66446
66418
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66447
66419
  deviceFeatures: array(string()).optional(),
@@ -66528,18 +66500,18 @@ var StreamProfileSchema = _enum([
66528
66500
  ]);
66529
66501
  /** The current encoding config of one stream profile. */
66530
66502
  var StreamProfileConfigSchema = object({
66531
- width: number(),
66532
- height: number(),
66503
+ width: number().meta(readingMeta("none")),
66504
+ height: number().meta(readingMeta("none")),
66533
66505
  codec: _enum(["h264", "h265"]),
66534
- framerate: number(),
66535
- bitrate: number(),
66506
+ framerate: number().meta(readingMeta("rate")),
66507
+ bitrate: number().meta(readingMeta("bitrate")),
66536
66508
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66537
66509
  encoderProfile: _enum([
66538
66510
  "high",
66539
66511
  "main",
66540
66512
  "baseline"
66541
66513
  ]).optional(),
66542
- gop: number().optional(),
66514
+ gop: number().optional().meta(readingMeta("none")),
66543
66515
  /**
66544
66516
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66545
66517
  *
@@ -66760,12 +66732,7 @@ var switchCapability = {
66760
66732
  label: "On",
66761
66733
  kind: "boolean"
66762
66734
  }
66763
- }] },
66764
- customize: {
66765
- version: 1,
66766
- advanced: "derive",
66767
- targets: []
66768
- }
66735
+ }] }
66769
66736
  };
66770
66737
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66771
66738
  function hasControlCharacter(value) {
@@ -67348,35 +67315,21 @@ var tamperCapability = {
67348
67315
  * entries with `device_class: temperature` and any future native
67349
67316
  * thermometer.
67350
67317
  *
67351
- * Unit normalisation: providers convert to Celsius before storing.
67352
- * The slice value is always Celsius so cross-cap aggregators
67353
- * (climate-control's `currentTemp`, energy analytics) can compose
67354
- * without per-source unit fixups. The canonical display unit (`°C`) is
67355
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67356
- * `sourceInfo`.
67318
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67319
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67357
67320
  *
67358
67321
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67359
67322
  * frozen feed (provider hung) distinct from a "temperature hasn't
67360
67323
  * changed" steady state.
67361
67324
  */
67362
67325
  var TemperatureSensorStatusSchema = object({
67363
- /** Current temperature in Celsius. */
67364
- celsius: number().meta({
67365
- unit: "°C",
67326
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67327
+ celsius: number().nullable().meta(readingMeta("temperature", {
67328
+ precision: 1,
67366
67329
  label: "Temperature"
67367
- }),
67330
+ })),
67368
67331
  /** Ms epoch when the slice was last updated (push or poll). */
67369
- lastFetchedAt: number(),
67370
- /** Live display unit from the upstream source (e.g. HA
67371
- * `attributes.unit_of_measurement`). The UI prefers this over the
67372
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67373
- * Absent → the UI falls back to the role's canonical unit. */
67374
- unit: string().optional(),
67375
- /** Suggested decimal places for numeric display.
67376
- * Populated live from the upstream source when provided (e.g. HA
67377
- * `attributes.suggested_display_precision`). Falls back to
67378
- * auto-formatting when absent. */
67379
- precision: number().int().min(0).max(10).optional()
67332
+ lastFetchedAt: number()
67380
67333
  });
67381
67334
  var temperatureSensorCapability = {
67382
67335
  name: "temperature-sensor",
@@ -67407,12 +67360,7 @@ var temperatureSensorCapability = {
67407
67360
  } } },
67408
67361
  /** Clock fields: written, but excluded from the compare that decides
67409
67362
  * whether persisting is worth a SQLite commit. */
67410
- volatileStateFields: ["lastFetchedAt"],
67411
- customize: {
67412
- version: 1,
67413
- advanced: "derive",
67414
- targets: []
67415
- }
67363
+ volatileStateFields: ["lastFetchedAt"]
67416
67364
  };
67417
67365
  /**
67418
67366
  * toast — system-scoped singleton capability that streams toast
@@ -67695,16 +67643,16 @@ var VacuumControlStatusSchema = object({
67695
67643
  ]),
67696
67644
  /** 0..100 battery percentage. Null when the device has no battery
67697
67645
  * reading. */
67698
- batteryLevel: number().min(0).max(100).nullable(),
67646
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67699
67647
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67700
67648
  * the vacuum has no speed control. */
67701
67649
  fanSpeed: string().nullable(),
67702
67650
  /** Speed tokens the hardware accepts — drives the UI selector. */
67703
67651
  availableFanSpeeds: array(string()),
67704
67652
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67705
- progressPercent: number().min(0).max(100).nullable(),
67653
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67706
67654
  /** Current error code (0 / null = no error). */
67707
- errorCode: number().nullable(),
67655
+ errorCode: number().nullable().meta(readingMeta("none")),
67708
67656
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67709
67657
  errorLabel: string().nullable(),
67710
67658
  /** Ms epoch when the slice was last updated. */
@@ -67782,7 +67730,7 @@ var ValveStatusSchema = object({
67782
67730
  ]),
67783
67731
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67784
67732
  * intermediate position surface. */
67785
- position: number().min(0).max(100).nullable(),
67733
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67786
67734
  /** Ms epoch when the slice was last updated. */
67787
67735
  lastChangedAt: number()
67788
67736
  });
@@ -67890,9 +67838,9 @@ var vibrationCapability = {
67890
67838
  */
67891
67839
  var WaterHeaterStatusSchema = object({
67892
67840
  /** Current measured temperature. Null when not reported. */
67893
- currentTemp: number().nullable(),
67841
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67894
67842
  /** Target temperature setpoint. Null when no setpoint surface. */
67895
- targetTemp: number().nullable(),
67843
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67896
67844
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67897
67845
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67898
67846
  * device reports an unknown state. */
@@ -67903,9 +67851,9 @@ var WaterHeaterStatusSchema = object({
67903
67851
  * has no away surface. */
67904
67852
  away: boolean().nullable(),
67905
67853
  /** HA `min_temp` attribute. Null when not reported. */
67906
- minTemp: number().nullable(),
67854
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67907
67855
  /** HA `max_temp` attribute. Null when not reported. */
67908
- maxTemp: number().nullable(),
67856
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67909
67857
  /** Ms epoch when the slice was last updated. */
67910
67858
  lastChangedAt: number()
67911
67859
  });
@@ -67990,25 +67938,18 @@ var waterHeaterCapability = {
67990
67938
  * Forecast deferred — current conditions only for v1.
67991
67939
  */
67992
67940
  var WeatherStatusSchema = object({
67993
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
67994
- * when no condition has been reported yet. */
67941
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
67995
67942
  condition: string().nullable(),
67996
- /** Current temperature in the reported unit. Null when not provided. */
67997
- temperature: number().nullable(),
67998
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
67999
- temperatureUnit: string().nullable(),
67943
+ /** Current temperature in Celsius. Null when not provided or refused. */
67944
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68000
67945
  /** Relative humidity (0..100). Null when not provided. */
68001
- humidity: number().min(0).max(100).nullable(),
68002
- /** Barometric pressure in the reported unit. Null when not provided. */
68003
- pressure: number().nullable(),
68004
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68005
- pressureUnit: string().nullable(),
68006
- /** Wind speed in the reported unit. Null when not provided. */
68007
- windSpeed: number().nullable(),
68008
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68009
- windSpeedUnit: string().nullable(),
67946
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
67947
+ /** Barometric pressure in hPa. Null when not provided or refused. */
67948
+ pressure: number().nullable().meta(readingMeta("pressure")),
67949
+ /** Wind speed in metres per second. Null when not provided or refused. */
67950
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68010
67951
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68011
- windBearing: number().nullable(),
67952
+ windBearing: number().nullable().meta(readingMeta("angle")),
68012
67953
  /** Ms epoch when the slice was last updated. */
68013
67954
  lastFetchedAt: number()
68014
67955
  });
@@ -68036,12 +67977,12 @@ var weatherCapability = {
68036
67977
  */
68037
67978
  durability: "session",
68038
67979
  /**
68039
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
67980
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68040
67981
  */
68041
67982
  history: { fields: {
68042
67983
  temperature: {
68043
67984
  kind: "gauge",
68044
- unitFrom: "temperatureUnit",
67985
+ unit: "°C",
68045
67986
  deadband: .2
68046
67987
  },
68047
67988
  humidity: {
@@ -68051,12 +67992,12 @@ var weatherCapability = {
68051
67992
  },
68052
67993
  pressure: {
68053
67994
  kind: "gauge",
68054
- unitFrom: "pressureUnit",
67995
+ unit: "hPa",
68055
67996
  deadband: .5
68056
67997
  },
68057
67998
  windSpeed: {
68058
67999
  kind: "gauge",
68059
- unitFrom: "windSpeedUnit",
68000
+ unit: "m/s",
68060
68001
  deadband: .5
68061
68002
  }
68062
68003
  } }
@@ -68988,6 +68929,389 @@ function runningFromDishwasherOperation(operation, program, previous, fetchedAt,
68988
68929
  if (operation === "Pause") return nativeSlice(false, "Pause", previous, transitionAtMs, fetchedAt, null);
68989
68930
  return nativeSlice(false, operation, previous, transitionAtMs, fetchedAt, null);
68990
68931
  }
68932
+ /**
68933
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
68934
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
68935
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
68936
+ * decimals). K derives from `°C = K − 273.15`.
68937
+ */
68938
+ var UNIT_TABLE = {
68939
+ "°C": {
68940
+ dimension: "temperature",
68941
+ factor: 1,
68942
+ offset: 0
68943
+ },
68944
+ "°F": {
68945
+ dimension: "temperature",
68946
+ factor: 5 / 9,
68947
+ offset: -160 / 9
68948
+ },
68949
+ K: {
68950
+ dimension: "temperature",
68951
+ factor: 1,
68952
+ offset: -273.15
68953
+ },
68954
+ hPa: {
68955
+ dimension: "pressure",
68956
+ factor: 1,
68957
+ offset: 0
68958
+ },
68959
+ kPa: {
68960
+ dimension: "pressure",
68961
+ factor: 10,
68962
+ offset: 0
68963
+ },
68964
+ Pa: {
68965
+ dimension: "pressure",
68966
+ factor: .01,
68967
+ offset: 0
68968
+ },
68969
+ mbar: {
68970
+ dimension: "pressure",
68971
+ factor: 1,
68972
+ offset: 0
68973
+ },
68974
+ bar: {
68975
+ dimension: "pressure",
68976
+ factor: 1e3,
68977
+ offset: 0
68978
+ },
68979
+ inHg: {
68980
+ dimension: "pressure",
68981
+ factor: 33.8639,
68982
+ offset: 0
68983
+ },
68984
+ mmHg: {
68985
+ dimension: "pressure",
68986
+ factor: 1.33322,
68987
+ offset: 0
68988
+ },
68989
+ psi: {
68990
+ dimension: "pressure",
68991
+ factor: 68.9476,
68992
+ offset: 0
68993
+ },
68994
+ "m/s": {
68995
+ dimension: "speed",
68996
+ factor: 1,
68997
+ offset: 0
68998
+ },
68999
+ "km/h": {
69000
+ dimension: "speed",
69001
+ factor: 1 / 3.6,
69002
+ offset: 0
69003
+ },
69004
+ mph: {
69005
+ dimension: "speed",
69006
+ factor: .44704,
69007
+ offset: 0
69008
+ },
69009
+ kn: {
69010
+ dimension: "speed",
69011
+ factor: .514444,
69012
+ offset: 0
69013
+ },
69014
+ "mm/h": {
69015
+ dimension: "precipitation-rate",
69016
+ factor: 1,
69017
+ offset: 0
69018
+ },
69019
+ "in/h": {
69020
+ dimension: "precipitation-rate",
69021
+ factor: 25.4,
69022
+ offset: 0
69023
+ },
69024
+ m: {
69025
+ dimension: "length",
69026
+ factor: 1,
69027
+ offset: 0
69028
+ },
69029
+ mm: {
69030
+ dimension: "length",
69031
+ factor: .001,
69032
+ offset: 0
69033
+ },
69034
+ cm: {
69035
+ dimension: "length",
69036
+ factor: .01,
69037
+ offset: 0
69038
+ },
69039
+ km: {
69040
+ dimension: "length",
69041
+ factor: 1e3,
69042
+ offset: 0
69043
+ },
69044
+ in: {
69045
+ dimension: "length",
69046
+ factor: .0254,
69047
+ offset: 0
69048
+ },
69049
+ ft: {
69050
+ dimension: "length",
69051
+ factor: .3048,
69052
+ offset: 0
69053
+ },
69054
+ mi: {
69055
+ dimension: "length",
69056
+ factor: 1609.344,
69057
+ offset: 0
69058
+ },
69059
+ "m²": {
69060
+ dimension: "area",
69061
+ factor: 1,
69062
+ offset: 0
69063
+ },
69064
+ lx: {
69065
+ dimension: "illuminance",
69066
+ factor: 1,
69067
+ offset: 0
69068
+ },
69069
+ "W/m²": {
69070
+ dimension: "irradiance",
69071
+ factor: 1,
69072
+ offset: 0
69073
+ },
69074
+ W: {
69075
+ dimension: "power",
69076
+ factor: 1,
69077
+ offset: 0
69078
+ },
69079
+ kW: {
69080
+ dimension: "power",
69081
+ factor: 1e3,
69082
+ offset: 0
69083
+ },
69084
+ VA: {
69085
+ dimension: "apparent-power",
69086
+ factor: 1,
69087
+ offset: 0
69088
+ },
69089
+ Wh: {
69090
+ dimension: "energy",
69091
+ factor: 1,
69092
+ offset: 0
69093
+ },
69094
+ kWh: {
69095
+ dimension: "energy",
69096
+ factor: 1e3,
69097
+ offset: 0
69098
+ },
69099
+ MWh: {
69100
+ dimension: "energy",
69101
+ factor: 1e6,
69102
+ offset: 0
69103
+ },
69104
+ V: {
69105
+ dimension: "voltage",
69106
+ factor: 1,
69107
+ offset: 0
69108
+ },
69109
+ mV: {
69110
+ dimension: "voltage",
69111
+ factor: .001,
69112
+ offset: 0
69113
+ },
69114
+ A: {
69115
+ dimension: "current",
69116
+ factor: 1,
69117
+ offset: 0
69118
+ },
69119
+ mA: {
69120
+ dimension: "current",
69121
+ factor: .001,
69122
+ offset: 0
69123
+ },
69124
+ ppm: {
69125
+ dimension: "concentration-volume",
69126
+ factor: 1,
69127
+ offset: 0
69128
+ },
69129
+ ppb: {
69130
+ dimension: "concentration-volume",
69131
+ factor: .001,
69132
+ offset: 0
69133
+ },
69134
+ "µg/m³": {
69135
+ dimension: "concentration-mass",
69136
+ factor: 1,
69137
+ offset: 0
69138
+ },
69139
+ "mg/m³": {
69140
+ dimension: "concentration-mass",
69141
+ factor: 1e3,
69142
+ offset: 0
69143
+ },
69144
+ kg: {
69145
+ dimension: "mass",
69146
+ factor: 1,
69147
+ offset: 0
69148
+ },
69149
+ g: {
69150
+ dimension: "mass",
69151
+ factor: .001,
69152
+ offset: 0
69153
+ },
69154
+ lb: {
69155
+ dimension: "mass",
69156
+ factor: .453592,
69157
+ offset: 0
69158
+ },
69159
+ oz: {
69160
+ dimension: "mass",
69161
+ factor: .0283495,
69162
+ offset: 0
69163
+ },
69164
+ "%": {
69165
+ dimension: "percentage",
69166
+ factor: 1,
69167
+ offset: 0
69168
+ },
69169
+ L: {
69170
+ dimension: "volume",
69171
+ factor: 1,
69172
+ offset: 0
69173
+ },
69174
+ mL: {
69175
+ dimension: "volume",
69176
+ factor: .001,
69177
+ offset: 0
69178
+ },
69179
+ gal: {
69180
+ dimension: "volume",
69181
+ factor: 3.785411784,
69182
+ offset: 0
69183
+ },
69184
+ s: {
69185
+ dimension: "duration",
69186
+ factor: 1,
69187
+ offset: 0
69188
+ },
69189
+ ms: {
69190
+ dimension: "duration",
69191
+ factor: .001,
69192
+ offset: 0
69193
+ },
69194
+ min: {
69195
+ dimension: "duration",
69196
+ factor: 60,
69197
+ offset: 0
69198
+ },
69199
+ h: {
69200
+ dimension: "duration",
69201
+ factor: 3600,
69202
+ offset: 0
69203
+ },
69204
+ d: {
69205
+ dimension: "duration",
69206
+ factor: 86400,
69207
+ offset: 0
69208
+ },
69209
+ B: {
69210
+ dimension: "information",
69211
+ factor: 1,
69212
+ offset: 0
69213
+ },
69214
+ KiB: {
69215
+ dimension: "information",
69216
+ factor: 1024,
69217
+ offset: 0
69218
+ },
69219
+ MiB: {
69220
+ dimension: "information",
69221
+ factor: 1024 ** 2,
69222
+ offset: 0
69223
+ },
69224
+ GiB: {
69225
+ dimension: "information",
69226
+ factor: 1024 ** 3,
69227
+ offset: 0
69228
+ },
69229
+ TiB: {
69230
+ dimension: "information",
69231
+ factor: 1024 ** 4,
69232
+ offset: 0
69233
+ },
69234
+ kB: {
69235
+ dimension: "information",
69236
+ factor: 1e3,
69237
+ offset: 0
69238
+ },
69239
+ MB: {
69240
+ dimension: "information",
69241
+ factor: 1e6,
69242
+ offset: 0
69243
+ },
69244
+ GB: {
69245
+ dimension: "information",
69246
+ factor: 1e9,
69247
+ offset: 0
69248
+ },
69249
+ "°": {
69250
+ dimension: "angle",
69251
+ factor: 1,
69252
+ offset: 0
69253
+ },
69254
+ dBFS: {
69255
+ dimension: "sound-level",
69256
+ factor: 1,
69257
+ offset: 0
69258
+ },
69259
+ dBm: {
69260
+ dimension: "signal-level",
69261
+ factor: 1,
69262
+ offset: 0
69263
+ },
69264
+ mired: {
69265
+ dimension: "colour-temperature",
69266
+ factor: 1,
69267
+ offset: 0
69268
+ },
69269
+ kbps: {
69270
+ dimension: "bitrate",
69271
+ factor: 1,
69272
+ offset: 0
69273
+ },
69274
+ Mbps: {
69275
+ dimension: "bitrate",
69276
+ factor: 1e3,
69277
+ offset: 0
69278
+ },
69279
+ fps: {
69280
+ dimension: "rate",
69281
+ factor: 1,
69282
+ offset: 0
69283
+ }
69284
+ };
69285
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
69286
+ function specFor(unit) {
69287
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
69288
+ }
69289
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
69290
+ function unitDimension(unit) {
69291
+ return specFor(unit)?.dimension;
69292
+ }
69293
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
69294
+ function toCanonical(value, spec) {
69295
+ return value * spec.factor + spec.offset;
69296
+ }
69297
+ /** Canonical-unit value → the unit described by `spec`. */
69298
+ function fromCanonical(canonical, spec) {
69299
+ return (canonical - spec.offset) / spec.factor;
69300
+ }
69301
+ /**
69302
+ * Graceful variant for the render path: returns the converted value, or
69303
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
69304
+ * Never throws — a render caller falls back to the raw value + source unit.
69305
+ */
69306
+ function tryConvertUnit(value, from, to) {
69307
+ if (from === to) return value;
69308
+ const fromSpec = specFor(from);
69309
+ if (!fromSpec) return void 0;
69310
+ const toSpec = specFor(to);
69311
+ if (!toSpec) return void 0;
69312
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
69313
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
69314
+ }
68991
69315
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
68992
69316
  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;
68993
69317
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
@@ -83657,7 +83981,6 @@ var SENSOR_CAP_VOCABULARY = {
83657
83981
  field: "value",
83658
83982
  quantity: t("the reading", "il valore"),
83659
83983
  label: t("Reading", "Valore"),
83660
- unitField: "unit",
83661
83984
  icon: "gauge"
83662
83985
  }]
83663
83986
  },
@@ -83793,7 +84116,7 @@ var SENSOR_CAP_VOCABULARY = {
83793
84116
  field: "temperature",
83794
84117
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83795
84118
  label: t("Temperature", "Temperatura"),
83796
- unitField: "temperatureUnit",
84119
+ unit: "°C",
83797
84120
  icon: "thermometer"
83798
84121
  },
83799
84122
  {
@@ -83809,7 +84132,7 @@ var SENSOR_CAP_VOCABULARY = {
83809
84132
  field: "windSpeed",
83810
84133
  quantity: t("the wind speed", "la velocità del vento"),
83811
84134
  label: t("Wind", "Vento"),
83812
- unitField: "windSpeedUnit",
84135
+ unit: "m/s",
83813
84136
  icon: "wind"
83814
84137
  },
83815
84138
  {
@@ -83817,7 +84140,7 @@ var SENSOR_CAP_VOCABULARY = {
83817
84140
  field: "pressure",
83818
84141
  quantity: t("the air pressure", "la pressione atmosferica"),
83819
84142
  label: t("Pressure", "Pressione"),
83820
- unitField: "pressureUnit",
84143
+ unit: "hPa",
83821
84144
  icon: "gauge"
83822
84145
  }
83823
84146
  ]
@@ -86954,6 +87277,11 @@ var PulseOverrideDraftSchema = object({
86954
87277
  targetId: string().min(1),
86955
87278
  enabled: boolean(),
86956
87279
  debounceMs: number().nonnegative(),
87280
+ edges: array(_enum([
87281
+ "rising",
87282
+ "button",
87283
+ "native-event"
87284
+ ])).min(1).optional(),
86957
87285
  sources: array(OverrideSourceSchema).min(1)
86958
87286
  });
86959
87287
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -88177,6 +88505,96 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
88177
88505
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
88178
88506
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
88179
88507
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
88508
+ /**
88509
+ * Convert an upstream number into the stored base of its quantity (D944 §4).
88510
+ * An unknown unit or a unit from another dimension is a refusal: the caller
88511
+ * logs it and publishes null (D393). This function never guesses.
88512
+ */
88513
+ var DIMENSION_QUANTITY = {
88514
+ temperature: "temperature",
88515
+ pressure: "pressure",
88516
+ speed: "speed",
88517
+ "precipitation-rate": "precipitation-rate",
88518
+ length: "length",
88519
+ area: "area",
88520
+ illuminance: "illuminance",
88521
+ irradiance: "irradiance",
88522
+ power: "power",
88523
+ "apparent-power": "apparent-power",
88524
+ energy: "energy",
88525
+ voltage: "voltage",
88526
+ current: "current",
88527
+ "concentration-volume": "concentration-volume",
88528
+ "concentration-mass": "concentration-mass",
88529
+ mass: "mass",
88530
+ percentage: "percentage",
88531
+ volume: "volume",
88532
+ duration: "duration",
88533
+ information: "information",
88534
+ angle: "angle",
88535
+ "sound-level": "sound-level",
88536
+ "signal-level": "signal-level",
88537
+ "colour-temperature": "colour-temperature",
88538
+ bitrate: "bitrate",
88539
+ rate: "rate"
88540
+ };
88541
+ /**
88542
+ * Quantity implied by a unit symbol. `ppb` and `ms` are the two symbols
88543
+ * whose stored base is not the dimension's canonical unit. `mm` is length;
88544
+ * precipitation is chosen by the caller (a device class), not by the symbol.
88545
+ */
88546
+ function quantityFromUnit(rawUnit) {
88547
+ const unit = normalizeUnit(rawUnit);
88548
+ if (unit === void 0) return void 0;
88549
+ if (unit === "ppb") return "concentration-ppb";
88550
+ if (unit === "ms") return "duration-ms";
88551
+ const dimension = unitDimension(unit);
88552
+ if (dimension === void 0) return void 0;
88553
+ return DIMENSION_QUANTITY[dimension];
88554
+ }
88555
+ function ingestReading(input) {
88556
+ if (!Number.isFinite(input.value)) return {
88557
+ ok: false,
88558
+ reason: "non-finite"
88559
+ };
88560
+ if (input.quantity === "device") return {
88561
+ ok: false,
88562
+ reason: "unknown-unit"
88563
+ };
88564
+ const raw = input.rawUnit?.trim();
88565
+ if (raw === void 0 || raw.length === 0) return {
88566
+ ok: true,
88567
+ value: input.value
88568
+ };
88569
+ const unit = normalizeUnit(raw);
88570
+ if (unit === void 0) return {
88571
+ ok: false,
88572
+ reason: "unknown-unit"
88573
+ };
88574
+ const base = baseUnitOf(input.quantity);
88575
+ if (base === void 0) {
88576
+ if (unitDimension(unit) !== void 0) return {
88577
+ ok: false,
88578
+ reason: "cross-dimension"
88579
+ };
88580
+ return {
88581
+ ok: false,
88582
+ reason: "unknown-unit"
88583
+ };
88584
+ }
88585
+ const converted = tryConvertUnit(input.value, unit, base);
88586
+ if (converted === void 0) return unitDimension(unit) === void 0 ? {
88587
+ ok: false,
88588
+ reason: "unknown-unit"
88589
+ } : {
88590
+ ok: false,
88591
+ reason: "cross-dimension"
88592
+ };
88593
+ return {
88594
+ ok: true,
88595
+ value: converted
88596
+ };
88597
+ }
88180
88598
  var MB = 1024 * 1024;
88181
88599
  1024 * MB, 3072 * MB;
88182
88600
  //#endregion
@@ -88318,6 +88736,12 @@ Object.defineProperty(exports, "heatingValveCapability", {
88318
88736
  return heatingValveCapability;
88319
88737
  }
88320
88738
  });
88739
+ Object.defineProperty(exports, "ingestReading", {
88740
+ enumerable: true,
88741
+ get: function() {
88742
+ return ingestReading;
88743
+ }
88744
+ });
88321
88745
  Object.defineProperty(exports, "isSecretMarker", {
88322
88746
  enumerable: true,
88323
88747
  get: function() {
@@ -88360,6 +88784,12 @@ Object.defineProperty(exports, "powerMeterCapability", {
88360
88784
  return powerMeterCapability;
88361
88785
  }
88362
88786
  });
88787
+ Object.defineProperty(exports, "quantityFromUnit", {
88788
+ enumerable: true,
88789
+ get: function() {
88790
+ return quantityFromUnit;
88791
+ }
88792
+ });
88363
88793
  Object.defineProperty(exports, "record", {
88364
88794
  enumerable: true,
88365
88795
  get: function() {