Fix incorrect data

This commit is contained in:
Sebastien Lavoie
2026-03-30 08:43:37 -04:00
parent 7f8bc63c62
commit 9582cb0665
4 changed files with 272 additions and 62 deletions
+54 -42
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.climate import ( from homeassistant.components.climate import (
HVACAction,
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
@@ -14,7 +15,6 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTempera
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ( from .const import (
DEVICE_MODE_AUTO, DEVICE_MODE_AUTO,
@@ -29,7 +29,16 @@ from .const import (
SPEED_80, SPEED_80,
SPEED_100, SPEED_100,
SPEED_AUTO, SPEED_AUTO,
TEMP_FAHRENHEIT, )
from .model import (
available_fan_speeds,
current_temperature as model_current_temperature,
inferred_hvac_action,
requested_mode,
supports_swing,
target_temperature as model_target_temperature,
target_temperature_key,
to_device_temperature,
) )
from .coordinator import DknCoordinator from .coordinator import DknCoordinator
from .entity import DknEntity from .entity import DknEntity
@@ -99,9 +108,11 @@ class DknClimateEntity(DknEntity, ClimateEntity):
features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
if self.hvac_mode not in _NO_TARGET_TEMP_MODES: if self.hvac_mode not in _NO_TARGET_TEMP_MODES:
features |= ClimateEntityFeature.TARGET_TEMPERATURE features |= ClimateEntityFeature.TARGET_TEMPERATURE
return ( if available_fan_speeds(self._device_data):
features | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE features |= ClimateEntityFeature.FAN_MODE
) if supports_swing(self._device_data):
features |= ClimateEntityFeature.SWING_MODE
return features
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
@@ -112,31 +123,39 @@ class DknClimateEntity(DknEntity, ClimateEntity):
optimistic_mode = self._optimistic_get("hvac_mode", None) optimistic_mode = self._optimistic_get("hvac_mode", None)
if optimistic_mode is not None: if optimistic_mode is not None:
return optimistic_mode return optimistic_mode
mode_int = data.get("real_mode") or data.get("mode", DEVICE_MODE_AUTO) mode_int = requested_mode(data) or DEVICE_MODE_AUTO
return _MODE_TO_HVAC.get(int(mode_int), HVACMode.AUTO) return _MODE_TO_HVAC.get(int(mode_int), HVACMode.AUTO)
@property @property
def current_temperature(self) -> float | None: def hvac_action(self) -> HVACAction | None:
value = self._device_data.get("work_temp") if self.hvac_mode == HVACMode.OFF:
if value is None: return HVACAction.OFF
action = inferred_hvac_action(self._device_data)
if action == "heating":
return HVACAction.HEATING
if action == "cooling":
return HVACAction.COOLING
if action == "idle":
return HVACAction.IDLE
if action == "fan":
return HVACAction.FAN
if action == "drying":
return HVACAction.DRYING
return None return None
return self._from_device_temperature(float(value))
@property
def current_temperature(self) -> float | None:
return model_current_temperature(self._device_data)
@property @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
mode = self.hvac_mode mode = self.hvac_mode
if mode in _NO_TARGET_TEMP_MODES: if mode in _NO_TARGET_TEMP_MODES:
return None return None
data = self._device_data fallback = model_target_temperature(self._device_data)
if mode == HVACMode.HEAT: if fallback is None:
value = data.get("setpoint_air_heat")
elif mode == HVACMode.COOL:
value = data.get("setpoint_air_cool")
else:
value = data.get("setpoint_air_auto")
if value is None:
return None return None
fallback = self._from_device_temperature(float(value))
return self._optimistic_get("target_temp", fallback) return self._optimistic_get("target_temp", fallback)
@property @property
@@ -220,6 +239,8 @@ class DknClimateEntity(DknEntity, ClimateEntity):
speed = _FAN_TO_SPEED.get(fan_mode) speed = _FAN_TO_SPEED.get(fan_mode)
if speed is None: if speed is None:
raise HomeAssistantError(f"Unsupported fan mode: {fan_mode}") raise HomeAssistantError(f"Unsupported fan mode: {fan_mode}")
if speed not in available_fan_speeds(self._device_data):
raise HomeAssistantError(f"Fan mode not supported by device: {fan_mode}")
installation_id = self._installation_id installation_id = self._installation_id
async with self._get_device_lock(): async with self._get_device_lock():
@@ -237,6 +258,8 @@ class DknClimateEntity(DknEntity, ClimateEntity):
async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_swing_mode(self, swing_mode: str) -> None:
if swing_mode not in {"off", "swing"}: if swing_mode not in {"off", "swing"}:
raise HomeAssistantError(f"Unsupported swing mode: {swing_mode}") raise HomeAssistantError(f"Unsupported swing mode: {swing_mode}")
if not supports_swing(self._device_data):
raise HomeAssistantError("Swing mode not supported by device")
installation_id = self._installation_id installation_id = self._installation_id
slat = 9 if swing_mode == "swing" else 0 slat = 9 if swing_mode == "swing" else 0
@@ -266,32 +289,21 @@ class DknClimateEntity(DknEntity, ClimateEntity):
return installation_id return installation_id
def _temperature_property_for_mode(self, hvac_mode: HVACMode) -> str: def _temperature_property_for_mode(self, hvac_mode: HVACMode) -> str:
if hvac_mode == HVACMode.HEAT:
return "setpoint_air_heat"
if hvac_mode == HVACMode.COOL:
return "setpoint_air_cool"
if hvac_mode not in {HVACMode.AUTO, HVACMode.HEAT, HVACMode.COOL}: if hvac_mode not in {HVACMode.AUTO, HVACMode.HEAT, HVACMode.COOL}:
raise HomeAssistantError( raise HomeAssistantError(
f"Target temperature is not supported in {hvac_mode} mode" f"Target temperature is not supported in {hvac_mode} mode"
) )
return "setpoint_air_auto" requested = {
HVACMode.AUTO: DEVICE_MODE_AUTO,
HVACMode.COOL: DEVICE_MODE_COOL,
HVACMode.HEAT: DEVICE_MODE_HEAT,
}[hvac_mode]
key = target_temperature_key(requested)
if key is None:
raise HomeAssistantError(
f"Target temperature is not supported in {hvac_mode} mode"
)
return key
def _to_device_temperature(self, temperature_c: float) -> float | int: def _to_device_temperature(self, temperature_c: float) -> float | int:
if int(self._device_data.get("units", 0)) != TEMP_FAHRENHEIT: return to_device_temperature(temperature_c, self._device_data.get("units"))
return temperature_c
fahrenheit = TemperatureConverter.convert(
temperature_c,
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
)
return round(fahrenheit)
def _from_device_temperature(self, value: float) -> float:
if int(self._device_data.get("units", 0)) != TEMP_FAHRENHEIT:
return value
celsius = TemperatureConverter.convert(
value,
UnitOfTemperature.FAHRENHEIT,
UnitOfTemperature.CELSIUS,
)
return round(celsius, 1)
+137
View File
@@ -0,0 +1,137 @@
"""Shared device-state helpers for DKN Cloud NA."""
from __future__ import annotations
from typing import Any
from .const import (
DEVICE_MODE_AUTO,
DEVICE_MODE_COOL,
DEVICE_MODE_DRY,
DEVICE_MODE_FAN,
DEVICE_MODE_HEAT,
SPEED_20,
SPEED_40,
SPEED_60,
SPEED_80,
SPEED_100,
SPEED_AUTO,
TEMP_FAHRENHEIT,
)
def as_bool(value: Any) -> bool | None:
"""Return a boolean for a real boolean value, else None."""
if isinstance(value, bool):
return value
return None
def as_int(value: Any) -> int | None:
"""Return an integer for int-like values, else None."""
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
return None
def to_celsius(value: Any, units: Any) -> float | None:
"""Convert a device temperature to Celsius if needed."""
if value is None:
return None
temp = float(value)
if as_int(units) != TEMP_FAHRENHEIT:
return temp
return round((temp - 32) * 5 / 9, 1)
def to_device_temperature(value_c: float, units: Any) -> float | int:
"""Convert a Celsius temperature to the device units."""
if as_int(units) != TEMP_FAHRENHEIT:
return value_c
return round((value_c * 9 / 5) + 32)
def requested_mode(data: dict[str, Any]) -> int | None:
"""Return the requested device mode."""
return as_int(data.get("mode"))
def live_mode(data: dict[str, Any]) -> int | None:
"""Return the live device mode, when reported."""
return as_int(data.get("real_mode"))
def current_temperature(data: dict[str, Any]) -> float | None:
"""Return the indoor temperature in Celsius."""
return to_celsius(data.get("work_temp", data.get("local_temp")), data.get("units"))
def exterior_temperature(data: dict[str, Any]) -> float | None:
"""Return the exterior temperature in Celsius."""
return to_celsius(data.get("ext_temp"), data.get("units"))
def target_temperature_key(mode: int | None) -> str | None:
"""Return the setpoint key for the requested mode."""
if mode == DEVICE_MODE_HEAT:
return "setpoint_air_heat"
if mode == DEVICE_MODE_COOL:
return "setpoint_air_cool"
if mode == DEVICE_MODE_AUTO:
return "setpoint_air_auto"
return None
def target_temperature(data: dict[str, Any]) -> float | None:
"""Return the requested target temperature in Celsius."""
key = target_temperature_key(requested_mode(data))
if key is None:
return None
return to_celsius(data.get(key), data.get("units"))
def inferred_hvac_action(data: dict[str, Any]) -> str:
"""Infer the active HVAC action from requested mode and temperatures."""
power = as_bool(data.get("power"))
if not power:
return "off"
mode = requested_mode(data)
current = current_temperature(data)
target = target_temperature(data)
if mode == DEVICE_MODE_HEAT:
if current is not None and target is not None and current < target:
return "heating"
return "idle"
if mode == DEVICE_MODE_COOL:
if current is not None and target is not None and current > target:
return "cooling"
return "idle"
if mode == DEVICE_MODE_AUTO:
if current is not None and target is not None:
if current < target:
return "heating"
if current > target:
return "cooling"
return "idle"
if mode == DEVICE_MODE_FAN:
return "fan"
if mode == DEVICE_MODE_DRY:
return "drying"
return "on"
def available_fan_speeds(data: dict[str, Any]) -> list[int]:
"""Return the supported fan speed codes for the device."""
raw = data.get("speed_available")
if isinstance(raw, list):
return [speed for speed in (as_int(item) for item in raw) if speed is not None]
return [SPEED_AUTO, SPEED_20, SPEED_40, SPEED_60, SPEED_80, SPEED_100]
def supports_swing(data: dict[str, Any]) -> bool:
"""Return whether the device appears to support vertical swing control."""
return "slats_vertical_1" in data or as_int(data.get("slats_vnum")) not in (None, 0)
+6
View File
@@ -20,11 +20,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import DknCoordinator from .coordinator import DknCoordinator
from .entity import DknEntity from .entity import DknEntity
from .model import current_temperature, exterior_temperature
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class DknSensorEntityDescription(SensorEntityDescription): class DknSensorEntityDescription(SensorEntityDescription):
"""Extend SensorEntityDescription with a device_data key.""" """Extend SensorEntityDescription with a device_data key."""
data_key: str = "" data_key: str = ""
@@ -95,4 +97,8 @@ class DknSensorEntity(DknEntity, SensorEntity):
@property @property
def native_value(self) -> Any: def native_value(self) -> Any:
if self.entity_description.data_key == "work_temp":
return current_temperature(self._device_data)
if self.entity_description.data_key == "ext_temp":
return exterior_temperature(self._device_data)
return self._device_data.get(self.entity_description.data_key) return self._device_data.get(self.entity_description.data_key)
+75 -20
View File
@@ -97,21 +97,57 @@ def _effective_state(device: dict[str, Any]) -> dict[str, Any]:
is_connected = _bool_or_none(device.get("isConnected")) is_connected = _bool_or_none(device.get("isConnected"))
machine_ready = _bool_or_none(device.get("machineready")) machine_ready = _bool_or_none(device.get("machineready"))
hvac_state = "off" hvac_mode = "off"
active_mode = real_mode if real_mode is not None else mode hvac_action = "off"
if power: if power:
if active_mode == 3: if mode == 3:
hvac_state = "heating" hvac_mode = "heat"
elif active_mode == 2: elif mode == 2:
hvac_state = "cooling" hvac_mode = "cool"
elif active_mode == 1: elif mode == 1:
hvac_state = "auto" hvac_mode = "auto"
elif active_mode == 4: elif mode == 4:
hvac_state = "fan_only" hvac_mode = "fan_only"
elif active_mode == 5: elif mode == 5:
hvac_state = "dry" hvac_mode = "dry"
else: else:
hvac_state = "on" hvac_mode = "on"
current = device.get("work_temp")
target = None
if mode == 3:
target = device.get("setpoint_air_heat")
elif mode == 2:
target = device.get("setpoint_air_cool")
elif mode == 1:
target = device.get("setpoint_air_auto")
if mode == 3:
if current is not None and target is not None and current < target:
hvac_action = "heating"
else:
hvac_action = "idle"
elif mode == 2:
if current is not None and target is not None and current > target:
hvac_action = "cooling"
else:
hvac_action = "idle"
elif mode == 1:
if current is not None and target is not None:
if current < target:
hvac_action = "heating"
elif current > target:
hvac_action = "cooling"
else:
hvac_action = "idle"
else:
hvac_action = "idle"
elif mode == 4:
hvac_action = "fan"
elif mode == 5:
hvac_action = "drying"
else:
hvac_action = "on"
return { return {
"connected": is_connected, "connected": is_connected,
@@ -120,7 +156,8 @@ def _effective_state(device: dict[str, Any]) -> dict[str, Any]:
"mode": mode, "mode": mode,
"real_mode": real_mode, "real_mode": real_mode,
"units": units, "units": units,
"hvac_state": hvac_state, "hvac_mode": hvac_mode,
"hvac_action": hvac_action,
"work_temp": device.get("work_temp"), "work_temp": device.get("work_temp"),
"setpoint_air_auto": device.get("setpoint_air_auto"), "setpoint_air_auto": device.get("setpoint_air_auto"),
"setpoint_air_cool": device.get("setpoint_air_cool"), "setpoint_air_cool": device.get("setpoint_air_cool"),
@@ -130,6 +167,19 @@ def _effective_state(device: dict[str, Any]) -> dict[str, Any]:
} }
def _supports_write_verification(device: dict[str, Any], property_name: str) -> bool:
if property_name == "slats_vertical_1":
return "slats_vertical_1" in device or _int_or_none(
device.get("slats_vnum")
) not in (None, 0)
if property_name == "speed_state":
available = device.get("speed_available")
return isinstance(available, list) and len(available) > 0
if property_name.startswith("setpoint_air_"):
return device.get(property_name) is not None
return True
def _has_live_state(device: dict[str, Any]) -> bool: def _has_live_state(device: dict[str, Any]) -> bool:
state = _effective_state(device) state = _effective_state(device)
return any( return any(
@@ -277,6 +327,14 @@ async def _main() -> None:
installation_id, mac, property_name, value installation_id, mac, property_name, value
) )
async def send_and_verify(
property_name: str, value: Any, timeout: float = 20.0
) -> bool | str:
if not _supports_write_verification(current_device, property_name):
return "unsupported_by_payload"
await send(property_name, value)
return await wait_for_property(property_name, value, timeout=timeout)
restore_actions: list[tuple[str, Any]] = [] restore_actions: list[tuple[str, Any]] = []
try: try:
await send("power", current_device.get("power", False)) await send("power", current_device.get("power", False))
@@ -287,8 +345,7 @@ async def _main() -> None:
original_swing = int(current_device.get("slats_vertical_1", 0) or 0) original_swing = int(current_device.get("slats_vertical_1", 0) or 0)
test_swing = 0 if original_swing == 9 else 9 test_swing = 0 if original_swing == 9 else 9
await send("slats_vertical_1", test_swing) results["writes"]["swing_toggle"] = await send_and_verify(
results["writes"]["swing_toggle"] = await wait_for_property(
"slats_vertical_1", test_swing "slats_vertical_1", test_swing
) )
restore_actions.append(("slats_vertical_1", original_swing)) restore_actions.append(("slats_vertical_1", original_swing))
@@ -298,8 +355,7 @@ async def _main() -> None:
if test_fan == original_fan: if test_fan == original_fan:
results["writes"]["fan_speed"] = "skipped_no_alternate" results["writes"]["fan_speed"] = "skipped_no_alternate"
else: else:
await send("speed_state", test_fan) results["writes"]["fan_speed"] = await send_and_verify(
results["writes"]["fan_speed"] = await wait_for_property(
"speed_state", test_fan "speed_state", test_fan
) )
restore_actions.append(("speed_state", original_fan)) restore_actions.append(("speed_state", original_fan))
@@ -322,8 +378,7 @@ async def _main() -> None:
if test_temp == original_temp: if test_temp == original_temp:
results["writes"]["temperature"] = "skipped_no_room" results["writes"]["temperature"] = "skipped_no_room"
else: else:
await send(property_name, test_temp) results["writes"]["temperature"] = await send_and_verify(
results["writes"]["temperature"] = await wait_for_property(
property_name, test_temp property_name, test_temp
) )
restore_actions.append((property_name, original_temp)) restore_actions.append((property_name, original_temp))