Initialize docker stack repo

This commit is contained in:
g4st3r
2026-03-07 17:28:59 +00:00
commit 319d4eeb3e
458 changed files with 67215 additions and 0 deletions

View File

@@ -0,0 +1,971 @@
"""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):
"""Generic HA mode for various devices."""
GENTLE = "gentle" # tuya vacuum?
MAX_PLUS_SIGN = "max+" # deebot?
HIGHEST = "highest" # smartir
class SmartThinQFanMode(StrEnum):
"""Fan mode for ollo69/ha-smartthinq-sensors integration."""
LOW_MID = "low_mid"
MID_HIGH = "mid_high"
class RoborockCleanupMode(StrEnum):
"""Cleanup mode for humbertogontijo/python-roborock library.
https://github.com/humbertogontijo/python-roborock/blob/1616217a06e20d51921de984134555bcc0775a92/roborock/code_mappings.py#L61
"""
OFF = "off"
SILENT = "silent"
BALANCED = "balanced"
TURBO = "turbo"
MAX = "max"
MAX_PLUS = "max_plus"
CUSTOM = "custom"
class RoombaCleanupMode(StrEnum):
"""Cleanup mode for roomba integration."""
AUTOMATIC = "Automatic"
ECO = "Eco"
PERFORMANCE = "Performance"
STANDARD = "Standard"
class TionFanSpeed(StrEnum):
"""Fan speed for airens/tion_home_assistant integration.
https://github.com/airens/tion_home_assistant#climateset_fan_mode
"""
S1 = "1"
S2 = "2"
S3 = "3"
S4 = "4"
S5 = "5"
S6 = "6"
class XiaomiHumidifierMode(StrEnum):
"""Humidifer mode for xiaomi_miio integration."""
MID = "mid"
class XiaomiMiotHumidifierMode(StrEnum):
"""Humidifer mode for al-one/hass-xiaomi-miot integration."""
CONST_HUMIDITY = "Const Humidity" # leshow.humidifier.jsq1
class XiaomiFanMode(StrEnum):
"""Fan mode for xiaomi_miio integration."""
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):
"""Fan mode for al-one/hass-xiaomi-miot and syssi/xiaomi_airpurifier integrations.
https://github.com/syssi/xiaomi_airpurifier#service-fanset_preset_mode
https://github.com/al-one/hass-xiaomi-miot/blob/fdca601c409f619b1c98a20e6ea990317cce20c7/custom_components/xiaomi_miot/core/templates.py#L102
"""
LEVEL_1 = "Level 1"
LEVEL_2 = "Level 2"
LEVEL_3 = "Level 3"
LEVEL_4 = "Level 4"
LEVEL_5 = "Level 5"
class XiaomiMiotCleanupMode(StrEnum):
"""Cleanup mode for al-one/hass-xiaomi-miot integration.
https://github.com/al-one/hass-xiaomi-miot/blob/fdca601c409f619b1c98a20e6ea990317cce20c7/custom_components/xiaomi_miot/core/miot_specs_extend.json#L641
"""
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.
https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/mode-docpage/
"""
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 list(map(str, 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."""
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
if mode is not None and ha_mode not in self.supported_ha_modes:
raise APIError(
ResponseCode.INVALID_VALUE,
f"Unsupported HA mode '{ha_mode}' for {self}: not in {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:
mode = self._modes_map_index_fallback[self.supported_ha_modes.index(ha_mode)]
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 [m.lower() for m in self.supported_ha_modes]:
_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:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(climate.ATTR_SWING_MODES, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(climate.ATTR_PRESET_MODES, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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 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(
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(fan.ATTR_PRESET_MODES, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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 Yandex mode for HA mode."""
return super().get_yandex_mode_by_ha_mode(ha_mode, hide_warnings=True)
async def set_instance_state(self, context: Context, state: ModeCapabilityInstanceActionState) -> None:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
modes = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, []) or []
filtered_modes = list(filter(lambda m: m not in ["Live TV"], modes)) # #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 the current unmapped capability value."""
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,
],
ModeCapabilityMode.QUIET: [
climate.FAN_OFF,
climate.FAN_DIFFUSE,
],
ModeCapabilityMode.MIN: [
TionFanSpeed.S1,
SmartThinQFanMode.LOW_MID,
],
ModeCapabilityMode.LOW: [
climate.FAN_LOW,
TionFanSpeed.S2,
],
ModeCapabilityMode.MEDIUM: [
climate.FAN_MEDIUM,
climate.FAN_MIDDLE,
XiaomiHumidifierMode.MID,
TionFanSpeed.S3,
],
ModeCapabilityMode.HIGH: [
climate.FAN_HIGH,
TionFanSpeed.S4,
],
ModeCapabilityMode.TURBO: [
climate.FAN_FOCUS,
GenericMode.HIGHEST,
TionFanSpeed.S5,
],
ModeCapabilityMode.MAX: [
TionFanSpeed.S6,
SmartThinQFanMode.MID_HIGH,
],
}
@property
def supported(self) -> bool:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
modes = self.state.attributes.get(climate.ATTR_FAN_MODES, []) or []
# esphome default state for some devices
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 the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(fan.ATTR_PRESET_MODES, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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]:
"""Returns a list of supported Yandex modes."""
return [ModeCapabilityMode(m) for m in self.supported_ha_modes]
def get_value(self) -> ModeCapabilityMode | None:
"""Return the current capability value."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
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 the current unmapped capability value."""
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:
"""Test if the capability is supported."""
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:
"""Change the capability state."""
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]:
"""Returns list of HA modes."""
return self.state.attributes.get(vacuum.ATTR_FAN_SPEED_LIST, []) or []
@property
def _ha_value(self) -> Any:
"""Return the current unmapped capability value."""
return self.state.attributes.get(vacuum.ATTR_FAN_SPEED)
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)