"""Implement the Yandex Smart Home mode capabilities.""" from abc import ABC, abstractmethod from contextlib import suppress from enum import StrEnum import logging import math from typing import Any, Iterable, Protocol from homeassistant.components import climate, fan, humidifier, media_player, vacuum from homeassistant.components.climate import ClimateEntityFeature, HVACMode from homeassistant.components.fan import FanEntityFeature from homeassistant.components.humidifier import HumidifierEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.util.percentage import ordered_list_item_to_percentage, percentage_to_ordered_list_item from .capability import STATE_CAPABILITIES_REGISTRY, Capability, StateCapability from .const import CONF_ENTITY_MODE_MAP, CONF_FEATURES, STATE_NONE, MediaPlayerFeature from .helpers import APIError from .schema import ( CapabilityType, ModeCapabilityInstance, ModeCapabilityInstanceActionState, ModeCapabilityMode, ModeCapabilityParameters, ResponseCode, ) _LOGGER = logging.getLogger(__name__) class GenericMode(StrEnum): GENTLE = "gentle" # tuya vacuum? MAX_PLUS_SIGN = "max+" # deebot? HIGHEST = "highest" # smartir class SmartThinQFanMode(StrEnum): LOW_MID = "low_mid" MID_HIGH = "mid_high" class RoborockCleanupMode(StrEnum): OFF = "off" SILENT = "silent" BALANCED = "balanced" TURBO = "turbo" MAX = "max" MAX_PLUS = "max_plus" CUSTOM = "custom" class RoombaCleanupMode(StrEnum): AUTOMATIC = "Automatic" ECO = "Eco" PERFORMANCE = "Performance" STANDARD = "Standard" class TionFanSpeed(StrEnum): S1 = "1" S2 = "2" S3 = "3" S4 = "4" S5 = "5" S6 = "6" class XiaomiHumidifierMode(StrEnum): MID = "mid" class XiaomiMiotHumidifierMode(StrEnum): CONST_HUMIDITY = "Const Humidity" # leshow.humidifier.jsq1 class XiaomiFanMode(StrEnum): AUTO = "Auto" SILENT = "Silent" LOW = "Low" FAVORITE = "Favorite" IDLE = "Idle" MEDIUM = "Medium" MIDDLE = "Middle" HIGH = "High" STRONG = "Strong" FAN = "Fan" NATURE = "Nature" class XiaomiMiotFanMode(StrEnum): LEVEL_1 = "Level 1" LEVEL_2 = "Level 2" LEVEL_3 = "Level 3" LEVEL_4 = "Level 4" LEVEL_5 = "Level 5" class XiaomiMiotCleanupMode(StrEnum): SILENT = "Silent" SLIENT = "slient" # https://github.com/al-one/hass-xiaomi-miot/issues/1605 BASIC = "Basic" STRONG = "Strong" FULL_SPEED = "Full Speed" MOP_ONLY = "Mop Only" CUSTOM = "Custom" class ModeCapability(Capability[ModeCapabilityInstanceActionState], Protocol): """Base class for capabilities with mode functionality like thermostat mode or fan speed.""" type: CapabilityType = CapabilityType.MODE instance: ModeCapabilityInstance _modes_map_default: dict[ModeCapabilityMode, list[str]] = {} _modes_map_index_fallback: dict[int, ModeCapabilityMode] = { 0: ModeCapabilityMode.ONE, 1: ModeCapabilityMode.TWO, 2: ModeCapabilityMode.THREE, 3: ModeCapabilityMode.FOUR, 4: ModeCapabilityMode.FIVE, 5: ModeCapabilityMode.SIX, 6: ModeCapabilityMode.SEVEN, 7: ModeCapabilityMode.EIGHT, 8: ModeCapabilityMode.NINE, 9: ModeCapabilityMode.TEN, } @property def supported(self) -> bool: """Test if the capability is supported.""" return bool(self.supported_yandex_modes) @property def parameters(self) -> ModeCapabilityParameters: """Return parameters for a devices list request.""" return ModeCapabilityParameters.from_list(self.instance, self.supported_yandex_modes) @property def supported_yandex_modes(self) -> list[ModeCapabilityMode]: """Returns a list of supported Yandex modes.""" modes = set() for ha_value in self.supported_ha_modes: if value := self.get_yandex_mode_by_ha_mode(ha_value, hide_warnings=True): modes.add(value) return sorted(modes) @property def supported_ha_modes(self) -> list[str]: """Returns list of supported HA modes.""" return [str(m) for m in self._ha_modes] @property def modes_map(self) -> dict[ModeCapabilityMode, list[str]]: """Return a modes mapping between Yandex and HA.""" return self.modes_map_config or self._modes_map_default @property def modes_map_config(self) -> dict[ModeCapabilityMode, list[str]]: """Return a modes mapping from a entity configuration.""" if CONF_ENTITY_MODE_MAP in self._entity_config: return { ModeCapabilityMode(k): v for k, v in self._entity_config[CONF_ENTITY_MODE_MAP].get(self.instance, {}).items() } return {} def get_yandex_mode_by_ha_mode(self, ha_mode: str, hide_warnings: bool = False) -> ModeCapabilityMode | None: """Return Yandex mode for HA mode (case-insensitive).""" ha_mode_lower = ha_mode.lower() mode = None for yandex_mode, names in self.modes_map.items(): if ha_mode_lower in [n.lower() for n in names]: mode = yandex_mode break supported_lower = [m.lower() for m in self.supported_ha_modes] if mode is not None and ha_mode_lower not in supported_lower: _LOGGER.debug( f"HA mode '{ha_mode}' mapped but case-mismatch in supported_ha_modes: {self.supported_ha_modes}" ) # НЕ бросаем ошибку — продолжаем работу if not self.modes_map_config: if mode is None: with suppress(ValueError): mode = ModeCapabilityMode(ha_mode_lower) if mode is None and ha_mode_lower != STATE_OFF: try: idx = next( (i for i, m in enumerate(supported_lower) if m == ha_mode_lower), None ) if idx is not None: mode = self._modes_map_index_fallback.get(idx) except (IndexError, ValueError, KeyError): pass if mode is None and not hide_warnings: if ha_mode_lower not in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_NONE): if ha_mode_lower in supported_lower: _LOGGER.warning( f"Failed to get Yandex mode for mode '{ha_mode}' for {self}. " f"It may cause inconsistencies between Yandex and HA. " f"See https://docs.yaha-cloud.ru/v1.0.x/config/modes/" ) return mode def get_ha_mode_by_yandex_mode(self, yandex_mode: ModeCapabilityMode) -> str: """Return HA mode for Yandex mode.""" ha_modes = self.modes_map.get(yandex_mode, []) if not self.modes_map_config: ha_modes.append(yandex_mode.value) for ha_mode in ha_modes: for am in self.supported_ha_modes: if am.lower() == ha_mode.lower(): return am if not self.modes_map_config: for ha_idx, yandex_mode_idx in self._modes_map_index_fallback.items(): if yandex_mode_idx == yandex_mode: return self.supported_ha_modes[ha_idx] raise APIError( ResponseCode.INVALID_VALUE, f"Unsupported mode '{yandex_mode}' for {self}, see https://docs.yaha-cloud.ru/v1.0.x/config/modes/", ) @abstractmethod def get_value(self) -> ModeCapabilityMode | None: """Return the current capability value.""" ... @property @abstractmethod def _ha_modes(self) -> Iterable[Any]: """Returns list of HA modes.""" ... class StateModeCapability(ModeCapability, StateCapability[ModeCapabilityInstanceActionState], Protocol): """Base class for a mode capability based on the state.""" def get_value(self) -> ModeCapabilityMode | None: """Return the current capability value.""" if self._ha_value is None: return None return self.get_yandex_mode_by_ha_mode(str(self._ha_value), False) @property def _ha_value(self) -> Any: """Return the current unmapped capability value.""" return self.state.state class ThermostatCapability(StateModeCapability): """Capability to control mode of a climate device.""" instance = ModeCapabilityInstance.THERMOSTAT _modes_map_default = { ModeCapabilityMode.HEAT: [HVACMode.HEAT], ModeCapabilityMode.COOL: [HVACMode.COOL], ModeCapabilityMode.AUTO: [HVACMode.HEAT_COOL, HVACMode.AUTO], ModeCapabilityMode.DRY: [HVACMode.DRY], ModeCapabilityMode.FAN_ONLY: [HVACMode.FAN_ONLY], } @property def supported(self) -> bool: """Test if the capability is supported.""" if self.state.domain == climate.DOMAIN: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: """Change the capability state.""" await self._hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_HVAC_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: """Returns list of HA modes.""" return self.state.attributes.get(climate.ATTR_HVAC_MODES, []) or [] class SwingCapability(StateModeCapability): """Capability to control swing mode of a climate device.""" instance = ModeCapabilityInstance.SWING _modes_map_default = { ModeCapabilityMode.VERTICAL: ["ud"], ModeCapabilityMode.HORIZONTAL: ["lr"], ModeCapabilityMode.STATIONARY: [climate.SWING_OFF], ModeCapabilityMode.AUTO: [climate.SWING_BOTH, "all"], } @property def supported(self) -> bool: if self.state.domain == climate.DOMAIN and self._state_features & ClimateEntityFeature.SWING_MODE: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_SWING_MODE, { ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_SWING_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(climate.ATTR_SWING_MODES, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(climate.ATTR_SWING_MODE) class ProgramCapability(StateModeCapability, ABC): """Base capability to control a device program.""" instance = ModeCapabilityInstance.PROGRAM class ProgramCapabilityClimate(ProgramCapability): """Capability to control the mode preset of a climate device.""" _modes_map_default = { ModeCapabilityMode.AUTO: [climate.const.PRESET_NONE], ModeCapabilityMode.ECO: [climate.const.PRESET_ECO], ModeCapabilityMode.MIN: [climate.const.PRESET_AWAY], ModeCapabilityMode.TURBO: [climate.const.PRESET_BOOST], ModeCapabilityMode.MEDIUM: [climate.const.PRESET_COMFORT], ModeCapabilityMode.MAX: [climate.const.PRESET_HOME], ModeCapabilityMode.QUIET: [climate.const.PRESET_SLEEP], } @property def supported(self) -> bool: if self.state.domain == climate.DOMAIN and self._state_features & ClimateEntityFeature.PRESET_MODE: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_PRESET_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(climate.ATTR_PRESET_MODES, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(climate.ATTR_PRESET_MODE) class ProgramCapabilityHumidifier(ProgramCapability): """Capability to control the mode of a humidifier device.""" _modes_map_default = { ModeCapabilityMode.FAN_ONLY: [XiaomiFanMode.FAN], ModeCapabilityMode.AUTO: [humidifier.const.MODE_AUTO, XiaomiMiotHumidifierMode.CONST_HUMIDITY], ModeCapabilityMode.ECO: [humidifier.const.MODE_ECO, XiaomiFanMode.IDLE], ModeCapabilityMode.QUIET: [humidifier.const.MODE_SLEEP, XiaomiFanMode.SILENT], ModeCapabilityMode.MIN: [humidifier.const.MODE_AWAY], ModeCapabilityMode.MEDIUM: [humidifier.const.MODE_COMFORT, XiaomiFanMode.MIDDLE, XiaomiHumidifierMode.MID], ModeCapabilityMode.NORMAL: [humidifier.const.MODE_NORMAL, XiaomiFanMode.FAVORITE], ModeCapabilityMode.MAX: [humidifier.const.MODE_HOME], ModeCapabilityMode.HIGH: [humidifier.const.MODE_BABY], ModeCapabilityMode.TURBO: [humidifier.const.MODE_BOOST, XiaomiFanMode.STRONG], } @property def supported(self) -> bool: if self.state.domain == humidifier.DOMAIN and self._state_features & HumidifierEntityFeature.MODES: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( humidifier.DOMAIN, humidifier.SERVICE_SET_MODE, { ATTR_ENTITY_ID: self.state.entity_id, humidifier.ATTR_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(humidifier.ATTR_MODE) class ProgramCapabilityFan(ProgramCapability): """Capability to control the mode preset of a fan device.""" _modes_map_default = { ModeCapabilityMode.ECO: [XiaomiFanMode.IDLE], ModeCapabilityMode.QUIET: [XiaomiFanMode.SILENT, XiaomiFanMode.NATURE, XiaomiMiotFanMode.LEVEL_1], ModeCapabilityMode.LOW: [XiaomiMiotFanMode.LEVEL_2], ModeCapabilityMode.MEDIUM: [XiaomiHumidifierMode.MID, XiaomiMiotFanMode.LEVEL_3], ModeCapabilityMode.NORMAL: [XiaomiFanMode.FAVORITE], ModeCapabilityMode.HIGH: [XiaomiMiotFanMode.LEVEL_4], ModeCapabilityMode.TURBO: [XiaomiFanMode.STRONG, XiaomiMiotFanMode.LEVEL_5], } @property def supported(self) -> bool: if self.state.domain == fan.DOMAIN: if self._state_features & FanEntityFeature.PRESET_MODE: if ( self._state_features & FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes ): return False return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_PRESET_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(fan.ATTR_PRESET_MODES, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(fan.ATTR_PRESET_MODE) class InputSourceCapability(StateModeCapability): """Capability to control the input source of a media player device.""" instance = ModeCapabilityInstance.INPUT_SOURCE @property def supported(self) -> bool: if self.state.domain == media_player.DOMAIN: if MediaPlayerFeature.SELECT_SOURCE in self._entity_config.get(CONF_FEATURES, []): return super().supported if self._state_features & MediaPlayerEntityFeature.SELECT_SOURCE: return super().supported return False def get_yandex_mode_by_ha_mode(self, ha_mode: str, hide_warnings: bool = False) -> ModeCapabilityMode | None: return super().get_yandex_mode_by_ha_mode(ha_mode, hide_warnings=True) async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE, { ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_INPUT_SOURCE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: modes = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, []) or [] filtered_modes = [m for m in modes if m not in ["Live TV"]] # #418 if filtered_modes or self.state.state not in (STATE_OFF, STATE_UNKNOWN): self._cache.save_attr_value(self.state.entity_id, media_player.ATTR_INPUT_SOURCE_LIST, modes) return modes return self._cache.get_attr_value(self.state.entity_id, media_player.ATTR_INPUT_SOURCE_LIST) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(media_player.ATTR_INPUT_SOURCE) class FanSpeedCapability(StateModeCapability, ABC): """Base capability to control a device fan speed.""" instance = ModeCapabilityInstance.FAN_SPEED class FanSpeedCapabilityClimate(FanSpeedCapability): """Capability to control the fan speed of a climate device.""" _modes_map_default = { ModeCapabilityMode.AUTO: [climate.FAN_AUTO, climate.FAN_ON, XiaomiFanMode.NATURE, "auto"], ModeCapabilityMode.QUIET: ["level1", "Level1", "silent", "quiet", "low"], ModeCapabilityMode.MIN: ["level2", "Level2", climate.FAN_LOW, TionFanSpeed.S2], ModeCapabilityMode.LOW: ["level3", "Level3", climate.FAN_LOW, "low_medium", TionFanSpeed.S3], ModeCapabilityMode.MEDIUM: ["level4", "Level4", climate.FAN_MEDIUM, "medium", "mid", TionFanSpeed.S4], ModeCapabilityMode.NORMAL: ["level5", "Level5", "medium_high", "normal"], ModeCapabilityMode.HIGH: ["level6", "Level6", climate.FAN_HIGH, "high", TionFanSpeed.S5], ModeCapabilityMode.TURBO: ["level7", "Level7", "turbo", "strong", climate.FAN_FOCUS, TionFanSpeed.S6], ModeCapabilityMode.MAX: ["level8", "Level8", "max", SmartThinQFanMode.MID_HIGH], } @property def supported(self) -> bool: if self.state.domain == climate.DOMAIN and self._state_features & ClimateEntityFeature.FAN_MODE: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_FAN_MODE, { ATTR_ENTITY_ID: self.state.entity_id, climate.ATTR_FAN_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) or [] if self._ha_value == climate.FAN_ON and climate.FAN_ON not in modes: modes.append(climate.FAN_ON) return modes @property def _ha_value(self) -> Any: return self.state.attributes.get(climate.ATTR_FAN_MODE) class FanSpeedCapabilityFanViaPreset(FanSpeedCapability): """Capability to control the fan speed of a fan device via preset.""" _modes_map_default = { ModeCapabilityMode.AUTO: [climate.FAN_AUTO, climate.FAN_ON], ModeCapabilityMode.ECO: [XiaomiFanMode.IDLE], ModeCapabilityMode.QUIET: [climate.FAN_OFF, XiaomiFanMode.SILENT, XiaomiMiotFanMode.LEVEL_1], ModeCapabilityMode.LOW: [XiaomiMiotFanMode.LEVEL_2], ModeCapabilityMode.MEDIUM: [XiaomiHumidifierMode.MID, XiaomiMiotFanMode.LEVEL_3], ModeCapabilityMode.NORMAL: [XiaomiFanMode.FAVORITE], ModeCapabilityMode.HIGH: [XiaomiMiotFanMode.LEVEL_4], ModeCapabilityMode.TURBO: [XiaomiFanMode.STRONG, XiaomiMiotFanMode.LEVEL_5], } @property def supported(self) -> bool: if self.state.domain == fan.DOMAIN: if self._state_features & FanEntityFeature.PRESET_MODE: if ( self._state_features & FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes ): return False return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE, { ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_PRESET_MODE: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(fan.ATTR_PRESET_MODES, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(fan.ATTR_PRESET_MODE) class FanSpeedCapabilityFanViaPercentage(FanSpeedCapability): """Capability to control the fan speed in percents of a fan device.""" @property def supported(self) -> bool: if self.state.domain == fan.DOMAIN: if ( self._state_features & fan.FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes ): return super().supported return False @property def supported_yandex_modes(self) -> list[ModeCapabilityMode]: return [ModeCapabilityMode(m) for m in self.supported_ha_modes] def get_value(self) -> ModeCapabilityMode | None: if not self._ha_value: return None value = int(self._ha_value) if self.modes_map: for yandex_mode, values in self.modes_map.items(): for str_value in values: if value == self._convert_mapping_speed_value(str_value): return yandex_mode return None return ModeCapabilityMode(percentage_to_ordered_list_item(self.supported_ha_modes, value)) async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: if self.modes_map: ha_modes = self.modes_map.get(state.value) if not ha_modes: raise APIError( ResponseCode.INVALID_VALUE, f"Unsupported mode '{state.value}' for {self}, see https://docs.yaha-cloud.ru/v1.0.x/config/modes/", ) ha_mode = self._convert_mapping_speed_value(ha_modes[0]) else: ha_mode = ordered_list_item_to_percentage(self.supported_ha_modes, state.value) await self._hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE, {ATTR_ENTITY_ID: self.state.entity_id, fan.ATTR_PERCENTAGE: ha_mode}, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: if self.modes_map: return self.modes_map.keys() percentage_step = self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP, 100) speed_count = math.ceil(100 / percentage_step) if speed_count == 1: return [] modes = [ModeCapabilityMode.LOW, ModeCapabilityMode.HIGH] if speed_count >= 3: modes.insert(modes.index(ModeCapabilityMode.HIGH), ModeCapabilityMode.MEDIUM) if speed_count >= 4: modes.insert(modes.index(ModeCapabilityMode.MEDIUM), ModeCapabilityMode.NORMAL) if speed_count >= 5: modes.insert(0, ModeCapabilityMode.ECO) if speed_count >= 6: modes.insert(modes.index(ModeCapabilityMode.LOW), ModeCapabilityMode.QUIET) if speed_count >= 7: modes.append(ModeCapabilityMode.TURBO) return modes @property def _ha_value(self) -> Any: return self.state.attributes.get(fan.ATTR_PERCENTAGE) def _convert_mapping_speed_value(self, value: str) -> int: try: return int(value.replace("%", "")) except ValueError: raise APIError(ResponseCode.INVALID_VALUE, f"Unsupported speed value '{value}' for {self}") class CleanupModeCapability(StateModeCapability): """Capability to control the program of a vacuum.""" instance = ModeCapabilityInstance.CLEANUP_MODE _modes_map_default = { ModeCapabilityMode.ECO: [RoborockCleanupMode.OFF], ModeCapabilityMode.AUTO: [RoombaCleanupMode.AUTOMATIC, RoborockCleanupMode.BALANCED], ModeCapabilityMode.TURBO: [ GenericMode.MAX_PLUS_SIGN, RoborockCleanupMode.TURBO, RoombaCleanupMode.PERFORMANCE, XiaomiMiotCleanupMode.FULL_SPEED, ], ModeCapabilityMode.LOW: [GenericMode.GENTLE], ModeCapabilityMode.MAX: [RoborockCleanupMode.MAX, XiaomiMiotCleanupMode.STRONG], ModeCapabilityMode.FAST: [RoborockCleanupMode.MAX_PLUS], ModeCapabilityMode.NORMAL: [RoombaCleanupMode.STANDARD, XiaomiMiotCleanupMode.BASIC], ModeCapabilityMode.QUIET: [RoborockCleanupMode.SILENT, RoombaCleanupMode.ECO], } @property def supported(self) -> bool: if self.state.domain == vacuum.DOMAIN and self._state_features & VacuumEntityFeature.FAN_SPEED: return super().supported return False async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None: await self._hass.services.async_call( vacuum.DOMAIN, vacuum.SERVICE_SET_FAN_SPEED, { ATTR_ENTITY_ID: self.state.entity_id, vacuum.ATTR_FAN_SPEED: self.get_ha_mode_by_yandex_mode(state.value), }, blocking=self._wait_for_service_call, context=context, ) @property def _ha_modes(self) -> Iterable[Any]: return self.state.attributes.get(vacuum.ATTR_FAN_SPEED_LIST, []) or [] @property def _ha_value(self) -> Any: return self.state.attributes.get(vacuum.ATTR_FAN_SPEED) # ────────────────────────────────────────────── # Регистрация всех capabilities # ────────────────────────────────────────────── STATE_CAPABILITIES_REGISTRY.register(ThermostatCapability) STATE_CAPABILITIES_REGISTRY.register(SwingCapability) STATE_CAPABILITIES_REGISTRY.register(ProgramCapabilityClimate) STATE_CAPABILITIES_REGISTRY.register(ProgramCapabilityHumidifier) STATE_CAPABILITIES_REGISTRY.register(ProgramCapabilityFan) STATE_CAPABILITIES_REGISTRY.register(InputSourceCapability) STATE_CAPABILITIES_REGISTRY.register(FanSpeedCapabilityClimate) STATE_CAPABILITIES_REGISTRY.register(FanSpeedCapabilityFanViaPreset) STATE_CAPABILITIES_REGISTRY.register(FanSpeedCapabilityFanViaPercentage) STATE_CAPABILITIES_REGISTRY.register(CleanupModeCapability)