core/homeassistant/components/airtouch5/climate.py

384 lines
14 KiB
Python

"""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.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 . import Airtouch5ConfigEntry
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: Airtouch5ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airtouch 5 Climate entities."""
client = config_entry.runtime_data
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_translation_key = DOMAIN
_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(setpoint=SetpointControl.CHANGE_SETPOINT, 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)