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 devicespull/52248/head
parent
594bcbcf7a
commit
3e1d32f4e0
|
@ -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.
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue