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 (
|
from aioesphomeapi import (
|
||||||
APIClient,
|
APIClient,
|
||||||
APIConnectionError,
|
APIConnectionError,
|
||||||
|
APIVersion,
|
||||||
DeviceInfo as EsphomeDeviceInfo,
|
DeviceInfo as EsphomeDeviceInfo,
|
||||||
EntityInfo,
|
EntityInfo,
|
||||||
EntityState,
|
EntityState,
|
||||||
|
@ -206,6 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
nonlocal device_id
|
nonlocal device_id
|
||||||
try:
|
try:
|
||||||
entry_data.device_info = await cli.device_info()
|
entry_data.device_info = await cli.device_info()
|
||||||
|
entry_data.api_version = cli.api_version
|
||||||
entry_data.available = True
|
entry_data.available = True
|
||||||
device_id = await _async_setup_device_registry(
|
device_id = await _async_setup_device_registry(
|
||||||
hass, entry, entry_data.device_info
|
hass, entry, entry_data.device_info
|
||||||
|
@ -785,6 +787,10 @@ class EsphomeBaseEntity(Entity):
|
||||||
def _entry_data(self) -> RuntimeEntryData:
|
def _entry_data(self) -> RuntimeEntryData:
|
||||||
return self.hass.data[DOMAIN][self._entry_id]
|
return self.hass.data[DOMAIN][self._entry_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _api_version(self) -> APIVersion:
|
||||||
|
return self._entry_data.api_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _static_info(self) -> EntityInfo:
|
def _static_info(self) -> EntityInfo:
|
||||||
# Check if value is in info database. Use a single lookup.
|
# Check if value is in info database. Use a single lookup.
|
||||||
|
|
|
@ -6,6 +6,7 @@ from aioesphomeapi import (
|
||||||
ClimateFanMode,
|
ClimateFanMode,
|
||||||
ClimateInfo,
|
ClimateInfo,
|
||||||
ClimateMode,
|
ClimateMode,
|
||||||
|
ClimatePreset,
|
||||||
ClimateState,
|
ClimateState,
|
||||||
ClimateSwingMode,
|
ClimateSwingMode,
|
||||||
)
|
)
|
||||||
|
@ -30,14 +31,21 @@ from homeassistant.components.climate.const import (
|
||||||
FAN_MIDDLE,
|
FAN_MIDDLE,
|
||||||
FAN_OFF,
|
FAN_OFF,
|
||||||
FAN_ON,
|
FAN_ON,
|
||||||
|
HVAC_MODE_AUTO,
|
||||||
HVAC_MODE_COOL,
|
HVAC_MODE_COOL,
|
||||||
HVAC_MODE_DRY,
|
HVAC_MODE_DRY,
|
||||||
HVAC_MODE_FAN_ONLY,
|
HVAC_MODE_FAN_ONLY,
|
||||||
HVAC_MODE_HEAT,
|
HVAC_MODE_HEAT,
|
||||||
HVAC_MODE_HEAT_COOL,
|
HVAC_MODE_HEAT_COOL,
|
||||||
HVAC_MODE_OFF,
|
HVAC_MODE_OFF,
|
||||||
|
PRESET_ACTIVITY,
|
||||||
PRESET_AWAY,
|
PRESET_AWAY,
|
||||||
|
PRESET_BOOST,
|
||||||
|
PRESET_COMFORT,
|
||||||
|
PRESET_ECO,
|
||||||
PRESET_HOME,
|
PRESET_HOME,
|
||||||
|
PRESET_NONE,
|
||||||
|
PRESET_SLEEP,
|
||||||
SUPPORT_FAN_MODE,
|
SUPPORT_FAN_MODE,
|
||||||
SUPPORT_PRESET_MODE,
|
SUPPORT_PRESET_MODE,
|
||||||
SUPPORT_SWING_MODE,
|
SUPPORT_SWING_MODE,
|
||||||
|
@ -80,11 +88,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper(
|
_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper(
|
||||||
{
|
{
|
||||||
ClimateMode.OFF: HVAC_MODE_OFF,
|
ClimateMode.OFF: HVAC_MODE_OFF,
|
||||||
ClimateMode.AUTO: HVAC_MODE_HEAT_COOL,
|
ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL,
|
||||||
ClimateMode.COOL: HVAC_MODE_COOL,
|
ClimateMode.COOL: HVAC_MODE_COOL,
|
||||||
ClimateMode.HEAT: HVAC_MODE_HEAT,
|
ClimateMode.HEAT: HVAC_MODE_HEAT,
|
||||||
ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
|
ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
|
||||||
ClimateMode.DRY: HVAC_MODE_DRY,
|
ClimateMode.DRY: HVAC_MODE_DRY,
|
||||||
|
ClimateMode.AUTO: HVAC_MODE_AUTO,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper(
|
_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper(
|
||||||
|
@ -118,6 +127,18 @@ _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper(
|
||||||
ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL,
|
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):
|
class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
||||||
|
@ -155,17 +176,20 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_modes(self):
|
def fan_modes(self) -> list[str]:
|
||||||
"""Return the list of available fan modes."""
|
"""Return the list of available fan modes."""
|
||||||
return [
|
return [
|
||||||
_FAN_MODES.from_esphome(mode)
|
_FAN_MODES.from_esphome(mode)
|
||||||
for mode in self._static_info.supported_fan_modes
|
for mode in self._static_info.supported_fan_modes
|
||||||
]
|
] + self._static_info.supported_custom_fan_modes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_modes(self):
|
def preset_modes(self) -> list[str]:
|
||||||
"""Return preset modes."""
|
"""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
|
@property
|
||||||
def swing_modes(self):
|
def swing_modes(self):
|
||||||
|
@ -199,7 +223,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
||||||
features |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
features |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
else:
|
else:
|
||||||
features |= SUPPORT_TARGET_TEMPERATURE
|
features |= SUPPORT_TARGET_TEMPERATURE
|
||||||
if self._static_info.supports_away:
|
if self.preset_modes:
|
||||||
features |= SUPPORT_PRESET_MODE
|
features |= SUPPORT_PRESET_MODE
|
||||||
if self._static_info.supported_fan_modes:
|
if self._static_info.supported_fan_modes:
|
||||||
features |= SUPPORT_FAN_MODE
|
features |= SUPPORT_FAN_MODE
|
||||||
|
@ -226,12 +250,16 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
||||||
@esphome_state_property
|
@esphome_state_property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return current fan setting."""
|
"""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
|
@esphome_state_property
|
||||||
def preset_mode(self):
|
def preset_mode(self) -> str | None:
|
||||||
"""Return current preset mode."""
|
"""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
|
@esphome_state_property
|
||||||
def swing_mode(self) -> str | None:
|
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)
|
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."""
|
"""Set preset mode."""
|
||||||
away = preset_mode == PRESET_AWAY
|
kwargs = {}
|
||||||
await self._client.climate_command(key=self._static_info.key, away=away)
|
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:
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
"""Set new fan mode."""
|
"""Set new fan mode."""
|
||||||
await self._client.climate_command(
|
kwargs = {}
|
||||||
key=self._static_info.key, fan_mode=_FAN_MODES.from_hass(fan_mode)
|
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:
|
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||||
"""Set new swing mode."""
|
"""Set new swing mode."""
|
||||||
|
|
|
@ -74,7 +74,7 @@ class EsphomeCover(EsphomeEntity, CoverEntity):
|
||||||
def is_closed(self) -> bool | None:
|
def is_closed(self) -> bool | None:
|
||||||
"""Return if the cover is closed or not."""
|
"""Return if the cover is closed or not."""
|
||||||
# Check closed state with api version due to a protocol change
|
# 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
|
@esphome_state_property
|
||||||
def is_opening(self) -> bool:
|
def is_opening(self) -> bool:
|
||||||
|
|
|
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
||||||
|
|
||||||
from aioesphomeapi import (
|
from aioesphomeapi import (
|
||||||
COMPONENT_TYPE_TO_INFO,
|
COMPONENT_TYPE_TO_INFO,
|
||||||
|
APIVersion,
|
||||||
BinarySensorInfo,
|
BinarySensorInfo,
|
||||||
CameraInfo,
|
CameraInfo,
|
||||||
ClimateInfo,
|
ClimateInfo,
|
||||||
|
@ -67,6 +68,7 @@ class RuntimeEntryData:
|
||||||
services: dict[int, UserService] = attr.ib(factory=dict)
|
services: dict[int, UserService] = attr.ib(factory=dict)
|
||||||
available: bool = attr.ib(default=False)
|
available: bool = attr.ib(default=False)
|
||||||
device_info: DeviceInfo | None = attr.ib(default=None)
|
device_info: DeviceInfo | None = attr.ib(default=None)
|
||||||
|
api_version: APIVersion = attr.ib(factory=APIVersion)
|
||||||
cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
|
cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list)
|
||||||
disconnect_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)
|
loaded_platforms: set[str] = attr.ib(factory=set)
|
||||||
|
@ -141,6 +143,10 @@ class RuntimeEntryData:
|
||||||
self.device_info = _attr_obj_from_dict(
|
self.device_info = _attr_obj_from_dict(
|
||||||
DeviceInfo, **restored.pop("device_info")
|
DeviceInfo, **restored.pop("device_info")
|
||||||
)
|
)
|
||||||
|
self.api_version = _attr_obj_from_dict(
|
||||||
|
APIVersion, **restored.pop("api_version", {})
|
||||||
|
)
|
||||||
|
|
||||||
infos = []
|
infos = []
|
||||||
for comp_type, restored_infos in restored.items():
|
for comp_type, restored_infos in restored.items():
|
||||||
if comp_type not in COMPONENT_TYPE_TO_INFO:
|
if comp_type not in COMPONENT_TYPE_TO_INFO:
|
||||||
|
@ -155,7 +161,11 @@ class RuntimeEntryData:
|
||||||
|
|
||||||
async def async_save_to_store(self) -> None:
|
async def async_save_to_store(self) -> None:
|
||||||
"""Generate dynamic data to store and save it to the filesystem."""
|
"""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():
|
for comp_type, infos in self.info.items():
|
||||||
store_data[comp_type] = [attr.asdict(info) for info in infos.values()]
|
store_data[comp_type] = [attr.asdict(info) for info in infos.values()]
|
||||||
|
|
|
@ -68,7 +68,7 @@ class EsphomeFan(EsphomeEntity, FanEntity):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _supports_speed_levels(self) -> bool:
|
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
|
return api_version.major == 1 and api_version.minor > 3
|
||||||
|
|
||||||
async def async_set_percentage(self, percentage: int) -> None:
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "ESPHome",
|
"name": "ESPHome",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||||
"requirements": ["aioesphomeapi==2.9.0"],
|
"requirements": ["aioesphomeapi==3.0.1"],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."],
|
"zeroconf": ["_esphomelib._tcp.local."],
|
||||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||||
"after_dependencies": ["zeroconf", "tag"],
|
"after_dependencies": ["zeroconf", "tag"],
|
||||||
|
|
|
@ -160,7 +160,7 @@ aioeafm==0.1.2
|
||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==2.9.0
|
aioesphomeapi==3.0.1
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==0.4.1
|
aioflo==0.4.1
|
||||||
|
|
|
@ -100,7 +100,7 @@ aioeafm==0.1.2
|
||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==2.9.0
|
aioesphomeapi==3.0.1
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==0.4.1
|
aioflo==0.4.1
|
||||||
|
|
Loading…
Reference in New Issue