From 5727beed8e15597e3708e84bf8f583d598b42d64 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 8 Apr 2019 15:44:24 +0200 Subject: [PATCH] Add ESPHome Cover position/tilt support (#22858) ## Description: Add ESPHome cover position and tilt support. The aioesphomeapi also received a small refactor for these changes and those are part of this PR (constants were refactored into enums and optimistic was renamed to assumed_state). If possible, I'd like to include those in this PR because: 1. It's mostly just very simple changes 2. Because of the new position change the dev branch would be in a non-working state for a while until the split PR is merged (unless I write some temporary glue logic, but I'd prefer to avoid that) ## Checklist: - [x] The code change is tested and works locally. - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [x] There is no commented out code in this PR. If the code communicates with devices, web services, or third-party tools: - [x] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]). - [x] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]). - [x] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. [ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 [manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_ --- homeassistant/components/esphome/__init__.py | 13 ++- homeassistant/components/esphome/cover.py | 89 +++++++++++++++---- homeassistant/components/esphome/fan.py | 33 ++++--- .../components/esphome/manifest.json | 4 +- homeassistant/components/esphome/switch.py | 2 +- requirements_all.txt | 2 +- 6 files changed, 105 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 39422c530b3..19cd851002a 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: ServiceCall, UserService DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.7.0'] +REQUIREMENTS = ['aioesphomeapi==2.0.0'] _LOGGER = logging.getLogger(__name__) @@ -381,16 +381,15 @@ async def _async_setup_device_registry(hass: HomeAssistantType, async def _register_service(hass: HomeAssistantType, entry_data: RuntimeEntryData, service: 'UserService'): - from aioesphomeapi import USER_SERVICE_ARG_BOOL, USER_SERVICE_ARG_INT, \ - USER_SERVICE_ARG_FLOAT, USER_SERVICE_ARG_STRING + from aioesphomeapi import UserServiceArgType service_name = '{}_{}'.format(entry_data.device_info.name, service.name) schema = {} for arg in service.args: schema[vol.Required(arg.name)] = { - USER_SERVICE_ARG_BOOL: cv.boolean, - USER_SERVICE_ARG_INT: vol.Coerce(int), - USER_SERVICE_ARG_FLOAT: vol.Coerce(float), - USER_SERVICE_ARG_STRING: cv.string, + UserServiceArgType.BOOL: cv.boolean, + UserServiceArgType.INT: vol.Coerce(int), + UserServiceArgType.FLOAT: vol.Coerce(float), + UserServiceArgType.STRING: cv.string, }[arg.type_] async def execute_service(call): diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index d86c40e627e..68eb4221a93 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -3,7 +3,9 @@ import logging from typing import TYPE_CHECKING, Optional from homeassistant.components.cover import ( - SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, CoverDevice) + ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, CoverDevice) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -38,44 +40,97 @@ class EsphomeCover(EsphomeEntity, CoverDevice): def _static_info(self) -> 'CoverInfo': return super()._static_info - @property - def _state(self) -> Optional['CoverState']: - return super()._state - @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + flags = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + if self._static_info.supports_position: + flags |= SUPPORT_SET_POSITION + if self._static_info.supports_tilt: + flags |= (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | + SUPPORT_SET_TILT_POSITION) + return flags + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._static_info.device_class @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" - return self._static_info.is_optimistic + return self._static_info.assumed_state + + @property + def _state(self) -> Optional['CoverState']: + return super()._state @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed or not.""" if self._state is None: return None - return bool(self._state.state) + # Check closed state with api version due to a protocol change + return self._state.is_closed(self._client.api_version) + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + from aioesphomeapi import CoverOperation + if self._state is None: + return None + return self._state.current_operation == CoverOperation.IS_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + from aioesphomeapi import CoverOperation + if self._state is None: + return None + return self._state.current_operation == CoverOperation.IS_CLOSING + + @property + def current_cover_position(self) -> Optional[float]: + """Return current position of cover. 0 is closed, 100 is open.""" + if self._state is None or not self._static_info.supports_position: + return None + return self._state.position * 100.0 + + @property + def current_cover_tilt_position(self) -> Optional[float]: + """Return current position of cover tilt. 0 is closed, 100 is open.""" + if self._state is None or not self._static_info.supports_tilt: + return None + return self._state.tilt * 100.0 async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" - from aioesphomeapi.client import COVER_COMMAND_OPEN - await self._client.cover_command(key=self._static_info.key, - command=COVER_COMMAND_OPEN) + position=1.0) async def async_close_cover(self, **kwargs) -> None: """Close cover.""" - from aioesphomeapi.client import COVER_COMMAND_CLOSE - await self._client.cover_command(key=self._static_info.key, - command=COVER_COMMAND_CLOSE) + position=0.0) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" - from aioesphomeapi.client import COVER_COMMAND_STOP + await self._client.cover_command(key=self._static_info.key, stop=True) + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" await self._client.cover_command(key=self._static_info.key, - command=COVER_COMMAND_STOP) + position=kwargs[ATTR_POSITION] / 100) + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the cover tilt.""" + await self._client.cover_command(key=self._static_info.key, tilt=1.0) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the cover tilt.""" + await self._client.cover_command(key=self._static_info.key, tilt=0.0) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover tilt to a specific position.""" + await self._client.cover_command(key=self._static_info.key, + tilt=kwargs[ATTR_TILT_POSITION] / 100) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 05f18cb014a..973fa85774c 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -12,7 +12,7 @@ from . import EsphomeEntity, platform_async_setup_entry if TYPE_CHECKING: # pylint: disable=unused-import - from aioesphomeapi import FanInfo, FanState # noqa + from aioesphomeapi import FanInfo, FanState, FanSpeed # noqa DEPENDENCIES = ['esphome'] _LOGGER = logging.getLogger(__name__) @@ -32,12 +32,24 @@ async def async_setup_entry(hass: HomeAssistantType, ) -FAN_SPEED_STR_TO_INT = { - SPEED_LOW: 0, - SPEED_MEDIUM: 1, - SPEED_HIGH: 2 -} -FAN_SPEED_INT_TO_STR = {v: k for k, v in FAN_SPEED_STR_TO_INT.items()} +def _ha_fan_speed_to_esphome(speed: str) -> 'FanSpeed': + # pylint: disable=redefined-outer-name + from aioesphomeapi import FanSpeed # noqa + return { + SPEED_LOW: FanSpeed.LOW, + SPEED_MEDIUM: FanSpeed.MEDIUM, + SPEED_HIGH: FanSpeed.HIGH, + }[speed] + + +def _esphome_fan_speed_to_ha(speed: 'FanSpeed') -> str: + # pylint: disable=redefined-outer-name + from aioesphomeapi import FanSpeed # noqa + return { + FanSpeed.LOW: SPEED_LOW, + FanSpeed.MEDIUM: SPEED_MEDIUM, + FanSpeed.HIGH: SPEED_HIGH, + }[speed] class EsphomeFan(EsphomeEntity, FanEntity): @@ -56,8 +68,9 @@ class EsphomeFan(EsphomeEntity, FanEntity): if speed == SPEED_OFF: await self.async_turn_off() return + await self._client.fan_command( - self._static_info.key, speed=FAN_SPEED_STR_TO_INT[speed]) + self._static_info.key, speed=_ha_fan_speed_to_esphome(speed)) async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: @@ -67,7 +80,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): return data = {'key': self._static_info.key, 'state': True} if speed is not None: - data['speed'] = FAN_SPEED_STR_TO_INT[speed] + data['speed'] = _ha_fan_speed_to_esphome(speed) await self._client.fan_command(**data) # pylint: disable=arguments-differ @@ -94,7 +107,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): return None if not self._static_info.supports_speed: return None - return FAN_SPEED_INT_TO_STR[self._state.speed] + return _esphome_fan_speed_to_ha(self._state.speed) @property def oscillating(self) -> None: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b00cdf9607d..734544b49c7 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -1,9 +1,9 @@ { "domain": "esphome", - "name": "Esphome", + "name": "ESPHome", "documentation": "https://www.home-assistant.io/components/esphome", "requirements": [ - "aioesphomeapi==1.7.0" + "aioesphomeapi==2.0.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index e5a9d0cf446..e736c1df209 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -49,7 +49,7 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice): @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" - return self._static_info.optimistic + return self._static_info.assumed_state @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 76ec55ee3ce..9709d7c87ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ aiobotocore==0.10.2 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.7.0 +aioesphomeapi==2.0.0 # homeassistant.components.freebox aiofreepybox==0.0.8