From 5c124e5fd287dccb2bcd68f4cf9bd8f5b664caf0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli <simone.chemelli@gmail.com> Date: Wed, 28 Feb 2024 14:15:44 +0100 Subject: [PATCH] Add Comelit humidifier/dehumidifier (#111203) * Add Comelit humidifier/dehumidifier * optimize turn_on * fix entity naming * raise ServiceValidationError * apply review comment * apply review comments * rename HumidifierComelitAction --- .coveragerc | 1 + homeassistant/components/comelit/__init__.py | 1 + .../components/comelit/humidifier.py | 212 ++++++++++++++++++ .../components/comelit/manifest.json | 2 +- homeassistant/components/comelit/strings.json | 11 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/comelit/humidifier.py diff --git a/.coveragerc b/.coveragerc index 45b1acefbba..378532dfd88 100644 --- a/.coveragerc +++ b/.coveragerc @@ -192,6 +192,7 @@ omit = homeassistant/components/comelit/const.py homeassistant/components/comelit/cover.py homeassistant/components/comelit/coordinator.py + homeassistant/components/comelit/humidifier.py homeassistant/components/comelit/light.py homeassistant/components/comelit/sensor.py homeassistant/components/comelit/switch.py diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py index 06db68a2444..2cf7a145eee 100644 --- a/homeassistant/components/comelit/__init__.py +++ b/homeassistant/components/comelit/__init__.py @@ -13,6 +13,7 @@ from .coordinator import ComelitBaseCoordinator, ComelitSerialBridge, ComelitVed BRIDGE_PLATFORMS = [ Platform.CLIMATE, Platform.COVER, + Platform.HUMIDIFIER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/comelit/humidifier.py b/homeassistant/components/comelit/humidifier.py new file mode 100644 index 00000000000..8ec2e9fd28b --- /dev/null +++ b/homeassistant/components/comelit/humidifier.py @@ -0,0 +1,212 @@ +"""Support for humidifiers.""" +from __future__ import annotations + +from enum import StrEnum +from typing import Any + +from aiocomelit import ComelitSerialBridgeObject +from aiocomelit.const import CLIMATE + +from homeassistant.components.humidifier import ( + MODE_AUTO, + MODE_NORMAL, + HumidifierAction, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ComelitSerialBridge + + +class HumidifierComelitMode(StrEnum): + """Serial Bridge humidifier modes.""" + + AUTO = "A" + OFF = "O" + LOWER = "L" + UPPER = "U" + + +class HumidifierComelitCommand(StrEnum): + """Serial Bridge humidifier commands.""" + + OFF = "off" + ON = "on" + MANUAL = "man" + SET = "set" + AUTO = "auto" + LOWER = "lower" + UPPER = "upper" + + +MODE_TO_ACTION: dict[str, HumidifierComelitCommand] = { + MODE_AUTO: HumidifierComelitCommand.AUTO, + MODE_NORMAL: HumidifierComelitCommand.MANUAL, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Comelit humidifiers.""" + + coordinator: ComelitSerialBridge = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[ComelitHumidifierEntity] = [] + for device in coordinator.data[CLIMATE].values(): + entities.append( + ComelitHumidifierEntity( + coordinator, + device, + config_entry.entry_id, + active_mode=HumidifierComelitMode.LOWER, + active_action=HumidifierAction.DRYING, + set_command=HumidifierComelitCommand.LOWER, + device_class=HumidifierDeviceClass.DEHUMIDIFIER, + ) + ) + entities.append( + ComelitHumidifierEntity( + coordinator, + device, + config_entry.entry_id, + active_mode=HumidifierComelitMode.UPPER, + active_action=HumidifierAction.HUMIDIFYING, + set_command=HumidifierComelitCommand.UPPER, + device_class=HumidifierDeviceClass.HUMIDIFIER, + ), + ) + + async_add_entities(entities) + + +class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], HumidifierEntity): + """Humidifier device.""" + + _attr_supported_features = HumidifierEntityFeature.MODES + _attr_available_modes = [MODE_NORMAL, MODE_AUTO] + _attr_min_humidity = 10 + _attr_max_humidity = 90 + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ComelitSerialBridge, + device: ComelitSerialBridgeObject, + config_entry_entry_id: str, + active_mode: HumidifierComelitMode, + active_action: HumidifierAction, + set_command: HumidifierComelitCommand, + device_class: HumidifierDeviceClass, + ) -> 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}-{device_class}" + self._attr_device_info = coordinator.platform_device_info(device, device_class) + self._attr_device_class = device_class + self._attr_translation_key = device_class.value + self._active_mode = active_mode + self._active_action = active_action + self._set_command = set_command + + @property + def _humidifier(self) -> list[Any]: + """Return humidifier device data.""" + # CLIMATE has a 2 item tuple: + # - first for Clima + # - second for Humidifier + return self.coordinator.data[CLIMATE][self._device.index].val[1] + + @property + def _api_mode(self) -> str: + """Return device mode.""" + # Values from API: "O", "L", "U" + return self._humidifier[2] + + @property + def _api_active(self) -> bool: + "Return device active/idle." + return self._humidifier[1] + + @property + def _api_automatic(self) -> bool: + """Return device in automatic/manual mode.""" + return self._humidifier[3] == HumidifierComelitMode.AUTO + + @property + def target_humidity(self) -> int: + """Set target humidity.""" + return self._humidifier[4] / 10 + + @property + def current_humidity(self) -> int: + """Return current humidity.""" + return self._humidifier[0] / 10 + + @property + def is_on(self) -> bool | None: + """Return true is humidifier is on.""" + return self._api_mode == self._active_mode + + @property + def mode(self) -> str | None: + """Return current mode.""" + return MODE_AUTO if self._api_automatic else MODE_NORMAL + + @property + def action(self) -> HumidifierAction | None: + """Return current action.""" + + if self._api_mode == HumidifierComelitMode.OFF: + return HumidifierAction.OFF + + if self._api_active and self._api_mode == self._active_mode: + return self._active_action + + return HumidifierAction.IDLE + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + if self.mode == HumidifierComelitMode.OFF: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="humidity_while_off", + ) + + await self.coordinator.api.set_humidity_status( + self._device.index, HumidifierComelitCommand.MANUAL + ) + await self.coordinator.api.set_humidity_status( + self._device.index, HumidifierComelitCommand.SET, humidity + ) + + async def async_set_mode(self, mode: str) -> None: + """Set humidifier mode.""" + await self.coordinator.api.set_humidity_status( + self._device.index, MODE_TO_ACTION[mode] + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on.""" + await self.coordinator.api.set_humidity_status( + self._device.index, self._set_command + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off.""" + await self.coordinator.api.set_humidity_status( + self._device.index, HumidifierComelitCommand.OFF + ) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index bbbb4efe7d6..d93ec349bba 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.8.3"] + "requirements": ["aiocomelit==0.9.0"] } diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index dac8bc4123d..14d947c7323 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -46,7 +46,18 @@ "rest": "Rest", "sabotated": "Sabotated" } + }, + "humidifier": { + "name": "Humidifier" + }, + "dehumidifier": { + "name": "Dehumidifier" } } + }, + "exceptions": { + "humidity_while_off": { + "message": "Cannot change humidity while off" + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 5faef8985c0..4bf1eb21d0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiobafi6==0.9.0 aiobotocore==2.9.1 # homeassistant.components.comelit -aiocomelit==0.8.3 +aiocomelit==0.9.0 # homeassistant.components.dhcp aiodhcpwatcher==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7dcfa396b..60fd4b0baab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ aiobafi6==0.9.0 aiobotocore==2.9.1 # homeassistant.components.comelit -aiocomelit==0.8.3 +aiocomelit==0.9.0 # homeassistant.components.dhcp aiodhcpwatcher==0.8.0 -- GitLab