diff --git a/custom_components/dkncloudna/climate.py b/custom_components/dkncloudna/climate.py new file mode 100644 index 0000000..2a11680 --- /dev/null +++ b/custom_components/dkncloudna/climate.py @@ -0,0 +1,175 @@ +"""Climate entity for DKN Cloud NA.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DEVICE_MODE_AUTO, + DEVICE_MODE_COOL, + DEVICE_MODE_DRY, + DEVICE_MODE_FAN, + DEVICE_MODE_HEAT, + DOMAIN, + LOGGER, + SPEED_20, + SPEED_40, + SPEED_60, + SPEED_80, + SPEED_100, + SPEED_AUTO, +) +from .coordinator import DknCoordinator +from .entity import DknEntity + +# Map DKN device mode integers to HA HVACMode +_MODE_TO_HVAC: dict[int, HVACMode] = { + DEVICE_MODE_AUTO: HVACMode.AUTO, + DEVICE_MODE_COOL: HVACMode.COOL, + DEVICE_MODE_HEAT: HVACMode.HEAT, + DEVICE_MODE_FAN: HVACMode.FAN_ONLY, + DEVICE_MODE_DRY: HVACMode.DRY, +} +_HVAC_TO_MODE: dict[HVACMode, int] = {v: k for k, v in _MODE_TO_HVAC.items()} + +# Fan speed labels +_FAN_MODES = ["auto", "20%", "40%", "60%", "80%", "100%"] +_SPEED_TO_FAN: dict[int, str] = { + SPEED_AUTO: "auto", + SPEED_20: "20%", + SPEED_40: "40%", + SPEED_60: "60%", + SPEED_80: "80%", + SPEED_100: "100%", +} +_FAN_TO_SPEED: dict[str, int] = {v: k for k, v in _SPEED_TO_FAN.items()} + +# Modes where a temperature target makes no sense +_NO_TARGET_TEMP_MODES = {HVACMode.FAN_ONLY, HVACMode.DRY, HVACMode.OFF} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate entities from a config entry.""" + coordinator: DknCoordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] + async_add_entities( + DknClimateEntity(coordinator, mac) + for mac in (coordinator.data or {}) + ) + + +class DknClimateEntity(DknEntity, ClimateEntity): + """Climate entity representing one DKN Cloud NA AC unit.""" + + _attr_name = None # entity name = device name + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_precision = PRECISION_WHOLE + _attr_hvac_modes = [ + HVACMode.OFF, + HVACMode.AUTO, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.FAN_ONLY, + ] + _attr_fan_modes = _FAN_MODES + _attr_swing_modes = ["off", "swing"] + _attr_min_temp = 16 + _attr_max_temp = 32 + _attr_target_temperature_step = 1 + + def __init__(self, coordinator: DknCoordinator, mac: str) -> None: + super().__init__(coordinator, mac) + self._attr_unique_id = f"{DOMAIN}_{mac}" + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return feature flags appropriate for the current HVAC mode.""" + features = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF + mode = self.hvac_mode + if mode not in _NO_TARGET_TEMP_MODES: + features |= ClimateEntityFeature.TARGET_TEMPERATURE + features |= ClimateEntityFeature.FAN_MODE + features |= ClimateEntityFeature.SWING_MODE + return features + + @property + def hvac_mode(self) -> HVACMode: + data = self._device_data + if not data.get("power", False): + return HVACMode.OFF + mode_int = data.get("real_mode") or data.get("mode", DEVICE_MODE_AUTO) + return _MODE_TO_HVAC.get(int(mode_int), HVACMode.AUTO) + + @property + def current_temperature(self) -> float | None: + return self._device_data.get("work_temp") + + @property + def target_temperature(self) -> float | None: + if self.hvac_mode in _NO_TARGET_TEMP_MODES: + return None + data = self._device_data + mode = data.get("real_mode") or data.get("mode", DEVICE_MODE_AUTO) + if int(mode) == DEVICE_MODE_HEAT: + return self._optimistic_get("target_temp", data.get("setpoint_air_heat")) + if int(mode) == DEVICE_MODE_COOL: + return self._optimistic_get("target_temp", data.get("setpoint_air_cool")) + return self._optimistic_get("target_temp", data.get("setpoint_air_auto")) + + @property + def target_temperature_high(self) -> float | None: + return None # no range mode + + @property + def target_temperature_low(self) -> float | None: + return None + + @property + def fan_mode(self) -> str | None: + speed = self._device_data.get("speed_state", SPEED_AUTO) + return self._optimistic_get("fan_mode", _SPEED_TO_FAN.get(int(speed), "auto")) + + @property + def swing_mode(self) -> str | None: + slat = self._device_data.get("slats_vertical_1", 0) + return self._optimistic_get("swing_mode", "swing" if int(slat) == 9 else "off") + + # ------------------------------------------------------------------ + # Service handlers — all stub (raise NotImplementedError for now) + # ------------------------------------------------------------------ + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + LOGGER.debug("set_hvac_mode(%s) stub for %s", hvac_mode, self._mac) + raise NotImplementedError("Device control not yet implemented") + + async def async_set_temperature(self, **kwargs: Any) -> None: + LOGGER.debug("set_temperature(%s) stub for %s", kwargs, self._mac) + raise NotImplementedError("Device control not yet implemented") + + async def async_set_fan_mode(self, fan_mode: str) -> None: + LOGGER.debug("set_fan_mode(%s) stub for %s", fan_mode, self._mac) + raise NotImplementedError("Device control not yet implemented") + + async def async_set_swing_mode(self, swing_mode: str) -> None: + LOGGER.debug("set_swing_mode(%s) stub for %s", swing_mode, self._mac) + raise NotImplementedError("Device control not yet implemented") + + async def async_turn_on(self) -> None: + await self.async_set_hvac_mode(HVACMode.AUTO) + + async def async_turn_off(self) -> None: + await self.async_set_hvac_mode(HVACMode.OFF)