"""AirTouch 5 component to control AirTouch 5 Climate Devices.""" import logging from typing import Any from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient from airtouch5py.packets.ac_ability import AcAbility from airtouch5py.packets.ac_control import ( AcControl, SetAcFanSpeed, SetAcMode, SetpointControl, SetPowerSetting, ) from airtouch5py.packets.ac_status import AcFanSpeed, AcMode, AcPowerState, AcStatus from airtouch5py.packets.zone_control import ( ZoneControlZone, ZoneSettingPower, ZoneSettingValue, ) from airtouch5py.packets.zone_name import ZoneName from airtouch5py.packets.zone_status import ZonePowerState, ZoneStatusZone from homeassistant.components.climate import ( FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, FAN_HIGH, FAN_LOW, FAN_MEDIUM, PRESET_BOOST, PRESET_NONE, ClimateEntity, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO from .entity import Airtouch5Entity _LOGGER = logging.getLogger(__name__) AC_MODE_TO_HVAC_MODE = { AcMode.AUTO: HVACMode.AUTO, AcMode.AUTO_COOL: HVACMode.AUTO, AcMode.AUTO_HEAT: HVACMode.AUTO, AcMode.COOL: HVACMode.COOL, AcMode.DRY: HVACMode.DRY, AcMode.FAN: HVACMode.FAN_ONLY, AcMode.HEAT: HVACMode.HEAT, } HVAC_MODE_TO_SET_AC_MODE = { HVACMode.AUTO: SetAcMode.SET_TO_AUTO, HVACMode.COOL: SetAcMode.SET_TO_COOL, HVACMode.DRY: SetAcMode.SET_TO_DRY, HVACMode.FAN_ONLY: SetAcMode.SET_TO_FAN, HVACMode.HEAT: SetAcMode.SET_TO_HEAT, } AC_FAN_SPEED_TO_FAN_SPEED = { AcFanSpeed.AUTO: FAN_AUTO, AcFanSpeed.QUIET: FAN_DIFFUSE, AcFanSpeed.LOW: FAN_LOW, AcFanSpeed.MEDIUM: FAN_MEDIUM, AcFanSpeed.HIGH: FAN_HIGH, AcFanSpeed.POWERFUL: FAN_FOCUS, AcFanSpeed.TURBO: FAN_TURBO, AcFanSpeed.INTELLIGENT_AUTO_1: FAN_INTELLIGENT_AUTO, AcFanSpeed.INTELLIGENT_AUTO_2: FAN_INTELLIGENT_AUTO, AcFanSpeed.INTELLIGENT_AUTO_3: FAN_INTELLIGENT_AUTO, AcFanSpeed.INTELLIGENT_AUTO_4: FAN_INTELLIGENT_AUTO, AcFanSpeed.INTELLIGENT_AUTO_5: FAN_INTELLIGENT_AUTO, AcFanSpeed.INTELLIGENT_AUTO_6: FAN_INTELLIGENT_AUTO, } FAN_MODE_TO_SET_AC_FAN_SPEED = { FAN_AUTO: SetAcFanSpeed.SET_TO_AUTO, FAN_DIFFUSE: SetAcFanSpeed.SET_TO_QUIET, FAN_LOW: SetAcFanSpeed.SET_TO_LOW, FAN_MEDIUM: SetAcFanSpeed.SET_TO_MEDIUM, FAN_HIGH: SetAcFanSpeed.SET_TO_HIGH, FAN_FOCUS: SetAcFanSpeed.SET_TO_POWERFUL, FAN_TURBO: SetAcFanSpeed.SET_TO_TURBO, FAN_INTELLIGENT_AUTO: SetAcFanSpeed.SET_TO_INTELLIGENT_AUTO, } async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Airtouch 5 Climate entities.""" client: Airtouch5SimpleClient = hass.data[DOMAIN][config_entry.entry_id] entities: list[ClimateEntity] = [] # Add each AC (and remember what zones they apply to). # Each zone is controlled by a single AC zone_to_ac: dict[int, AcAbility] = {} for ac in client.ac: for i in range(ac.start_zone_number, ac.start_zone_number + ac.zone_count): zone_to_ac[i] = ac entities.append(Airtouch5AC(client, ac)) # Add each zone entities.extend( Airtouch5Zone(client, zone, zone_to_ac[zone.zone_number]) for zone in client.zones ) async_add_entities(entities) class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity): """Base class for Airtouch5 Climate Entities.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 _attr_name = None _enable_turn_on_off_backwards_compatibility = False class Airtouch5AC(Airtouch5ClimateEntity): """Representation of the AC unit. Used to control the overall HVAC Mode.""" def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None: """Initialise the Climate Entity.""" super().__init__(client) self._ability = ability self._attr_unique_id = f"ac_{ability.ac_number}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"ac_{ability.ac_number}")}, name=f"AC {ability.ac_number}", manufacturer="Polyaire", model="AirTouch 5", ) self._attr_hvac_modes = [HVACMode.OFF] if ability.supports_mode_auto: self._attr_hvac_modes.append(HVACMode.AUTO) if ability.supports_mode_cool: self._attr_hvac_modes.append(HVACMode.COOL) if ability.supports_mode_dry: self._attr_hvac_modes.append(HVACMode.DRY) if ability.supports_mode_fan: self._attr_hvac_modes.append(HVACMode.FAN_ONLY) if ability.supports_mode_heat: self._attr_hvac_modes.append(HVACMode.HEAT) self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) if len(self.hvac_modes) > 1: self._attr_supported_features |= ( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) self._attr_fan_modes = [] if ability.supports_fan_speed_quiet: self._attr_fan_modes.append(FAN_DIFFUSE) if ability.supports_fan_speed_low: self._attr_fan_modes.append(FAN_LOW) if ability.supports_fan_speed_medium: self._attr_fan_modes.append(FAN_MEDIUM) if ability.supports_fan_speed_high: self._attr_fan_modes.append(FAN_HIGH) if ability.supports_fan_speed_powerful: self._attr_fan_modes.append(FAN_FOCUS) if ability.supports_fan_speed_turbo: self._attr_fan_modes.append(FAN_TURBO) if ability.supports_fan_speed_auto: self._attr_fan_modes.append(FAN_AUTO) if ability.supports_fan_speed_intelligent_auto: self._attr_fan_modes.append(FAN_INTELLIGENT_AUTO) # We can have different setpoints for heat cool, we expose the lowest low and highest high self._attr_min_temp = min( ability.min_cool_set_point, ability.min_heat_set_point ) self._attr_max_temp = max( ability.max_cool_set_point, ability.max_heat_set_point ) @callback def _async_update_attrs(self, data: dict[int, AcStatus]) -> None: if self._ability.ac_number not in data: return status = data[self._ability.ac_number] self._attr_current_temperature = status.temperature self._attr_target_temperature = status.ac_setpoint if status.ac_power_state in [AcPowerState.OFF, AcPowerState.AWAY_OFF]: self._attr_hvac_mode = HVACMode.OFF else: self._attr_hvac_mode = AC_MODE_TO_HVAC_MODE[status.ac_mode] self._attr_fan_mode = AC_FAN_SPEED_TO_FAN_SPEED[status.ac_fan_speed] self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Add data updated listener after this object has been initialized.""" await super().async_added_to_hass() self._client.ac_status_callbacks.append(self._async_update_attrs) self._async_update_attrs(self._client.latest_ac_status) async def async_will_remove_from_hass(self) -> None: """Remove data updated listener after this object has been initialized.""" await super().async_will_remove_from_hass() self._client.ac_status_callbacks.remove(self._async_update_attrs) async def _control( self, *, power: SetPowerSetting = SetPowerSetting.KEEP_POWER_SETTING, ac_mode: SetAcMode = SetAcMode.KEEP_AC_MODE, fan: SetAcFanSpeed = SetAcFanSpeed.KEEP_AC_FAN_SPEED, setpoint: SetpointControl = SetpointControl.KEEP_SETPOINT_VALUE, temp: int = 0, ) -> None: control = AcControl( power, self._ability.ac_number, ac_mode, fan, setpoint, temp, ) packet = self._client.data_packet_factory.ac_control([control]) await self._client.send_packet(packet) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" set_power_setting: SetPowerSetting set_ac_mode: SetAcMode if hvac_mode == HVACMode.OFF: set_power_setting = SetPowerSetting.SET_TO_OFF set_ac_mode = SetAcMode.KEEP_AC_MODE else: set_power_setting = SetPowerSetting.SET_TO_ON if hvac_mode not in HVAC_MODE_TO_SET_AC_MODE: raise ValueError(f"Unsupported hvac mode: {hvac_mode}") set_ac_mode = HVAC_MODE_TO_SET_AC_MODE[hvac_mode] await self._control(power=set_power_setting, ac_mode=set_ac_mode) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if fan_mode not in FAN_MODE_TO_SET_AC_FAN_SPEED: raise ValueError(f"Unsupported fan mode: {fan_mode}") fan_speed = FAN_MODE_TO_SET_AC_FAN_SPEED[fan_mode] await self._control(fan=fan_speed) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: _LOGGER.debug("Argument `temperature` is missing in set_temperature") return await self._control(temp=temp) class Airtouch5Zone(Airtouch5ClimateEntity): """Representation of a Zone. Used to control the AC effect in the zone.""" _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] _attr_preset_modes = [PRESET_NONE, PRESET_BOOST] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) def __init__( self, client: Airtouch5SimpleClient, name: ZoneName, ac: AcAbility ) -> None: """Initialise the Climate Entity.""" super().__init__(client) self._name = name self._attr_unique_id = f"zone_{name.zone_number}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"zone_{name.zone_number}")}, name=name.zone_name, manufacturer="Polyaire", model="AirTouch 5", ) # We can have different setpoints for heat and cool, we expose the lowest low and highest high self._attr_min_temp = min(ac.min_cool_set_point, ac.min_heat_set_point) self._attr_max_temp = max(ac.max_cool_set_point, ac.max_heat_set_point) @callback def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None: if self._name.zone_number not in data: return status = data[self._name.zone_number] self._attr_current_temperature = status.temperature self._attr_target_temperature = status.set_point if status.zone_power_state == ZonePowerState.OFF: self._attr_hvac_mode = HVACMode.OFF self._attr_preset_mode = PRESET_NONE elif status.zone_power_state == ZonePowerState.ON: self._attr_hvac_mode = HVACMode.FAN_ONLY self._attr_preset_mode = PRESET_NONE elif status.zone_power_state == ZonePowerState.TURBO: self._attr_hvac_mode = HVACMode.FAN_ONLY self._attr_preset_mode = PRESET_BOOST else: self._attr_hvac_mode = None self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Add data updated listener after this object has been initialized.""" await super().async_added_to_hass() self._client.zone_status_callbacks.append(self._async_update_attrs) self._async_update_attrs(self._client.latest_zone_status) async def async_will_remove_from_hass(self) -> None: """Remove data updated listener after this object has been initialized.""" await super().async_will_remove_from_hass() self._client.zone_status_callbacks.remove(self._async_update_attrs) async def _control( self, *, zsv: ZoneSettingValue = ZoneSettingValue.KEEP_SETTING_VALUE, power: ZoneSettingPower = ZoneSettingPower.KEEP_POWER_STATE, value: float = 0, ) -> None: control = ZoneControlZone(self._name.zone_number, zsv, power, value) packet = self._client.data_packet_factory.zone_control([control]) await self._client.send_packet(packet) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" power: ZoneSettingPower if hvac_mode is HVACMode.OFF: power = ZoneSettingPower.SET_TO_OFF elif self._attr_preset_mode is PRESET_BOOST: power = ZoneSettingPower.SET_TO_TURBO else: power = ZoneSettingPower.SET_TO_ON await self._control(power=power) async def async_set_preset_mode(self, preset_mode: str) -> None: """Enable or disable Turbo. Done this way as we can't have a turbo HVACMode.""" power: ZoneSettingPower if preset_mode == PRESET_BOOST: power = ZoneSettingPower.SET_TO_TURBO else: power = ZoneSettingPower.SET_TO_ON await self._control(power=power) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: _LOGGER.debug("Argument `temperature` is missing in set_temperature") return await self._control( zsv=ZoneSettingValue.SET_TARGET_SETPOINT, value=float(temp), ) async def async_turn_on(self) -> None: """Turn the zone on.""" await self.async_set_hvac_mode(HVACMode.FAN_ONLY) async def async_turn_off(self) -> None: """Turn the zone off.""" await self.async_set_hvac_mode(HVACMode.OFF)