@camstack/addon-provider-homeassistant 1.2.625 → 1.2.626

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.
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as mediaPlayerCapability, A as calendarSourceCapability, B as deviceAdminLinkCapability, Bt as discriminatedUnion, C as bestLocationMatch, Ct as vibrationCapability, D as buildAddonRouteProvider, Dt as DeviceRole, E as brokerCapability, Et as DeviceFeature, Ft as entryBody, G as fanControlCapability, Gt as string, Ht as number, J as humidifierCapability, Jt as url, K as floodCapability, Kt as union, L as contactCapability, Lt as _enum, M as climateControlCapability, Mt as createEvent, N as colorCapability, Nt as defineEntryKind, O as buildDeviceAdminUrl, Ot as DeviceType, P as connectivityCapability, Pt as defineRecordKinds, Q as lockControlCapability, R as controlCapability, Rt as array, S as batteryCapability, St as valveCapability, T as brightnessCapability, Tt as weatherCapability, U as enumSensorCapability, Ut as object, V as deviceAdoptionCapability, Vt as literal, W as eventEmitterCapability, Wt as record, X as imageCapability, Y as humiditySensorCapability, Yt as EventCategory, Z as lawnMowerControlCapability, _ as addonRoutesCapability, _t as switchCapability, b as ambientLightSensorCapability, bt as updateCapability, ct as prepareNotification, dt as runningFromLawnMower, et as motionCapability, ft as runningFromMediaPlayer, g as accessoriesCapability, gt as smokeCapability, h as TargetSchema, ht as scriptRunnerCapability, it as numericSensorCapability, j as carbonMonoxideCapability, jt as RecordSecretUnavailableError, k as buttonCapability, kt as errMsg, lt as presenceCapability, m as RUNNING_STATE_COLD, mt as runningStateCapability, n as BaseDevice, nt as notificationOutputCapability, ot as parseAdminBase, pt as runningFromVacuumState, q as gasCapability, qt as unknown, r as BaseDeviceProvider, rt as notifierCapability, st as powerMeterCapability, tt as normalizeUnit, u as EnumSensorDateTimeFormatSchema, ut as pressureSensorCapability, v as airQualitySensorCapability, vt as tamperCapability, w as binaryCapability, wt as waterHeaterCapability, x as automationControlCapability, xt as vacuumControlCapability, y as alarmPanelCapability, yt as temperatureSensorCapability, z as coverCapability, zt as boolean } from "./dist-DvZEnx9C.mjs";
1
+ import { $ as lockControlCapability, $t as unknown, A as calendarSourceCapability, At as DeviceFeature, B as deviceAdminLinkCapability, Bt as entryBody, C as bestLocationMatch, Ct as tryConvertUnit, D as buildAddonRouteProvider, Dt as vibrationCapability, E as brokerCapability, Et as valveCapability, Ft as RecordSecretUnavailableError, G as fanControlCapability, Gt as boolean, It as baseUnitOf, J as humidifierCapability, Jt as number, K as floodCapability, Kt as discriminatedUnion, L as contactCapability, Lt as createEvent, M as climateControlCapability, Mt as DeviceType, N as colorCapability, Nt as errMsg, O as buildDeviceAdminUrl, Ot as waterHeaterCapability, P as connectivityCapability, Q as lawnMowerControlCapability, Qt as union, R as controlCapability, Rt as defineEntryKind, S as batteryCapability, St as tryConvertDelta, T as brightnessCapability, Tt as vacuumControlCapability, U as enumSensorCapability, Ut as _enum, V as deviceAdoptionCapability, W as eventEmitterCapability, Wt as array, X as imageCapability, Xt as record, Y as humiditySensorCapability, Yt as object, Z as ingestReading, Zt as string, _ as addonRoutesCapability, _t as scriptRunnerCapability, at as numericSensorCapability, b as ambientLightSensorCapability, bt as tamperCapability, ct as powerMeterCapability, dt as pressureSensorCapability, en as url, et as mediaPlayerCapability, ft as quantityFromUnit, g as accessoriesCapability, gt as runningStateCapability, h as TargetSchema, ht as runningFromVacuumState, it as notifierCapability, j as carbonMonoxideCapability, jt as DeviceRole, k as buttonCapability, kt as weatherCapability, lt as prepareNotification, m as RUNNING_STATE_COLD, mt as runningFromMediaPlayer, n as BaseDevice, nt as normalizeUnit, pt as runningFromLawnMower, q as gasCapability, qt as literal, r as BaseDeviceProvider, rt as notificationOutputCapability, st as parseAdminBase, tn as EventCategory, tt as motionCapability, u as EnumSensorDateTimeFormatSchema, ut as presenceCapability, v as airQualitySensorCapability, vt as smokeCapability, w as binaryCapability, wt as updateCapability, x as automationControlCapability, xt as temperatureSensorCapability, y as alarmPanelCapability, yt as switchCapability, z as coverCapability, zt as defineRecordKinds } from "./dist-B7nW5y58.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -765,6 +765,30 @@ var HaSwitchDevice = class extends HaBaseChildDevice {
765
765
  }
