import logging from dataclasses import dataclass from typing import Any from homeassistant.components.climate import ( ClimateEntity, ClimateEntityDescription, ) from homeassistant.components.climate.const import ( SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, UnitOfTemperature, ) from homeassistant.core import callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from pyhon.appliance import HonAppliance from pyhon.parameter.range import HonParameterRange from .const import HON_HVAC_MODE, HON_FAN, DOMAIN, HON_HVAC_PROGRAM from .entity import HonEntity _LOGGER = logging.getLogger(__name__) @dataclass(frozen=True) class HonACClimateEntityDescription(ClimateEntityDescription): pass @dataclass(frozen=True) class HonClimateEntityDescription(ClimateEntityDescription): mode: HVACMode = HVACMode.AUTO CLIMATES: dict[ str, tuple[HonACClimateEntityDescription | HonClimateEntityDescription, ...] ] = { "AC": ( HonACClimateEntityDescription( key="settings", name="Air Conditioner", icon="mdi:air-conditioner", translation_key="air_conditioner", ), ), "REF": ( HonClimateEntityDescription( key="settings.tempSelZ1", mode=HVACMode.COOL, name="Fridge", icon="mdi:thermometer", translation_key="fridge", ), HonClimateEntityDescription( key="settings.tempSelZ2", mode=HVACMode.COOL, name="Freezer", icon="mdi:snowflake-thermometer", translation_key="freezer", ), HonClimateEntityDescription( key="settings.tempSelZ3", mode=HVACMode.COOL, name="MyZone", icon="mdi:thermometer", translation_key="my_zone", ), ), "OV": ( HonClimateEntityDescription( key="settings.tempSel", mode=HVACMode.HEAT, name="Oven", icon="mdi:thermometer", translation_key="oven", ), ), "WC": ( HonClimateEntityDescription( key="settings.tempSel", mode=HVACMode.COOL, name="Wine Cellar", icon="mdi:thermometer", translation_key="wine", ), HonClimateEntityDescription( key="settings.tempSelZ2", mode=HVACMode.COOL, name="Wine Cellar", icon="mdi:thermometer", translation_key="wine", ), ), } async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonClimateEntity | HonACClimateEntity for device in hass.data[DOMAIN][entry.unique_id]["hon"].appliances: for description in CLIMATES.get(device.appliance_type, []): if isinstance(description, HonACClimateEntityDescription): if description.key not in list(device.commands): continue entity = HonACClimateEntity(hass, entry, device, description) elif isinstance(description, HonClimateEntityDescription): if description.key not in device.available_settings: continue entity = HonClimateEntity(hass, entry, device, description) else: continue # type: ignore[unreachable] entities.append(entity) async_add_entities(entities) class HonACClimateEntity(HonEntity, ClimateEntity): entity_description: HonACClimateEntityDescription _enable_turn_on_off_backwards_compatibility = False def __init__( self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance, description: HonACClimateEntityDescription, ) -> None: super().__init__(hass, entry, device, description) self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._set_temperature_bound() self._attr_hvac_modes = [HVACMode.OFF] for mode in device.settings["settings.machMode"].values: self._attr_hvac_modes.append(HON_HVAC_MODE[int(mode)]) self._attr_preset_modes = [] for mode in device.settings["startProgram.program"].values: self._attr_preset_modes.append(mode) self._attr_swing_modes = [ SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH, ] self._attr_supported_features = ( ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.PRESET_MODE ) self._handle_coordinator_update(update=False) def _set_temperature_bound(self) -> None: temperature = self._device.settings["settings.tempSel"] if not isinstance(temperature, HonParameterRange): raise ValueError self._attr_max_temp = temperature.max self._attr_target_temperature_step = temperature.step self._attr_min_temp = temperature.min @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device.get("tempSel", 0.0) @property def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.get("tempIndoor", 0.0) async def async_set_temperature(self, **kwargs: Any) -> None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._device.settings["settings.tempSel"].value = str(int(temperature)) await self._device.commands["settings"].send() self.async_write_ha_state() @property def hvac_mode(self) -> HVACMode: if self._device.get("onOffStatus") == 0: return HVACMode.OFF else: return HON_HVAC_MODE[self._device.get("machMode")] async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: self._attr_hvac_mode = hvac_mode if hvac_mode == HVACMode.OFF: await self._device.commands["stopProgram"].send() self._device.sync_command("stopProgram", "settings") else: self._device.settings["settings.onOffStatus"].value = "1" setting = self._device.settings["settings.machMode"] modes = {HON_HVAC_MODE[int(number)]: number for number in setting.values} if hvac_mode in modes: setting.value = modes[hvac_mode] else: await self.async_set_preset_mode(HON_HVAC_PROGRAM[hvac_mode]) return await self._device.commands["settings"].send() self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: await self._device.commands["startProgram"].send() self._device.sync_command("startProgram", "settings") async def async_turn_off(self, **kwargs: Any) -> None: await self._device.commands["stopProgram"].send() self._device.sync_command("stopProgram", "settings") @property def preset_mode(self) -> str | None: """Return the current Preset for this channel.""" return None async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the new preset mode.""" if program := self._device.settings.get("startProgram.program"): program.value = preset_mode self._device.sync_command("startProgram", "settings") self._set_temperature_bound() self._handle_coordinator_update(update=False) self.async_write_ha_state() self._attr_preset_mode = preset_mode await self._device.commands["startProgram"].send() self.async_write_ha_state() @property def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" fan_modes = [] for mode in reversed(self._device.settings["settings.windSpeed"].values): fan_modes.append(HON_FAN[int(mode)]) return fan_modes @property def fan_mode(self) -> str | None: """Return the fan setting.""" return HON_FAN[self._device.get("windSpeed")] async def async_set_fan_mode(self, fan_mode: str) -> None: fan_modes = {} for mode in reversed(self._device.settings["settings.windSpeed"].values): fan_modes[HON_FAN[int(mode)]] = mode self._device.settings["settings.windSpeed"].value = str(fan_modes[fan_mode]) self._attr_fan_mode = fan_mode await self._device.commands["settings"].send() self.async_write_ha_state() @property def swing_mode(self) -> str | None: """Return the swing setting.""" horizontal = self._device.get("windDirectionHorizontal") vertical = self._device.get("windDirectionVertical") if horizontal == 7 and vertical == 8: return SWING_BOTH if horizontal == 7: return SWING_HORIZONTAL if vertical == 8: return SWING_VERTICAL return SWING_OFF async def async_set_swing_mode(self, swing_mode: str) -> None: horizontal = self._device.settings["settings.windDirectionHorizontal"] vertical = self._device.settings["settings.windDirectionVertical"] if swing_mode in [SWING_BOTH, SWING_HORIZONTAL]: horizontal.value = "7" if swing_mode in [SWING_BOTH, SWING_VERTICAL]: vertical.value = "8" if swing_mode in [SWING_OFF, SWING_HORIZONTAL] and vertical.value == "8": vertical.value = "5" if swing_mode in [SWING_OFF, SWING_VERTICAL] and horizontal.value == "7": horizontal.value = "0" self._attr_swing_mode = swing_mode await self._device.commands["settings"].send() self.async_write_ha_state() @callback def _handle_coordinator_update(self, update: bool = True) -> None: if update: self.async_write_ha_state() class HonClimateEntity(HonEntity, ClimateEntity): entity_description: HonClimateEntityDescription _enable_turn_on_off_backwards_compatibility = False def __init__( self, hass: HomeAssistantType, entry: ConfigEntry, device: HonAppliance, description: HonClimateEntityDescription, ) -> None: super().__init__(hass, entry, device, description) self._attr_supported_features = ( ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TARGET_TEMPERATURE ) self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._set_temperature_bound() self._attr_hvac_modes = [description.mode] if "stopProgram" in device.commands: self._attr_supported_features |= ClimateEntityFeature.TURN_OFF self._attr_hvac_modes += [HVACMode.OFF] modes = [] else: modes = ["no_mode"] for mode, data in device.commands["startProgram"].categories.items(): if mode not in data.parameters["program"].values: continue if (zone := data.parameters.get("zone")) and isinstance( self.entity_description.name, str ): if self.entity_description.name.lower() in zone.values: modes.append(mode) else: modes.append(mode) if modes: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_preset_modes = modes self._handle_coordinator_update(update=False) @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device.get(self.entity_description.key, 0.0) @property def current_temperature(self) -> float | None: """Return the current temperature.""" temp_key = self.entity_description.key.split(".")[-1].replace("Sel", "") return self._device.get(temp_key, 0.0) async def async_set_temperature(self, **kwargs: Any) -> None: if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._device.settings[self.entity_description.key].value = str(int(temperature)) await self._device.commands["settings"].send() self.async_write_ha_state() @property def hvac_mode(self) -> HVACMode: if self._device.get("onOffStatus") == 0: return HVACMode.OFF else: return self.entity_description.mode async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: if len(self.hvac_modes) <= 1: return if hvac_mode == HVACMode.OFF: await self._device.commands["stopProgram"].send() else: await self._device.commands["startProgram"].send() self._attr_hvac_mode = hvac_mode self.async_write_ha_state() async def async_turn_on(self) -> None: """Set the HVAC State to on.""" await self._device.commands["startProgram"].send() async def async_turn_off(self) -> None: """Set the HVAC State to off.""" await self._device.commands["stopProgram"].send() @property def preset_mode(self) -> str | None: """Return the current Preset for this channel.""" if self._device.get("onOffStatus") is not None: return self._device.get("programName", "") else: return self._device.get( f"mode{self.entity_description.key[-2:]}", "no_mode" ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the new preset mode.""" if preset_mode == "no_mode" and HVACMode.OFF in self.hvac_modes: command = "stopProgram" elif preset_mode == "no_mode": command = "settings" self._device.commands["settings"].reset() else: command = "startProgram" if program := self._device.settings.get(f"{command}.program"): program.value = preset_mode zone = self._device.settings.get(f"{command}.zone") if zone and isinstance(self.entity_description.name, str): zone.value = self.entity_description.name.lower() self._device.sync_command(command, "settings") self._set_temperature_bound() self._attr_preset_mode = preset_mode self.async_write_ha_state() await self._device.commands[command].send() self.async_write_ha_state() def _set_temperature_bound(self) -> None: temperature = self._device.settings[self.entity_description.key] if not isinstance(temperature, HonParameterRange): raise ValueError self._attr_max_temp = temperature.max self._attr_target_temperature_step = temperature.step self._attr_min_temp = temperature.min @callback def _handle_coordinator_update(self, update: bool = True) -> None: if update: self.async_write_ha_state()