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