diff --git a/.coveragerc b/.coveragerc index 511f30c507e8c467ce31f99b4e5c3a73c68d6e3c..c05cab27f79ca19b1e80d4ebbe14d56dd39f39d9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -182,6 +182,7 @@ omit = homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comelit/__init__.py homeassistant/components/comelit/alarm_control_panel.py + homeassistant/components/comelit/climate.py homeassistant/components/comelit/const.py homeassistant/components/comelit/cover.py homeassistant/components/comelit/coordinator.py diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py index c51081196c975fed5f08e85bacd3e03e53a56a51..06db68a2444f093572fac5f1f65c1b906ba58d25 100644 --- a/homeassistant/components/comelit/__init__.py +++ b/homeassistant/components/comelit/__init__.py @@ -11,6 +11,7 @@ from .const import DEFAULT_PORT, DOMAIN from .coordinator import ComelitBaseCoordinator, ComelitSerialBridge, ComelitVedoSystem BRIDGE_PLATFORMS = [ + Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SENSOR, diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py new file mode 100644 index 0000000000000000000000000000000000000000..6f45968be4ffa62f7e68ed583a1ac79b4cb877f2 --- /dev/null +++ b/homeassistant/components/comelit/climate.py @@ -0,0 +1,206 @@ +"""Support for climates.""" +from __future__ import annotations + +import asyncio +from enum import StrEnum +from typing import Any + +from aiocomelit import ComelitSerialBridgeObject +from aiocomelit.const import CLIMATE, SLEEP_BETWEEN_CALLS + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, + UnitOfTemperature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import _LOGGER, DOMAIN +from .coordinator import ComelitSerialBridge + + +class ClimaMode(StrEnum): + """Serial Bridge clima modes.""" + + AUTO = "A" + OFF = "O" + LOWER = "L" + UPPER = "U" + + +class ClimaAction(StrEnum): + """Serial Bridge clima actions.""" + + OFF = "off" + ON = "on" + MANUAL = "man" + SET = "set" + AUTO = "auto" + + +API_STATUS: dict[str, dict[str, Any]] = { + ClimaMode.OFF: { + "action": "off", + "hvac_mode": HVACMode.OFF, + "hvac_action": HVACAction.OFF, + }, + ClimaMode.LOWER: { + "action": "lower", + "hvac_mode": HVACMode.COOL, + "hvac_action": HVACAction.COOLING, + }, + ClimaMode.UPPER: { + "action": "upper", + "hvac_mode": HVACMode.HEAT, + "hvac_action": HVACAction.HEATING, + }, +} + +MODE_TO_ACTION: dict[HVACMode, ClimaAction] = { + HVACMode.OFF: ClimaAction.OFF, + HVACMode.AUTO: ClimaAction.AUTO, + HVACMode.COOL: ClimaAction.MANUAL, + HVACMode.HEAT: ClimaAction.MANUAL, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Comelit climates.""" + + coordinator: ComelitSerialBridge = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + ComelitClimateEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[CLIMATE].values() + ) + + +class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity): + """Climate device.""" + + _attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] + _attr_max_temp = 30 + _attr_min_temp = 5 + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_TENTHS + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + coordinator: ComelitSerialBridge, + device: ComelitSerialBridgeObject, + config_entry_entry_id: str, + ) -> None: + """Init light entity.""" + self._api = coordinator.api + self._device = device + super().__init__(coordinator) + # Use config_entry.entry_id as base for unique_id + # because no serial number or mac is available + self._attr_unique_id = f"{config_entry_entry_id}-{device.index}" + self._attr_device_info = coordinator.platform_device_info(device, device.type) + + @property + def _clima(self) -> list[Any]: + """Return clima device data.""" + # CLIMATE has 2 turple: + # - first for Clima + # - second for Humidifier + return self.coordinator.data[CLIMATE][self._device.index].val[0] + + @property + def _api_mode(self) -> str: + """Return device mode.""" + # Values from API: "O", "L", "U" + return self._clima[2] + + @property + def _api_active(self) -> bool: + "Return device active/idle." + return self._clima[1] + + @property + def _api_automatic(self) -> bool: + """Return device in automatic/manual mode.""" + return self._clima[3] == ClimaMode.AUTO + + @property + def target_temperature(self) -> float: + """Set target temperature.""" + return self._clima[4] / 10 + + @property + def current_temperature(self) -> float: + """Return current temperature.""" + return self._clima[0] / 10 + + @property + def hvac_mode(self) -> HVACMode | None: + """HVAC current mode.""" + + if self._api_mode == ClimaMode.OFF: + return HVACMode.OFF + + if self._api_automatic: + return HVACMode.AUTO + + if self._api_mode in API_STATUS: + return API_STATUS[self._api_mode]["hvac_mode"] + + _LOGGER.warning("Unknown API mode '%s' in hvac_mode", self._api_mode) + return None + + @property + def hvac_action(self) -> HVACAction | None: + """HVAC current action.""" + + if self._api_mode == ClimaMode.OFF: + return HVACAction.OFF + + if not self._api_active: + return HVACAction.IDLE + + if self._api_mode in API_STATUS: + return API_STATUS[self._api_mode]["hvac_action"] + + _LOGGER.warning("Unknown API mode '%s' in hvac_action", self._api_mode) + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if ( + target_temp := kwargs.get(ATTR_TEMPERATURE) + ) is None or self.hvac_mode == HVACMode.OFF: + return + + await self.coordinator.api.set_clima_status( + self._device.index, ClimaAction.MANUAL + ) + await asyncio.sleep(SLEEP_BETWEEN_CALLS) + await self.coordinator.api.set_clima_status( + self._device.index, ClimaAction.SET, target_temp + ) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + + if hvac_mode != HVACMode.OFF: + await self.coordinator.api.set_clima_status( + self._device.index, ClimaAction.ON + ) + await asyncio.sleep(SLEEP_BETWEEN_CALLS) + await self.coordinator.api.set_clima_status( + self._device.index, MODE_TO_ACTION[hvac_mode] + )