mirror of
https://github.com/home-assistant/core.git
synced 2026-09-08 14:41:38 +01:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
424 lines
15 KiB
Python
424 lines
15 KiB
Python
"""Provides climate entities for Home Connect."""
|
|
|
|
import logging
|
|
from typing import Any, cast, override
|
|
|
|
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey
|
|
from aiohomeconnect.model.error import HomeConnectError
|
|
from aiohomeconnect.model.program import Execution, ProgramDefinitionConstraints
|
|
|
|
from homeassistant.components.climate import (
|
|
FAN_AUTO,
|
|
ClimateEntity,
|
|
ClimateEntityDescription,
|
|
ClimateEntityFeature,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
|
|
from .common import setup_home_connect_entry
|
|
from .const import BSH_POWER_ON, BSH_POWER_STANDBY, DOMAIN, UNIT_MAP
|
|
from .coordinator import HomeConnectApplianceCoordinator, HomeConnectConfigEntry
|
|
from .entity import HomeConnectEntity
|
|
from .utils import get_dict_from_home_connect_error
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
HVAC_MODES_PROGRAMS_MAP = {
|
|
HVACMode.AUTO: (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_AUTO
|
|
),
|
|
HVACMode.COOL: (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_COOL
|
|
),
|
|
HVACMode.DRY: (ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_DRY),
|
|
HVACMode.FAN_ONLY: (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN
|
|
),
|
|
HVACMode.HEAT: (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_HEAT
|
|
),
|
|
}
|
|
|
|
PROGRAMS_HVAC_MODES_MAP = {v: k for k, v in HVAC_MODES_PROGRAMS_MAP.items()}
|
|
|
|
PRESET_MODES_PROGRAMS_MAP = {
|
|
"active_clean": (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_ACTIVE_CLEAN
|
|
),
|
|
}
|
|
PROGRAMS_PRESET_MODES_MAP = {v: k for k, v in PRESET_MODES_PROGRAMS_MAP.items()}
|
|
|
|
FAN_MODES_OPTIONS = {
|
|
FAN_AUTO: (
|
|
"HeatingVentilationAirConditioning"
|
|
".AirConditioner.EnumType.FanSpeedMode.Automatic"
|
|
),
|
|
"manual": (
|
|
"HeatingVentilationAirConditioning.AirConditioner.EnumType.FanSpeedMode.Manual"
|
|
),
|
|
}
|
|
|
|
FAN_MODES_OPTIONS_INVERTED = {v: k for k, v in FAN_MODES_OPTIONS.items()}
|
|
|
|
|
|
AIR_CONDITIONER_ENTITY_DESCRIPTION = ClimateEntityDescription(
|
|
key="air_conditioner",
|
|
translation_key="air_conditioner",
|
|
name=None,
|
|
)
|
|
|
|
|
|
def _get_entities_for_appliance(
|
|
appliance_coordinator: HomeConnectApplianceCoordinator,
|
|
) -> list[HomeConnectEntity]:
|
|
"""Get a list of entities."""
|
|
return (
|
|
[HomeConnectAirConditioningEntity(appliance_coordinator)]
|
|
if (programs := appliance_coordinator.data.programs)
|
|
and any(
|
|
program.key in PROGRAMS_HVAC_MODES_MAP
|
|
and (
|
|
program.constraints is None
|
|
or program.constraints.execution
|
|
in (Execution.SELECT_AND_START, Execution.START_ONLY)
|
|
)
|
|
for program in programs
|
|
)
|
|
else []
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: HomeConnectConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Home Connect climate entities."""
|
|
setup_home_connect_entry(
|
|
hass,
|
|
entry,
|
|
_get_entities_for_appliance,
|
|
async_add_entities,
|
|
)
|
|
|
|
|
|
class HomeConnectAirConditioningEntity(HomeConnectEntity, ClimateEntity):
|
|
"""Representation of a Home Connect climate entity."""
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: HomeConnectApplianceCoordinator,
|
|
) -> None:
|
|
"""Initialize the entity."""
|
|
super().__init__(
|
|
coordinator,
|
|
AIR_CONDITIONER_ENTITY_DESCRIPTION,
|
|
context_override=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def hvac_modes(self) -> list[HVACMode]:
|
|
"""Return the list of available hvac operation modes."""
|
|
hvac_modes = [
|
|
hvac_mode
|
|
for program in self.appliance.programs
|
|
if (hvac_mode := PROGRAMS_HVAC_MODES_MAP.get(program.key))
|
|
and (
|
|
program.constraints is None
|
|
or program.constraints.execution
|
|
in (Execution.SELECT_AND_START, Execution.START_ONLY)
|
|
)
|
|
]
|
|
if SettingKey.BSH_COMMON_POWER_STATE in self.appliance.settings:
|
|
hvac_modes.append(HVACMode.OFF)
|
|
return hvac_modes
|
|
|
|
@property
|
|
@override
|
|
def preset_modes(self) -> list[str] | None:
|
|
"""Return a list of available preset modes."""
|
|
active_clean = (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_ACTIVE_CLEAN
|
|
)
|
|
return (
|
|
[PROGRAMS_PRESET_MODES_MAP[active_clean]]
|
|
if any(program.key is active_clean for program in self.appliance.programs)
|
|
else None
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def target_temperature(self) -> float | None:
|
|
"""Return the temperature we try to reach."""
|
|
if event := self.appliance.events.get(
|
|
EventKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_OPTION_SETPOINT_TEMPERATURE
|
|
):
|
|
return cast(float, event.value)
|
|
return None
|
|
|
|
@property
|
|
@override
|
|
def temperature_unit(self) -> str:
|
|
"""Return the unit of measurement."""
|
|
if (
|
|
(
|
|
option_definition := self.appliance.options.get(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_SETPOINT_TEMPERATURE
|
|
)
|
|
)
|
|
and (proto_unit := option_definition.unit) is not None
|
|
and (unit := UNIT_MAP.get(proto_unit)) is not None
|
|
):
|
|
return unit
|
|
return UnitOfTemperature.CELSIUS
|
|
|
|
def _get_temperature_constraints(self) -> ProgramDefinitionConstraints | None:
|
|
"""Get the temperature constraints for the appliance."""
|
|
if option_definition := self.appliance.options.get(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_SETPOINT_TEMPERATURE
|
|
):
|
|
return option_definition.constraints
|
|
return None
|
|
|
|
@property
|
|
@override
|
|
def min_temp(self) -> float:
|
|
"""Return the minimum temperature."""
|
|
if (
|
|
option_constraints := self._get_temperature_constraints()
|
|
) and option_constraints.min is not None:
|
|
return option_constraints.min
|
|
return super().min_temp
|
|
|
|
@property
|
|
@override
|
|
def max_temp(self) -> float:
|
|
"""Return the maximum temperature."""
|
|
if (
|
|
option_constraints := self._get_temperature_constraints()
|
|
) and option_constraints.max is not None:
|
|
return option_constraints.max
|
|
return super().max_temp
|
|
|
|
@property
|
|
@override
|
|
def target_temperature_step(self) -> float | None:
|
|
"""Return the temperature step."""
|
|
if (
|
|
option_constraints := self._get_temperature_constraints()
|
|
) and option_constraints.step_size is not None:
|
|
return option_constraints.step_size
|
|
return None
|
|
|
|
@property
|
|
@override
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Return the list of supported features."""
|
|
features = ClimateEntityFeature(0)
|
|
if SettingKey.BSH_COMMON_POWER_STATE in self.appliance.settings:
|
|
features |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
|
if self.preset_modes:
|
|
features |= ClimateEntityFeature.PRESET_MODE
|
|
if self.appliance.options.get(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_SETPOINT_TEMPERATURE
|
|
):
|
|
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
|
if self.appliance.options.get(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN_SPEED_MODE
|
|
):
|
|
features |= ClimateEntityFeature.FAN_MODE
|
|
return features
|
|
|
|
@callback
|
|
def _handle_coordinator_update_fan_mode(self) -> None:
|
|
"""Handle updated data from the coordinator."""
|
|
self.async_write_ha_state()
|
|
_LOGGER.debug(
|
|
"Updated %s (fan mode), new state: %s", self.entity_id, self.fan_mode
|
|
)
|
|
|
|
@override
|
|
async def async_added_to_hass(self) -> None:
|
|
"""When entity is added to hass."""
|
|
await super().async_added_to_hass()
|
|
self.async_on_remove(
|
|
self.coordinator.async_add_listener(
|
|
self.async_write_ha_state,
|
|
EventKey.BSH_COMMON_APPLIANCE_CONNECTED,
|
|
)
|
|
)
|
|
self.async_on_remove(
|
|
self.coordinator.async_add_listener(
|
|
self._handle_coordinator_update_fan_mode,
|
|
EventKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_OPTION_FAN_SPEED_MODE,
|
|
)
|
|
)
|
|
self.async_on_remove(
|
|
self.coordinator.async_add_listener(
|
|
self._handle_coordinator_update,
|
|
EventKey.BSH_COMMON_SETTING_POWER_STATE,
|
|
)
|
|
)
|
|
self.async_on_remove(
|
|
self.coordinator.async_add_listener(
|
|
self._handle_coordinator_update,
|
|
EventKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_OPTION_SETPOINT_TEMPERATURE,
|
|
)
|
|
)
|
|
|
|
@override
|
|
def update_native_value(self) -> None:
|
|
"""Set the HVAC Mode and preset mode values."""
|
|
event = self.appliance.events.get(EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM)
|
|
program_key = cast(ProgramKey, event.value) if event else None
|
|
power_state = self.appliance.settings.get(SettingKey.BSH_COMMON_POWER_STATE)
|
|
active_clean = (
|
|
ProgramKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_ACTIVE_CLEAN
|
|
)
|
|
self._attr_hvac_mode = (
|
|
HVACMode.OFF
|
|
if power_state is not None and power_state.value != BSH_POWER_ON
|
|
else PROGRAMS_HVAC_MODES_MAP.get(program_key)
|
|
if program_key and program_key != active_clean
|
|
else None
|
|
)
|
|
self._attr_preset_mode = (
|
|
PROGRAMS_PRESET_MODES_MAP.get(program_key)
|
|
if program_key == active_clean
|
|
else None
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def fan_mode(self) -> str | None:
|
|
"""Return the fan setting."""
|
|
option_value = None
|
|
if event := self.appliance.events.get(
|
|
EventKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_OPTION_FAN_SPEED_MODE
|
|
):
|
|
option_value = event.value
|
|
return (
|
|
FAN_MODES_OPTIONS_INVERTED.get(cast(str, option_value))
|
|
if option_value is not None
|
|
else None
|
|
)
|
|
|
|
@property
|
|
@override
|
|
def fan_modes(self) -> list[str] | None:
|
|
"""Return the list of available fan modes."""
|
|
if (
|
|
(
|
|
option_definition := self.appliance.options.get(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN_SPEED_MODE
|
|
)
|
|
)
|
|
and (option_constraints := option_definition.constraints)
|
|
and option_constraints.allowed_values
|
|
):
|
|
return [
|
|
fan_mode
|
|
for fan_mode, api_value in FAN_MODES_OPTIONS.items()
|
|
if api_value in option_constraints.allowed_values
|
|
]
|
|
if option_definition:
|
|
# Then the constraints or the allowed values are not present
|
|
# So we stick to the default values
|
|
return list(FAN_MODES_OPTIONS.keys())
|
|
return None
|
|
|
|
@override
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
"""Switch the device on."""
|
|
try:
|
|
await self.coordinator.client.set_setting(
|
|
self.appliance.info.ha_id,
|
|
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
|
value=BSH_POWER_ON,
|
|
)
|
|
except HomeConnectError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="power_on",
|
|
translation_placeholders={
|
|
**get_dict_from_home_connect_error(err),
|
|
"appliance_name": self.appliance.info.name,
|
|
},
|
|
) from err
|
|
|
|
@override
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Switch the device off."""
|
|
try:
|
|
await self.coordinator.client.set_setting(
|
|
self.appliance.info.ha_id,
|
|
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
|
value=BSH_POWER_STANDBY,
|
|
)
|
|
except HomeConnectError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="power_off",
|
|
translation_placeholders={
|
|
**get_dict_from_home_connect_error(err),
|
|
"appliance_name": self.appliance.info.name,
|
|
"value": BSH_POWER_STANDBY,
|
|
},
|
|
) from err
|
|
|
|
async def _set_program(self, program_key: ProgramKey) -> None:
|
|
try:
|
|
await self.coordinator.client.start_program(
|
|
self.appliance.info.ha_id, program_key=program_key
|
|
)
|
|
except HomeConnectError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="start_program",
|
|
translation_placeholders={
|
|
**get_dict_from_home_connect_error(err),
|
|
"program": program_key.value,
|
|
},
|
|
) from err
|
|
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, self.state)
|
|
|
|
@override
|
|
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Set new target hvac mode."""
|
|
if hvac_mode is HVACMode.OFF:
|
|
await self.async_turn_off()
|
|
else:
|
|
await self._set_program(HVAC_MODES_PROGRAMS_MAP[hvac_mode])
|
|
|
|
@override
|
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Set new preset mode."""
|
|
await self._set_program(PRESET_MODES_PROGRAMS_MAP[preset_mode])
|
|
|
|
@override
|
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
"""Set new target temperature."""
|
|
if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
|
await self.async_set_option_with_key(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_SETPOINT_TEMPERATURE,
|
|
temp,
|
|
)
|
|
|
|
@override
|
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Set new target fan mode."""
|
|
await self.async_set_option_with_key(
|
|
OptionKey.HEATING_VENTILATION_AIR_CONDITIONING_AIR_CONDITIONER_FAN_SPEED_MODE,
|
|
FAN_MODES_OPTIONS[fan_mode],
|
|
)
|
|
_LOGGER.debug(
|
|
"Updated %s's speed mode option, new state: %s", self.entity_id, self.state
|
|
)
|