From 3e1d32f4e004f8e056dccea978252ce5e9827585 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 28 Jun 2021 13:43:45 +0200 Subject: [PATCH] ESPHome Climate add preset, custom preset, custom fan mode (#52133) * ESPHome Climate add preset, custom preset, custom fan mode * Fix copy paste error * Bump aioesphomeapi to 3.0.0 * Bump aioesphomeapi to 3.0.1 * Persist api version to prevent exception for offline devices --- homeassistant/components/esphome/__init__.py | 6 ++ homeassistant/components/esphome/climate.py | 65 ++++++++++++++----- homeassistant/components/esphome/cover.py | 2 +- .../components/esphome/entry_data.py | 12 +++- homeassistant/components/esphome/fan.py | 2 +- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 72 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 7783047a662..2490c0bdbd3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -10,6 +10,7 @@ from typing import Generic, TypeVar from aioesphomeapi import ( APIClient, APIConnectionError, + APIVersion, DeviceInfo as EsphomeDeviceInfo, EntityInfo, EntityState, @@ -206,6 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nonlocal device_id try: entry_data.device_info = await cli.device_info() + entry_data.api_version = cli.api_version entry_data.available = True device_id = await _async_setup_device_registry( hass, entry, entry_data.device_info @@ -785,6 +787,10 @@ class EsphomeBaseEntity(Entity): def _entry_data(self) -> RuntimeEntryData: return self.hass.data[DOMAIN][self._entry_id] + @property + def _api_version(self) -> APIVersion: + return self._entry_data.api_version + @property def _static_info(self) -> EntityInfo: # Check if value is in info database. Use a single lookup. diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 15edcdd8150..f7ebccc8434 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -6,6 +6,7 @@ from aioesphomeapi import ( ClimateFanMode, ClimateInfo, ClimateMode, + ClimatePreset, ClimateState, ClimateSwingMode, ) @@ -30,14 +31,21 @@ from homeassistant.components.climate.const import ( FAN_MIDDLE, FAN_OFF, FAN_ON, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_ACTIVITY, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, @@ -80,11 +88,12 @@ async def async_setup_entry(hass, entry, async_add_entities): _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper( { ClimateMode.OFF: HVAC_MODE_OFF, - ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, + ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, ClimateMode.HEAT: HVAC_MODE_HEAT, ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, ClimateMode.DRY: HVAC_MODE_DRY, + ClimateMode.AUTO: HVAC_MODE_AUTO, } ) _CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper( @@ -118,6 +127,18 @@ _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper( ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } ) +_PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper( + { + ClimatePreset.NONE: PRESET_NONE, + ClimatePreset.HOME: PRESET_HOME, + ClimatePreset.AWAY: PRESET_AWAY, + ClimatePreset.BOOST: PRESET_BOOST, + ClimatePreset.COMFORT: PRESET_COMFORT, + ClimatePreset.ECO: PRESET_ECO, + ClimatePreset.SLEEP: PRESET_SLEEP, + ClimatePreset.ACTIVITY: PRESET_ACTIVITY, + } +) class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @@ -155,17 +176,20 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): ] @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return [ _FAN_MODES.from_esphome(mode) for mode in self._static_info.supported_fan_modes - ] + ] + self._static_info.supported_custom_fan_modes @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return preset modes.""" - return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] + return [ + _PRESETS.from_esphome(preset) + for preset in self._static_info.supported_presets_compat(self._api_version) + ] + self._static_info.supported_custom_presets @property def swing_modes(self): @@ -199,7 +223,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): features |= SUPPORT_TARGET_TEMPERATURE_RANGE else: features |= SUPPORT_TARGET_TEMPERATURE - if self._static_info.supports_away: + if self.preset_modes: features |= SUPPORT_PRESET_MODE if self._static_info.supported_fan_modes: features |= SUPPORT_FAN_MODE @@ -226,12 +250,16 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): @esphome_state_property def fan_mode(self) -> str | None: """Return current fan setting.""" - return _FAN_MODES.from_esphome(self._state.fan_mode) + return self._state.custom_fan_mode or _FAN_MODES.from_esphome( + self._state.fan_mode + ) @esphome_state_property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return current preset mode.""" - return PRESET_AWAY if self._state.away else PRESET_HOME + return self._state.custom_preset or _PRESETS.from_esphome( + self._state.preset_compat(self._api_version) + ) @esphome_state_property def swing_mode(self) -> str | None: @@ -277,16 +305,23 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): key=self._static_info.key, mode=_CLIMATE_MODES.from_hass(hvac_mode) ) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - away = preset_mode == PRESET_AWAY - await self._client.climate_command(key=self._static_info.key, away=away) + kwargs = {} + if preset_mode in self._static_info.supported_custom_presets: + kwargs["custom_preset"] = preset_mode + else: + kwargs["preset"] = _PRESETS.from_hass(preset_mode) + await self._client.climate_command(key=self._static_info.key, **kwargs) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - await self._client.climate_command( - key=self._static_info.key, fan_mode=_FAN_MODES.from_hass(fan_mode) - ) + kwargs = {} + if fan_mode in self._static_info.supported_custom_fan_modes: + kwargs["custom_fan_mode"] = fan_mode + else: + kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode) + await self._client.climate_command(key=self._static_info.key, **kwargs) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 3f4bd29198c..3064f827d7f 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -74,7 +74,7 @@ class EsphomeCover(EsphomeEntity, CoverEntity): def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change - return self._state.is_closed(self._client.api_version) + return self._state.is_closed(self._api_version) @esphome_state_property def is_opening(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c5d36e3a68d..dc964d0eb2e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, + APIVersion, BinarySensorInfo, CameraInfo, ClimateInfo, @@ -67,6 +68,7 @@ class RuntimeEntryData: services: dict[int, UserService] = attr.ib(factory=dict) available: bool = attr.ib(default=False) device_info: DeviceInfo | None = attr.ib(default=None) + api_version: APIVersion = attr.ib(factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list) disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list) loaded_platforms: set[str] = attr.ib(factory=set) @@ -141,6 +143,10 @@ class RuntimeEntryData: self.device_info = _attr_obj_from_dict( DeviceInfo, **restored.pop("device_info") ) + self.api_version = _attr_obj_from_dict( + APIVersion, **restored.pop("api_version", {}) + ) + infos = [] for comp_type, restored_infos in restored.items(): if comp_type not in COMPONENT_TYPE_TO_INFO: @@ -155,7 +161,11 @@ class RuntimeEntryData: async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" - store_data = {"device_info": attr.asdict(self.device_info), "services": []} + store_data = { + "device_info": attr.asdict(self.device_info), + "services": [], + "api_version": attr.asdict(self.api_version), + } for comp_type, infos in self.info.items(): store_data[comp_type] = [attr.asdict(info) for info in infos.values()] diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index b73df42c8dc..e02958d5885 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -68,7 +68,7 @@ class EsphomeFan(EsphomeEntity, FanEntity): @property def _supports_speed_levels(self) -> bool: - api_version = self._client.api_version + api_version = self._api_version return api_version.major == 1 and api_version.minor > 3 async def async_set_percentage(self, percentage: int) -> None: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e5992c84358..9182be8d496 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.9.0"], + "requirements": ["aioesphomeapi==3.0.1"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 7f8dba6c6e3..6cb0d7cf98e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.9.0 +aioesphomeapi==3.0.1 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6381470a6cd..4db30aecf05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.9.0 +aioesphomeapi==3.0.1 # homeassistant.components.flo aioflo==0.4.1