Markus Adrario 426e9846d9
Add Homee climate platform (#141616)
* Add climate platform

* Add climate tests

* Add service tests

* Add snapshot test

* Code optimazitions 1

* Add test for current preset mode.

* code optimization 2

* code optimization 3

* small tweaks

* another small tweak

* Last minute changes

* Update tests/components/homee/test_climate.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fix review comments

* typo

* more review fixes.

* maybe final review fixes.

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-01 18:08:36 +02:00

201 lines
6.7 KiB
Python

"""The Homee climate platform."""
from typing import Any
from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeNode
from homeassistant.components.climate import (
ATTR_TEMPERATURE,
PRESET_BOOST,
PRESET_ECO,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from .const import CLIMATE_PROFILES, DOMAIN, HOMEE_UNIT_TO_HA_UNIT, PRESET_MANUAL
from .entity import HomeeNodeEntity
PARALLEL_UPDATES = 0
ROOM_THERMOSTATS = {
NodeProfile.ROOM_THERMOSTAT,
NodeProfile.ROOM_THERMOSTAT_WITH_HUMIDITY_SENSOR,
NodeProfile.WIFI_ROOM_THERMOSTAT,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the climate component."""
async_add_devices(
HomeeClimate(node, config_entry)
for node in config_entry.runtime_data.nodes
if node.profile in CLIMATE_PROFILES
)
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
"""Representation of a Homee climate entity."""
_attr_name = None
_attr_translation_key = DOMAIN
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
"""Initialize a Homee climate entity."""
super().__init__(node, entry)
(
self._attr_supported_features,
self._attr_hvac_modes,
self._attr_preset_modes,
) = get_climate_features(self._node)
self._target_temp = self._node.get_attribute_by_type(
AttributeType.TARGET_TEMPERATURE
)
assert self._target_temp is not None
self._attr_temperature_unit = str(HOMEE_UNIT_TO_HA_UNIT[self._target_temp.unit])
self._attr_target_temperature_step = self._target_temp.step_value
self._attr_unique_id = f"{self._attr_unique_id}-{self._target_temp.id}"
self._heating_mode = self._node.get_attribute_by_type(
AttributeType.HEATING_MODE
)
self._temperature = self._node.get_attribute_by_type(AttributeType.TEMPERATURE)
self._valve_position = self._node.get_attribute_by_type(
AttributeType.CURRENT_VALVE_POSITION
)
@property
def hvac_mode(self) -> HVACMode:
"""Return the hvac operation mode."""
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
self._heating_mode is not None
):
if self._heating_mode.current_value == 0:
return HVACMode.OFF
return HVACMode.HEAT
@property
def hvac_action(self) -> HVACAction:
"""Return the hvac action."""
if self._heating_mode is not None and self._heating_mode.current_value == 0:
return HVACAction.OFF
if (
self._valve_position is not None and self._valve_position.current_value == 0
) or (
self._temperature is not None
and self._temperature.current_value >= self.target_temperature
):
return HVACAction.IDLE
return HVACAction.HEATING
@property
def preset_mode(self) -> str:
"""Return the present preset mode."""
if (
ClimateEntityFeature.PRESET_MODE in self.supported_features
and self._heating_mode is not None
and self._heating_mode.current_value > 0
):
assert self._attr_preset_modes is not None
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
return PRESET_NONE
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self._temperature is not None:
return self._temperature.current_value
return None
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
assert self._target_temp is not None
return self._target_temp.current_value
@property
def min_temp(self) -> float:
"""Return the lowest settable target temperature."""
assert self._target_temp is not None
return self._target_temp.minimum
@property
def max_temp(self) -> float:
"""Return the lowest settable target temperature."""
assert self._target_temp is not None
return self._target_temp.maximum
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
# Currently only HEAT and OFF are supported.
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
assert self._heating_mode is not None and self._attr_preset_modes is not None
await self.async_set_homee_value(
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
assert self._target_temp is not None
if ATTR_TEMPERATURE in kwargs:
await self.async_set_homee_value(
self._target_temp, kwargs[ATTR_TEMPERATURE]
)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 1)
async def async_turn_off(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 0)
def get_climate_features(
node: HomeeNode,
) -> tuple[ClimateEntityFeature, list[HVACMode], list[str] | None]:
"""Determine supported climate features of a node based on the available attributes."""
features = ClimateEntityFeature.TARGET_TEMPERATURE
hvac_modes = [HVACMode.HEAT]
preset_modes: list[str] = []
if (
attribute := node.get_attribute_by_type(AttributeType.HEATING_MODE)
) is not None:
features |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
hvac_modes.append(HVACMode.OFF)
if attribute.maximum > 1:
# Node supports more modes than off and heating.
features |= ClimateEntityFeature.PRESET_MODE
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
if len(preset_modes) > 0:
preset_modes.insert(0, PRESET_NONE)
return (features, hvac_modes, preset_modes if len(preset_modes) > 0 else None)