python
This commit is contained in:
755
custom_components/yandex_smart_home/capability_mode.py
Normal file
755
custom_components/yandex_smart_home/capability_mode.py
Normal file
@@ -0,0 +1,755 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user