Files
core/homeassistant/components/miele/climate.py
2025-11-14 09:10:24 +01:00

263 lines
8.6 KiB
Python

"""Platform for Miele integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, Final, cast
from aiohttp import ClientResponseError
from pymiele import MieleDevice, MieleTemperature
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DEVICE_TYPE_TAGS, DISABLED_TEMP_ENTITIES, DOMAIN, MieleAppliance
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity
PARALLEL_UPDATES = 1
_LOGGER = logging.getLogger(__name__)
def _get_temperature_value(
temperatures: list[MieleTemperature], index: int
) -> float | None:
"""Return the temperature value for the given index."""
if len(temperatures) > index:
return cast(int, temperatures[index].temperature) / 100.0
return None
@dataclass(frozen=True, kw_only=True)
class MieleClimateDescription(ClimateEntityDescription):
"""Class describing Miele climate entities."""
value_fn: Callable[[MieleDevice], StateType]
target_fn: Callable[[MieleDevice], StateType]
zone: int = 1
@dataclass
class MieleClimateDefinition:
"""Class for defining climate entities."""
types: tuple[MieleAppliance, ...]
description: MieleClimateDescription
CLIMATE_TYPES: Final[tuple[MieleClimateDefinition, ...]] = (
MieleClimateDefinition(
types=(
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.WINE_CABINET_FREEZER,
),
description=MieleClimateDescription(
key="thermostat",
value_fn=(
lambda value: _get_temperature_value(value.state_temperatures, 0)
),
target_fn=(
lambda value: _get_temperature_value(value.state_target_temperature, 0)
),
zone=1,
),
),
MieleClimateDefinition(
types=(
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.WINE_CABINET_FREEZER,
),
description=MieleClimateDescription(
key="thermostat2",
value_fn=(
lambda value: _get_temperature_value(value.state_temperatures, 1)
),
target_fn=(
lambda value: _get_temperature_value(value.state_target_temperature, 1)
),
translation_key="zone_2",
zone=2,
),
),
MieleClimateDefinition(
types=(
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.WINE_CABINET,
MieleAppliance.WINE_CONDITIONING_UNIT,
MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
MieleAppliance.WINE_CABINET_FREEZER,
),
description=MieleClimateDescription(
key="thermostat3",
value_fn=(
lambda value: _get_temperature_value(value.state_temperatures, 2)
),
target_fn=(
lambda value: _get_temperature_value(value.state_target_temperature, 2)
),
translation_key="zone_3",
zone=3,
),
),
)
ZONE1_DEVICES = {
MieleAppliance.FRIDGE: DEVICE_TYPE_TAGS[MieleAppliance.FRIDGE],
MieleAppliance.FRIDGE_FREEZER: DEVICE_TYPE_TAGS[MieleAppliance.FRIDGE],
MieleAppliance.FREEZER: DEVICE_TYPE_TAGS[MieleAppliance.FREEZER],
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: MieleConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the climate platform."""
coordinator = config_entry.runtime_data
added_devices: set[str] = set()
def _async_add_new_devices() -> None:
nonlocal added_devices
new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
added_devices = current_devices
async_add_entities(
MieleClimate(coordinator, device_id, definition.description)
for device_id, device in coordinator.data.devices.items()
for definition in CLIMATE_TYPES
if (
device_id in new_devices_set
and device.device_type in definition.types
and (
definition.description.value_fn(device)
not in DISABLED_TEMP_ENTITIES
)
)
)
config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
_async_add_new_devices()
class MieleClimate(MieleEntity, ClimateEntity):
"""Representation of a climate entity."""
entity_description: MieleClimateDescription
_attr_precision = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1.0
_attr_hvac_modes = [HVACMode.COOL]
_attr_hvac_mode = HVACMode.COOL
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return cast(float, self.entity_description.value_fn(self.device))
def __init__(
self,
coordinator: MieleDataUpdateCoordinator,
device_id: str,
description: MieleClimateDescription,
) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator, device_id, description)
t_key = self.entity_description.translation_key
if description.zone == 1:
t_key = ZONE1_DEVICES.get(
cast(MieleAppliance, self.device.device_type), "zone_1"
)
if self.device.device_type in (
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
):
self._attr_name = None
if description.zone == 2:
t_key = "zone_2"
if self.device.device_type in (
MieleAppliance.FRIDGE_FREEZER,
MieleAppliance.WINE_CABINET_FREEZER,
):
t_key = DEVICE_TYPE_TAGS[MieleAppliance.FREEZER]
elif description.zone == 3:
t_key = "zone_3"
self._attr_translation_key = t_key
self._attr_unique_id = f"{device_id}-{description.key}-{description.zone}"
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
return cast(float | None, self.entity_description.target_fn(self.device))
@property
def max_temp(self) -> float:
"""Return the maximum target temperature."""
if len(self.action.target_temperature) < self.entity_description.zone:
return super().max_temp
return cast(
float,
self.action.target_temperature[self.entity_description.zone - 1].max,
)
@property
def min_temp(self) -> float:
"""Return the minimum target temperature."""
if len(self.action.target_temperature) < self.entity_description.zone:
return super().min_temp
return cast(
float,
self.action.target_temperature[self.entity_description.zone - 1].min,
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
try:
await self.api.set_target_temperature(
self._device_id,
cast(float, kwargs.get(ATTR_TEMPERATURE)),
self.entity_description.zone,
)
except ClientResponseError as err:
_LOGGER.debug("Error setting climate state for %s: %s", self.entity_id, err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_state_error",
translation_placeholders={
"entity": self.entity_id,
},
) from err
await self.coordinator.async_request_refresh()