From 64f997718a44e270c013f209957ba5af62b2273e Mon Sep 17 00:00:00 2001 From: Luke Wale Date: Sat, 27 Jul 2024 18:36:48 +0800 Subject: [PATCH] Add AirTouch5 cover (#122462) * AirTouch5 - add cover Each zone has a damper that can be controlled as a cover. * remove unused assignment * remove opinionated feature support * Revert "remove unused assignment" This reverts commit b4205a60a22ae3869436229b4a45547348496d39. * ruff formatting changes * git push translation and refactor --- .../components/airtouch5/__init__.py | 2 +- homeassistant/components/airtouch5/climate.py | 1 + homeassistant/components/airtouch5/cover.py | 134 ++++++++++++++++++ homeassistant/components/airtouch5/entity.py | 3 - .../components/airtouch5/strings.json | 5 + 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/airtouch5/cover.py diff --git a/homeassistant/components/airtouch5/__init__.py b/homeassistant/components/airtouch5/__init__.py index 1931098282d..8aab41d72cb 100644 --- a/homeassistant/components/airtouch5/__init__.py +++ b/homeassistant/components/airtouch5/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.COVER] type Airtouch5ConfigEntry = ConfigEntry[Airtouch5SimpleClient] diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index 1f97c254efe..2d5740b1837 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -121,6 +121,7 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity): """Base class for Airtouch5 Climate Entities.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = DOMAIN _attr_target_temperature_step = 1 _attr_name = None _enable_turn_on_off_backwards_compatibility = False diff --git a/homeassistant/components/airtouch5/cover.py b/homeassistant/components/airtouch5/cover.py new file mode 100644 index 00000000000..62cf7938fc2 --- /dev/null +++ b/homeassistant/components/airtouch5/cover.py @@ -0,0 +1,134 @@ +"""Representation of the Damper for AirTouch 5 Devices.""" + +import logging +from typing import Any + +from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient +from airtouch5py.packets.zone_control import ( + ZoneControlZone, + ZoneSettingPower, + ZoneSettingValue, +) +from airtouch5py.packets.zone_name import ZoneName +from airtouch5py.packets.zone_status import ZoneStatusZone + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import Airtouch5ConfigEntry +from .const import DOMAIN +from .entity import Airtouch5Entity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: Airtouch5ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Airtouch 5 Cover entities.""" + client = config_entry.runtime_data + + # Each zone has a cover for its open percentage + async_add_entities( + Airtouch5ZoneOpenPercentage( + client, zone, client.latest_zone_status[zone.zone_number].has_sensor + ) + for zone in client.zones + ) + + +class Airtouch5ZoneOpenPercentage(CoverEntity, Airtouch5Entity): + """How open the damper is in each zone.""" + + _attr_device_class = CoverDeviceClass.DAMPER + _attr_translation_key = "damper" + + # Zones with temperature sensors shouldn't be manually controlled. + # We allow it but warn the user in the integration documentation. + _attr_supported_features = ( + CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + ) + + def __init__( + self, client: Airtouch5SimpleClient, zone_name: ZoneName, has_sensor: bool + ) -> None: + """Initialise the Cover Entity.""" + super().__init__(client) + self._zone_name = zone_name + + self._attr_unique_id = f"zone_{zone_name.zone_number}_open_percentage" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"zone_{zone_name.zone_number}")}, + name=zone_name.zone_name, + manufacturer="Polyaire", + model="AirTouch 5", + ) + + @callback + def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None: + if self._zone_name.zone_number not in data: + return + status = data[self._zone_name.zone_number] + + self._attr_current_cover_position = int(status.open_percentage * 100) + if status.open_percentage == 0: + self._attr_is_closed = True + else: + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Add data updated listener after this object has been initialized.""" + await super().async_added_to_hass() + self._client.zone_status_callbacks.append(self._async_update_attrs) + self._async_update_attrs(self._client.latest_zone_status) + + async def async_will_remove_from_hass(self) -> None: + """Remove data updated listener after this object has been initialized.""" + await super().async_will_remove_from_hass() + self._client.zone_status_callbacks.remove(self._async_update_attrs) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the damper.""" + await self._set_cover_position(100) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close damper.""" + await self._set_cover_position(0) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Update the damper to a specific position.""" + + if (position := kwargs.get(ATTR_POSITION)) is None: + _LOGGER.debug("Argument `position` is missing in set_cover_position") + return + await self._set_cover_position(position) + + async def _set_cover_position(self, position_percent: float) -> None: + power: ZoneSettingPower + + if position_percent == 0: + power = ZoneSettingPower.SET_TO_OFF + else: + power = ZoneSettingPower.SET_TO_ON + + zcz = ZoneControlZone( + self._zone_name.zone_number, + ZoneSettingValue.SET_OPEN_PERCENTAGE, + power, + position_percent / 100.0, + ) + + packet = self._client.data_packet_factory.zone_control([zcz]) + await self._client.send_packet(packet) diff --git a/homeassistant/components/airtouch5/entity.py b/homeassistant/components/airtouch5/entity.py index e5899850e0f..d0a3cc8fea3 100644 --- a/homeassistant/components/airtouch5/entity.py +++ b/homeassistant/components/airtouch5/entity.py @@ -6,15 +6,12 @@ from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from .const import DOMAIN - class Airtouch5Entity(Entity): """Base class for Airtouch5 entities.""" _attr_should_poll = False _attr_has_entity_name = True - _attr_translation_key = DOMAIN def __init__(self, client: Airtouch5SimpleClient) -> None: """Initialise the Entity.""" diff --git a/homeassistant/components/airtouch5/strings.json b/homeassistant/components/airtouch5/strings.json index 6a91fa85fa5..effeb0c72e0 100644 --- a/homeassistant/components/airtouch5/strings.json +++ b/homeassistant/components/airtouch5/strings.json @@ -27,6 +27,11 @@ } } } + }, + "cover": { + "damper": { + "name": "[%key:component::cover::entity_component::damper::name%]" + } } } }