766
766
  };
767
767
  //#endregion
768
+ //#region src/ha-reading.ts
769
+ /**
770
+ * Convert one sample into the quantity's base unit. A refusal publishes
771
+ * nothing (the caller stores null) and is logged with the device id —
772
+ * an unknown unit is never guessed (D393, D944).
773
+ */
774
+ function takeReading(logger, deviceId, value, rawUnit, quantity) {
775
+ const result = ingestReading({
776
+ value,
777
+ rawUnit,
778
+ quantity
779
+ });
780
+ if (result.ok) return result.value;
781
+ logger.warn("numeric reading refused", {
782
+ tags: { deviceId },
783
+ meta: {
784
+ quantity,
785
+ unit: rawUnit ?? "",
786
+ reason: result.reason
787
+ }
788
+ });
789
+ return null;
790
+ }
791
+ //#endregion
768
792
  //#region src/devices/ha-numeric-sensor-device.ts
769
793
  var SUPPORTED_CAP_NAMES$1 = [
770
794
  "temperature-sensor",
@@ -781,6 +805,13 @@ var VALUE_FIELD = {
781
805
  "pressure-sensor": "hpa",
782
806
  "numeric-sensor": "value"
783
807
  };
808
+ /** Typed sensors already name their quantity. The generic sensor learns it. */
809
+ var FIXED_QUANTITY = {
810
+ "temperature-sensor": "temperature",
811
+ "humidity-sensor": "humidity",
812
+ "ambient-light-sensor": "illuminance",
813
+ "pressure-sensor": "pressure"
814
+ };
784
815
  /**
785
816
  * Generic HA numeric-sensor child device. One class hosts every
786
817
  * single-metric numeric sensor cap (Phase B B2 + the B3 fallback)
@@ -818,15 +849,14 @@ var HaNumericSensorDevice = class extends HaBaseChildDevice {
818
849
  this.hasStringFallback = capName === "numeric-sensor";
819
850
  this.registerCap(capName);
820
851
  if (this.hasStringFallback) this.registerStringFallbackCap();
821
- this.runtimeState.setCapState(this.capName, this.buildSlice(0, 0, void 0, void 0));
852
+ this.runtimeState.setCapState(this.capName, this.buildSlice(0, 0, void 0));
822
853
  }
823
854
  handleStatePush(state) {
824
855
  const value = Number(state.state);
825
856
  const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
826
857
  if (Number.isFinite(value)) {
827
858
  const unitRaw = typeof state.attributes["unit_of_measurement"] === "string" ? state.attributes["unit_of_measurement"] : void 0;
828
- const precisionRaw = typeof state.attributes["suggested_display_precision"] === "number" ? state.attributes["suggested_display_precision"] : void 0;
829
- this.runtimeState.setCapState(this.capName, this.buildSlice(value, lastFetchedAt, unitRaw, precisionRaw));
859
+ this.runtimeState.setCapState(this.capName, this.buildSlice(value, lastFetchedAt, unitRaw));
830
860
  return;
831
861
  }
832
862
  const rawState = state.state;
@@ -838,24 +868,43 @@ var HaNumericSensorDevice = class extends HaBaseChildDevice {
838
868
  this.runtimeState.setCapState("enum-sensor", enumSlice);
839
869
  }
840
870
  }
841
- buildSlice(value, lastFetchedAt, unitRaw, precisionRaw) {
842
- if (this.capName === "numeric-sensor") {
843
- const unit = normalizeUnit(unitRaw);
844
- return {
845
- value,
846
- lastFetchedAt,
847
- ...unit !== void 0 ? { unit } : {},
848
- ...precisionRaw !== void 0 ? { precision: precisionRaw } : {}
849
- };
850
- }
851
- const baseSlice = {
852
- [this.valueKey]: value,
871
+ buildSlice(value, lastFetchedAt, unitRaw) {
872
+ const quantity = this.resolveQuantity(unitRaw);
873
+ const stored = quantity === null ? null : takeReading(this.deviceLogger, this.id, value, unitRaw, quantity);
874
+ if (this.capName === "numeric-sensor") return {
875
+ value: stored,
853
876
  lastFetchedAt
854
877
  };
855
- const unit = normalizeUnit(unitRaw);
856
- if (unit !== void 0) baseSlice["unit"] = unit;
857
- if (precisionRaw !== void 0) baseSlice["precision"] = precisionRaw;
858
- return baseSlice;
878
+ return {
879
+ [this.valueKey]: stored,
880
+ lastFetchedAt
881
+ };
882
+ }
883
+ /**
884
+ * Typed sensors already name their quantity. A generic numeric sensor
885
+ * learns it once from the unit and stores it on the device, so later
886
+ * samples convert against the same quantity.
887
+ */
888
+ resolveQuantity(unitRaw) {
889
+ const fixed = FIXED_QUANTITY[this.capName];
890
+ if (fixed !== void 0) return fixed;
891
+ const stored = this.sourceInfo.quantity;
892
+ if (stored !== void 0 && stored !== "device") return stored;
893
+ if (unitRaw === void 0) return "none";
894
+ const learned = quantityFromUnit(unitRaw);
895
+ if (learned === void 0 || learned === "device") {
896
+ this.deviceLogger.warn("numeric reading refused", {
897
+ tags: { deviceId: this.id },
898
+ meta: {
899
+ quantity: "device",
900
+ unit: unitRaw,
901
+ reason: "unknown-unit"
902
+ }
903
+ });
904
+ return null;
905
+ }
906
+ this.updateSourceInfo({ quantity: learned });
907
+ return learned;
859
908
  }
860
909
  /**
861
910
  * Register an `enum-sensor` cap backed by the same `runtimeState` so that
@@ -1614,6 +1663,22 @@ var HaClimateDevice = class extends HaBaseChildDevice {
1614
1663
  if (state.state === "unavailable" || state.state === "unknown") return;
1615
1664
  this.swingModes = readStringArray$4(state.attributes, "swing_modes");
1616
1665
  this.swingHorizontalModes = readStringArray$4(state.attributes, "swing_horizontal_modes");
1666
+ const unit = readStringAttr$6(state.attributes, "temperature_unit");
1667
+ if (unit !== null) {
1668
+ const probe = ingestReading({
1669
+ value: 0,
1670
+ rawUnit: unit,
1671
+ quantity: "temperature"
1672
+ });
1673
+ if (!probe.ok) this.deviceLogger.warn("numeric reading refused", {
1674
+ tags: { deviceId: this.id },
1675
+ meta: {
1676
+ quantity: "temperature",
1677
+ unit,
1678
+ reason: probe.reason
1679
+ }
1680
+ });
1681
+ }
1617
1682
  this.runtimeState.setCapState("climate-control", climateSliceFromState(state));
1618
1683
  }
1619
1684
  registerClimateCap() {
@@ -1672,6 +1737,16 @@ function climateSliceFromState(state) {
1672
1737
  const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
1673
1738
  const attrs = state.attributes;
1674
1739
  const mode = sanitizeHvacMode(state.state);
1740
+ const tempUnit = readStringAttr$6(attrs, "temperature_unit") ?? void 0;
1741
+ const celsius = (raw) => {
1742
+ if (raw === null) return null;
1743
+ const result = ingestReading({
1744
+ value: raw,
1745
+ rawUnit: tempUnit,
1746
+ quantity: "temperature"
1747
+ });
1748
+ return result.ok ? result.value : null;
1749
+ };
1675
1750
  const availableModes = readStringArray$4(attrs, "hvac_modes").map(sanitizeHvacMode).filter((m) => m !== null);
1676
1751
  const fanMode = readStringAttr$6(attrs, "fan_mode") ?? "";
1677
1752
  const availableFanModes = readStringArray$4(attrs, "fan_modes");
@@ -1684,16 +1759,16 @@ function climateSliceFromState(state) {
1684
1759
  availableFanModes,
1685
1760
  preset,
1686
1761
  availablePresets,
1687
- target: readNumberAttr$5(attrs, "temperature"),
1688
- targetHigh: readNumberAttr$5(attrs, "target_temp_high"),
1689
- targetLow: readNumberAttr$5(attrs, "target_temp_low"),
1762
+ target: celsius(readNumberAttr$5(attrs, "temperature")),
1763
+ targetHigh: celsius(readNumberAttr$5(attrs, "target_temp_high")),
1764
+ targetLow: celsius(readNumberAttr$5(attrs, "target_temp_low")),
1690
1765
  targetHumidity: readNumberAttr$5(attrs, "target_humidity"),
1691
- currentTemp: readNumberAttr$5(attrs, "current_temperature"),
1766
+ currentTemp: celsius(readNumberAttr$5(attrs, "current_temperature")),
1692
1767
  currentHumidity: readNumberAttr$5(attrs, "current_humidity"),
1693
1768
  ...readSwing(attrs),
1694
- minTemp: readNumberAttr$5(attrs, "min_temp"),
1695
- maxTemp: readNumberAttr$5(attrs, "max_temp"),
1696
- tempStep: readNumberAttr$5(attrs, "target_temp_step"),
1769
+ minTemp: celsius(readNumberAttr$5(attrs, "min_temp")),
1770
+ maxTemp: celsius(readNumberAttr$5(attrs, "max_temp")),
1771
+ tempStep: celsius(readNumberAttr$5(attrs, "target_temp_step")),
1697
1772
  lastFetchedAt
1698
1773
  };
1699
1774
  }
@@ -2516,10 +2591,10 @@ function readNumberAttr$1(attrs, key) {
2516
2591
  * - number / input_number → kind='numeric', service set_value({value})
2517
2592
  * - select / input_select → kind='select', service select_option({option})
2518
2593
  * - text / input_text → kind='text', service set_value({value})
2519
- * - date → kind='datetime', format='date', set_value({date})
2520
- * - time → kind='datetime', format='time', set_value({time})
2521
- * - datetime → kind='datetime', format='datetime', set_value({datetime})
2522
- * - input_datetime → kind='datetime', format derived from
2594
+ * - date → kind='datetime', shape date, set_value({date})
2595
+ * - time → kind='datetime', shape time, set_value({time})
2596
+ * - datetime → kind='datetime', shape datetime, set_value({datetime})
2597
+ * - input_datetime → kind='datetime', shape derived from
2523
2598
  * has_date/has_time, service set_datetime({datetime})
2524
2599
  *
2525
2600
  * The datetime `value` is kept as the HA-native string (date.* → 'YYYY-MM-DD',
@@ -2530,6 +2605,8 @@ function readNumberAttr$1(attrs, key) {
2530
2605
  var HaControlDevice = class extends HaBaseChildDevice {
2531
2606
  kind;
2532
2607
  haDomain;
2608
+ /** Unit the upstream entity speaks. Writes convert the base value back to it. */
2609
+ upstreamUnit;
2533
2610
  constructor(ctx) {
2534
2611
  const persisted = ctx.persistedConfig ?? {};
2535
2612
  super(ctx, persisted.role !== void 0 ? { role: persisted.role } : {});
@@ -2537,18 +2614,30 @@ var HaControlDevice = class extends HaBaseChildDevice {
2537
2614
  this.kind = roleToKind(persisted.role);
2538
2615
  const seedOptions = Array.isArray(persisted.options) ? persisted.options.filter((x) => typeof x === "string") : [];
2539
2616
  this.registerControlCap();
2617
+ if (this.kind === "datetime") this.updateSourceInfo({ dateTimeShape: this.datetimeFormat(void 0) });
2540
2618
  this.runtimeState.setCapState("control", {
2541
2619
  kind: this.kind,
2542
2620
  value: this.kind === "numeric" ? 0 : "",
2543
2621
  options: seedOptions,
2544
- lastChangedAt: 0,
2545
- ...this.kind === "datetime" ? { format: this.datetimeFormat(void 0) } : {}
2622
+ lastChangedAt: 0
2546
2623
  });
2547
2624
  }
2548
2625
  handleStatePush(state) {
2549
2626
  if (state.state === "unavailable" || state.state === "unknown") return;
2550
2627
  const lastChangedAt = Date.parse(state.last_changed) || Date.now();
2551
- const slice = decodeControlSlice(this.kind, state, lastChangedAt, () => this.datetimeFormat(state.attributes));
2628
+ const slice = decodeControlSlice(this.kind, state, lastChangedAt, {
2629
+ logger: this.deviceLogger,
2630
+ deviceId: this.id,
2631
+ quantity: this.sourceInfo.quantity,
2632
+ note: (quantity, unit) => {
2633
+ this.upstreamUnit = unit;
2634
+ if (this.sourceInfo.quantity !== quantity) this.updateSourceInfo({ quantity });
2635
+ },
2636
+ noteShape: (shape) => {
2637
+ if (this.sourceInfo.dateTimeShape !== shape) this.updateSourceInfo({ dateTimeShape: shape });
2638
+ },
2639
+ shape: () => this.datetimeFormat(state.attributes)
2640
+ });
2552
2641
  if (slice !== null) this.runtimeState.setCapState("control", slice);
2553
2642
  }
2554
2643
  registerControlCap() {
@@ -2558,8 +2647,7 @@ var HaControlDevice = class extends HaBaseChildDevice {
2558
2647
  kind: this.kind,
2559
2648
  value: this.kind === "numeric" ? 0 : "",
2560
2649
  options: [],
2561
- lastChangedAt: 0,
2562
- ...this.kind === "datetime" ? { format: this.datetimeFormat(void 0) } : {}
2650
+ lastChangedAt: 0
2563
2651
  };
2564
2652
  },
2565
2653
  setValue: async ({ control }) => {
@@ -2589,11 +2677,32 @@ var HaControlDevice = class extends HaBaseChildDevice {
2589
2677
  default: return "datetime";
2590
2678
  }
2591
2679
  }
2680
+ /** Base-unit value → the unit the upstream entity still speaks. */
2681
+ toUpstream(value) {
2682
+ const quantity = this.sourceInfo.quantity;
2683
+ const base = quantity === void 0 ? void 0 : baseUnitOf(quantity);
2684
+ const upstream = this.upstreamUnit;
2685
+ if (base === void 0 || upstream === void 0 || upstream === base) return value;
2686
+ const converted = tryConvertUnit(value, base, upstream);
2687
+ if (converted !== void 0) return converted;
2688
+ this.deviceLogger.warn("numeric reading refused", {
2689
+ tags: { deviceId: this.id },
2690
+ meta: {
2691
+ quantity: quantity ?? "device",
2692
+ unit: upstream,
2693
+ reason: "unknown-unit"
2694
+ }
2695
+ });
2696
+ return null;
2697
+ }
2592
2698
  async dispatchSet(control) {
2593
2699
  switch (control.kind) {
2594
- case "numeric":
2595
- await this.callHaService(this.haDomain, "set_value", { value: control.value });
2700
+ case "numeric": {
2701
+ const outbound = this.toUpstream(control.value);
2702
+ if (outbound === null) return;
2703
+ await this.callHaService(this.haDomain, "set_value", { value: outbound });
2596
2704
  return;
2705
+ }
2597
2706
  case "select":
2598
2707
  await this.callHaService(this.haDomain, "select_option", { option: control.value });
2599
2708
  return;
@@ -2642,47 +2751,9 @@ function roleToKind(role) {
2642
2751
  default: return "text";
2643
2752
  }
2644
2753
  }
2645
- /**
2646
- * Derive display precision from a numeric step value when HA does not provide
2647
- * `suggested_display_precision` explicitly. Returns the number of decimal
2648
- * places implied by the step granularity (e.g. 0.5 → 1, 0.01 → 2, 1 → 0).
2649
- * Returns `undefined` when step is absent or is a whole number (let the UI
2650
- * apply its own auto-formatting for integer steps).
2651
- */
2652
- function precisionFromStep(step) {
2653
- if (step === void 0 || step <= 0) return void 0;
2654
- if (Number.isInteger(step)) return 0;
2655
- const str = step.toString();
2656
- const dotIndex = str.indexOf(".");
2657
- if (dotIndex === -1) return 0;
2658
- return str.length - dotIndex - 1;
2659
- }
2660
- function decodeControlSlice(kind, state, lastChangedAt, resolveFormat) {
2754
+ function decodeControlSlice(kind, state, lastChangedAt, decode) {
2661
2755
  switch (kind) {
2662
- case "numeric": {
2663
- const v = Number(state.state);
2664
- if (!Number.isFinite(v)) return null;
2665
- const rawUnit = state.attributes["unit_of_measurement"];
2666
- const rawMin = state.attributes["min"];
2667
- const rawMax = state.attributes["max"];
2668
- const rawStep = state.attributes["step"];
2669
- const rawPrecision = state.attributes["suggested_display_precision"];
2670
- const unit = typeof rawUnit === "string" && rawUnit.length > 0 ? rawUnit : void 0;
2671
- const min = typeof rawMin === "number" && Number.isFinite(rawMin) ? rawMin : void 0;
2672
- const max = typeof rawMax === "number" && Number.isFinite(rawMax) ? rawMax : void 0;
2673
- const step = typeof rawStep === "number" && Number.isFinite(rawStep) && rawStep > 0 ? rawStep : void 0;
2674
- return {
2675
- kind: "numeric",
2676
- value: v,
2677
- options: [],
2678
- lastChangedAt,
2679
- unit,
2680
- min,
2681
- max,
2682
- step,
2683
- precision: typeof rawPrecision === "number" && Number.isFinite(rawPrecision) ? rawPrecision : precisionFromStep(step)
2684
- };
2685
- }
2756
+ case "numeric": return decodeNumeric(state, lastChangedAt, decode);
2686
2757
  case "select": {
2687
2758
  const options = Array.isArray(state.attributes["options"]) ? state.attributes["options"].filter((x) => typeof x === "string") : [];
2688
2759
  return {
@@ -2698,14 +2769,72 @@ function decodeControlSlice(kind, state, lastChangedAt, resolveFormat) {
2698
2769
  options: [],
2699
2770
  lastChangedAt
2700
2771
  };
2701
- case "datetime": return {
2702
- kind: "datetime",
2703
- value: state.state,
2704
- options: [],
2705
- lastChangedAt,
2706
- format: resolveFormat()
2707
- };
2772
+ case "datetime":
2773
+ decode.noteShape(decode.shape());
2774
+ return {
2775
+ kind: "datetime",
2776
+ value: state.state,
2777
+ options: [],
2778
+ lastChangedAt
2779
+ };
2780
+ }
2781
+ }
2782
+ function decodeNumeric(state, lastChangedAt, decode) {
2783
+ const v = Number(state.state);
2784
+ if (!Number.isFinite(v)) return null;
2785
+ const rawUnit = state.attributes["unit_of_measurement"];
2786
+ const unit = typeof rawUnit === "string" && rawUnit.length > 0 ? rawUnit : void 0;
2787
+ const incoming = unit === void 0 ? void 0 : quantityFromUnit(unit);
2788
+ if (unit !== void 0 && (incoming === void 0 || incoming === "device")) {
2789
+ decode.logger.warn("numeric reading refused", {
2790
+ tags: { deviceId: decode.deviceId },
2791
+ meta: {
2792
+ quantity: "device",
2793
+ unit,
2794
+ reason: "unknown-unit"
2795
+ }
2796
+ });
2797
+ return null;
2708
2798
  }
2799
+ const stored = decode.quantity;
2800
+ const quantity = stored !== void 0 && stored !== "device" ? stored : incoming ?? "none";
2801
+ if (incoming !== void 0 && incoming !== "device" && incoming !== quantity) {
2802
+ decode.logger.warn("numeric reading refused", {
2803
+ tags: { deviceId: decode.deviceId },
2804
+ meta: {
2805
+ quantity,
2806
+ unit: unit ?? "",
2807
+ reason: "cross-dimension"
2808
+ }
2809
+ });
2810
+ return null;
2811
+ }
2812
+ const value = takeReading(decode.logger, decode.deviceId, v, unit, quantity);
2813
+ if (value === null) return null;
2814
+ decode.note(quantity, unit === void 0 ? void 0 : normalizeUnit(unit));
2815
+ const rawMin = state.attributes["min"];
2816
+ const rawMax = state.attributes["max"];
2817
+ const rawStep = state.attributes["step"];
2818
+ const min = typeof rawMin === "number" ? takeReading(decode.logger, decode.deviceId, rawMin, unit, quantity) : void 0;
2819
+ const max = typeof rawMax === "number" ? takeReading(decode.logger, decode.deviceId, rawMax, unit, quantity) : void 0;
2820
+ const step = convertStep(typeof rawStep === "number" && rawStep > 0 ? rawStep : void 0, unit, quantity);
2821
+ return {
2822
+ kind: "numeric",
2823
+ value,
2824
+ options: [],
2825
+ lastChangedAt,
2826
+ ...min !== void 0 && min !== null ? { min } : {},
2827
+ ...max !== void 0 && max !== null ? { max } : {},
2828
+ ...step !== void 0 ? { step } : {}
2829
+ };
2830
+ }
2831
+ /** A step is a difference: °F's offset must not move it. */
2832
+ function convertStep(step, unit, quantity) {
2833
+ if (step === void 0) return void 0;
2834
+ const base = baseUnitOf(quantity);
2835
+ const from = unit === void 0 ? void 0 : normalizeUnit(unit);
2836
+ if (base === void 0 || from === void 0 || from === base) return step;
2837
+ return tryConvertDelta(step, from, base);
2709
2838
  }
2710
2839
  //#endregion
2711
2840
  //#region src/devices/ha-script-device.ts
@@ -2951,15 +3080,15 @@ var HaPowerMeterDevice = class extends HaBaseChildDevice {
2951
3080
  const value = Number(state.state);
2952
3081
  if (!Number.isFinite(value)) return;
2953
3082
  const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
2954
- const deviceClass = typeof state.attributes["device_class"] === "string" ? state.attributes["device_class"] : this.haDeviceClass;
2955
- const unit = normalizeUnit(typeof state.attributes["unit_of_measurement"] === "string" ? state.attributes["unit_of_measurement"] : void 0);
2956
- const precision = typeof state.attributes["suggested_display_precision"] === "number" ? state.attributes["suggested_display_precision"] : void 0;
2957
- const update = applyDeviceClass$1(this.runtimeState.getCapState("power-meter") ?? COLD_START$4, deviceClass, value, lastFetchedAt);
2958
- if (update !== null) this.runtimeState.setCapState("power-meter", {
2959
- ...update,
2960
- ...unit !== void 0 ? { unit } : {},
2961
- ...precision !== void 0 ? { precision } : {}
2962
- });
3083
+ const deviceClassRaw = state.attributes["device_class"];
3084
+ const deviceClass = typeof deviceClassRaw === "string" ? deviceClassRaw : this.haDeviceClass;
3085
+ const quantity = powerQuantity(deviceClass);
3086
+ if (quantity === void 0) return;
3087
+ const unitRaw = state.attributes["unit_of_measurement"];
3088
+ const stored = takeReading(this.deviceLogger, this.id, value, typeof unitRaw === "string" ? unitRaw : void 0, quantity);
3089
+ if (stored === null) return;
3090
+ const update = applyDeviceClass$1(this.runtimeState.getCapState("power-meter") ?? COLD_START$4, deviceClass, stored, lastFetchedAt);
3091
+ if (update !== null) this.runtimeState.setCapState("power-meter", update);
2963
3092
  }
2964
3093
  registerPowerMeterCap() {
2965
3094
  this.ctx.registerNativeCap(powerMeterCapability, { getStatus: async () => {
@@ -2967,6 +3096,15 @@ var HaPowerMeterDevice = class extends HaBaseChildDevice {
2967
3096
  } });
2968
3097
  }
2969
3098
  };
3099
+ function powerQuantity(deviceClass) {
3100
+ switch (deviceClass) {
3101
+ case "power": return "power";
3102
+ case "energy": return "energy";
3103
+ case "voltage": return "voltage";
3104
+ case "current": return "current";
3105
+ default: return;
3106
+ }
3107
+ }
2970
3108
  function applyDeviceClass$1(prev, deviceClass, value, lastFetchedAt) {
2971
3109
  switch (deviceClass) {
2972
3110
  case "power": return {
@@ -3021,15 +3159,15 @@ var HaAirQualityDevice = class extends HaBaseChildDevice {
3021
3159
  const value = Number(state.state);
3022
3160
  if (!Number.isFinite(value)) return;
3023
3161
  const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
3024
- const deviceClass = typeof state.attributes["device_class"] === "string" ? state.attributes["device_class"] : null;
3025
- const unit = normalizeUnit(typeof state.attributes["unit_of_measurement"] === "string" ? state.attributes["unit_of_measurement"] : void 0);
3026
- const precision = typeof state.attributes["suggested_display_precision"] === "number" ? state.attributes["suggested_display_precision"] : void 0;
3027
- const update = applyDeviceClass(this.runtimeState.getCapState("air-quality-sensor") ?? COLD_START$3, deviceClass, value, lastFetchedAt);
3028
- if (update !== null) this.runtimeState.setCapState("air-quality-sensor", {
3029
- ...update,
3030
- ...unit !== void 0 ? { unit } : {},
3031
- ...precision !== void 0 ? { precision } : {}
3032
- });
3162
+ const deviceClassRaw = state.attributes["device_class"];
3163
+ const deviceClass = typeof deviceClassRaw === "string" ? deviceClassRaw : null;
3164
+ const quantity = airQuantity(deviceClass);
3165
+ if (quantity === void 0) return;
3166
+ const unitRaw = state.attributes["unit_of_measurement"];
3167
+ const stored = takeReading(this.deviceLogger, this.id, value, typeof unitRaw === "string" ? unitRaw : void 0, quantity);
3168
+ if (stored === null) return;
3169
+ const update = applyDeviceClass(this.runtimeState.getCapState("air-quality-sensor") ?? COLD_START$3, deviceClass, stored, lastFetchedAt);
3170
+ if (update !== null) this.runtimeState.setCapState("air-quality-sensor", update);
3033
3171
  }
3034
3172
  registerCap() {
3035
3173
  this.ctx.registerNativeCap(airQualitySensorCapability, { getStatus: async () => {
@@ -3037,6 +3175,17 @@ var HaAirQualityDevice = class extends HaBaseChildDevice {
3037
3175
  } });
3038
3176
  }
3039
3177
  };
3178
+ function airQuantity(deviceClass) {
3179
+ switch (deviceClass) {
3180
+ case "co2": return "concentration-volume";
3181
+ case "volatile_organic_compounds": return "concentration-ppb";
3182
+ case "pm25":
3183
+ case "pm2_5":
3184
+ case "pm10": return "concentration-mass";
3185
+ case "aqi": return "index";
3186
+ default: return;
3187
+ }
3188
+ }
3040
3189
  function applyDeviceClass(prev, deviceClass, value, lastFetchedAt) {
3041
3190
  switch (deviceClass) {
3042
3191
  case "co2": return {
@@ -3088,9 +3237,9 @@ var HaEnumSensorDevice = class extends HaBaseChildDevice {
3088
3237
  /**
3089
3238
  * Locale-render shape for a `DateTimeSensor`-role sensor (HA
3090
3239
  * `device_class=timestamp` → `'datetime'`, `date` → `'date'`). Read from the
3091
- * persisted child config (threaded by the HA mapping) and seeded into the cap
3092
- * slice alongside `value` so the UI renders the ISO string locale-formatted.
3093
- * `undefined` for a plain enum sensor — the slice then omits `format`.
3240
+ * persisted child config (threaded by the HA mapping) and stored once on the
3241
+ * device. A timestamp sensor marks the slice so history does not learn the
3242
+ * ISO string as an enum state.
3094
3243
  */
3095
3244
  dateTimeFormat;
3096
3245
  constructor(ctx) {
@@ -3098,15 +3247,14 @@ var HaEnumSensorDevice = class extends HaBaseChildDevice {
3098
3247
  super(ctx, persisted.role !== void 0 ? { role: persisted.role } : {});
3099
3248
  const parsedFormat = EnumSensorDateTimeFormatSchema.safeParse(persisted.dateTimeFormat);
3100
3249
  this.dateTimeFormat = parsedFormat.success ? parsedFormat.data : void 0;
3250
+ if (this.dateTimeFormat !== void 0) this.updateSourceInfo({ dateTimeShape: this.dateTimeFormat });
3101
3251
  this.registerCap();
3102
3252
  this.runtimeState.setCapState("enum-sensor", this.coldStart());
3103
3253
  }
3104
- /** Cold-start slice — carries the format up-front so the UI can render the
3105
- * correct shape the instant a value lands, with no slice churn. */
3106
3254
  coldStart() {
3107
3255
  return {
3108
3256
  ...COLD_START$2,
3109
- ...this.dateTimeFormat !== void 0 ? { format: this.dateTimeFormat } : {}
3257
+ ...this.dateTimeFormat !== void 0 ? { timestamp: true } : {}
3110
3258
  };
3111
3259
  }
3112
3260
  handleStatePush(state) {
@@ -3114,7 +3262,7 @@ var HaEnumSensorDevice = class extends HaBaseChildDevice {
3114
3262
  const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
3115
3263
  this.runtimeState.setCapState("enum-sensor", {
3116
3264
  value: state.state,
3117
- ...this.dateTimeFormat !== void 0 ? { format: this.dateTimeFormat } : {},
3265
+ ...this.dateTimeFormat !== void 0 ? { timestamp: true } : {},
3118
3266
  lastFetchedAt
3119
3267
  });
3120
3268
  }
@@ -3271,12 +3419,9 @@ function readEventTypesFromAttributes(attributes) {
3271
3419
  var COLD_START$1 = {
3272
3420
  condition: null,
3273
3421
  temperature: null,
3274
- temperatureUnit: null,
3275
3422
  humidity: null,
3276
3423
  pressure: null,
3277
- pressureUnit: null,
3278
3424
  windSpeed: null,
3279
- windSpeedUnit: null,
3280
3425
  windBearing: null,
3281
3426
  lastFetchedAt: 0
3282
3427
  };
@@ -3308,13 +3453,10 @@ var HaWeatherDevice = class extends HaBaseChildDevice {
3308
3453
  const attrs = state.attributes;
3309
3454
  const slice = {
3310
3455
  condition: state.state,
3311
- temperature: readNumberAttr(attrs, "temperature"),
3312
- temperatureUnit: readStringAttr$1(attrs, "temperature_unit"),
3456
+ temperature: this.readBase(attrs, "temperature", "temperature_unit", "temperature"),
3313
3457
  humidity: clampOrNull(readNumberAttr(attrs, "humidity"), 0, 100),
3314
- pressure: readNumberAttr(attrs, "pressure"),
3315
- pressureUnit: readStringAttr$1(attrs, "pressure_unit"),
3316
- windSpeed: readNumberAttr(attrs, "wind_speed"),
3317
- windSpeedUnit: readStringAttr$1(attrs, "wind_speed_unit"),
3458
+ pressure: this.readBase(attrs, "pressure", "pressure_unit", "pressure"),
3459
+ windSpeed: this.readBase(attrs, "wind_speed", "wind_speed_unit", "speed"),
3318
3460
  windBearing: readNumberAttr(attrs, "wind_bearing"),
3319
3461
  lastFetchedAt
3320
3462
  };
@@ -3325,6 +3467,13 @@ var HaWeatherDevice = class extends HaBaseChildDevice {
3325
3467
  return this.runtimeState.getCapState("weather") ?? COLD_START$1;
3326
3468
  } });
3327
3469
  }
3470
+ /** One weather attribute, converted into the quantity's base unit. */
3471
+ readBase(attrs, valueKey, unitKey, quantity) {
3472
+ const raw = readNumberAttr(attrs, valueKey);
3473
+ if (raw === null) return null;
3474
+ const unit = readStringAttr$1(attrs, unitKey);
3475
+ return takeReading(this.deviceLogger, this.id, raw, unit ?? void 0, quantity);
3476
+ }
3328
3477
  };
3329
3478
  function readNumberAttr(attrs, key) {
3330
3479
  const v = attrs[key];