first commit
This commit is contained in:
@@ -0,0 +1,757 @@
|
||||
"""Implement the Yandex Smart Home modes capabilities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import climate, fan, humidifier, media_player, vacuum
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.util.percentage import ordered_list_item_to_percentage, percentage_to_ordered_list_item
|
||||
|
||||
from . import const
|
||||
from .capability import PREFIX_CAPABILITIES, AbstractCapability, register_capability
|
||||
from .const import CONF_ENTITY_MODE_MAP, ERR_INVALID_VALUE, STATE_NONE
|
||||
from .error import SmartHomeError
|
||||
from .helpers import RequestData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CAPABILITIES_MODE = PREFIX_CAPABILITIES + 'mode'
|
||||
|
||||
|
||||
class ModeCapability(AbstractCapability, ABC):
|
||||
"""Base class of capabilities with mode functionality like thermostat mode
|
||||
or fan speed.
|
||||
|
||||
https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/mode-docpage/
|
||||
"""
|
||||
|
||||
type = CAPABILITIES_MODE
|
||||
modes_map_default: dict[str, list[str]] = {}
|
||||
modes_map_index_fallback: dict[int, str] = {}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
return bool(self.supported_yandex_modes)
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Return parameters for a devices request."""
|
||||
return {
|
||||
'instance': self.instance,
|
||||
'modes': [{'value': v} for v in self.supported_yandex_modes],
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_yandex_modes(self) -> list[str]:
|
||||
"""Returns list of supported Yandex modes for this entity."""
|
||||
modes = []
|
||||
|
||||
for ha_value in self.supported_ha_modes:
|
||||
value = self.get_yandex_mode_by_ha_mode(ha_value, hide_warnings=True)
|
||||
if value is not None and value not in modes:
|
||||
modes.append(value)
|
||||
|
||||
return modes
|
||||
|
||||
@property
|
||||
def supported_ha_modes(self) -> list[str]:
|
||||
"""Returns list of supported HA modes for this entity."""
|
||||
return self.state.attributes.get(self.modes_list_attribute, []) or []
|
||||
|
||||
@property
|
||||
def modes_map(self) -> dict[str, list[str]]:
|
||||
"""Return modes mapping between Yandex and HA."""
|
||||
return self.modes_map_config or self.modes_map_default
|
||||
|
||||
@property
|
||||
def modes_map_config(self) -> dict[str, list[str]] | None:
|
||||
"""Return modes mapping from entity configuration."""
|
||||
if CONF_ENTITY_MODE_MAP in self.entity_config:
|
||||
return self.entity_config[CONF_ENTITY_MODE_MAP].get(self.instance)
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return None
|
||||
|
||||
def get_yandex_mode_by_ha_mode(self, ha_mode: str, hide_warnings=False) -> str | None:
|
||||
rv = None
|
||||
for yandex_mode, names in self.modes_map.items():
|
||||
lower_names = [str(n).lower() for n in names]
|
||||
if str(ha_mode).lower() in lower_names:
|
||||
rv = yandex_mode
|
||||
break
|
||||
|
||||
if rv is None and self.modes_map_config is None and self.modes_map_index_fallback:
|
||||
try:
|
||||
rv = self.modes_map_index_fallback[self.supported_ha_modes.index(ha_mode)]
|
||||
except (IndexError, ValueError, KeyError):
|
||||
pass
|
||||
|
||||
if rv is not None and ha_mode not in self.supported_ha_modes:
|
||||
err = f'Unsupported HA mode "{ha_mode}" for {self.instance} instance of {self.state.entity_id}.'
|
||||
if self.modes_list_attribute:
|
||||
err += f' Maybe it missing in entity attribute {self.modes_list_attribute}?'
|
||||
|
||||
raise SmartHomeError(ERR_INVALID_VALUE, err)
|
||||
|
||||
if rv is None and not hide_warnings:
|
||||
if str(ha_mode).lower() not in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_NONE):
|
||||
if str(ha_mode).lower() in [str(m).lower() for m in self.supported_ha_modes]:
|
||||
_LOGGER.warning(
|
||||
f'Unable to get Yandex mode for "{ha_mode}" for {self.instance} instance '
|
||||
f'of {self.state.entity_id}. It may cause inconsistencies between Yandex and HA. '
|
||||
f'Check \"modes\" setting for this entity'
|
||||
)
|
||||
|
||||
return rv
|
||||
|
||||
def get_ha_mode_by_yandex_mode(self, yandex_mode: str) -> str:
|
||||
ha_modes = self.modes_map.get(yandex_mode, [])
|
||||
for ha_mode in ha_modes:
|
||||
for am in self.supported_ha_modes:
|
||||
if str(am).lower() == str(ha_mode).lower():
|
||||
return am
|
||||
|
||||
if self.modes_map_config is None:
|
||||
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 SmartHomeError(
|
||||
ERR_INVALID_VALUE,
|
||||
f'Unsupported mode "{yandex_mode}" for {self.instance} instance of {self.state.entity_id}. '
|
||||
f'Check \"modes\" setting for this entity'
|
||||
)
|
||||
|
||||
def get_value(self) -> str | None:
|
||||
"""Return the state value of this capability for this entity."""
|
||||
ha_mode = self.state.state
|
||||
if self.state_value_attribute:
|
||||
ha_mode = self.state.attributes.get(self.state_value_attribute)
|
||||
return self.get_yandex_mode_by_ha_mode(ha_mode, False)
|
||||
|
||||
|
||||
@register_capability
|
||||
class ThermostatCapability(ModeCapability):
|
||||
"""Thermostat functionality"""
|
||||
|
||||
instance = const.MODE_INSTANCE_THERMOSTAT
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_HEAT: [climate.HVACMode.HEAT],
|
||||
const.MODE_INSTANCE_MODE_COOL: [climate.HVACMode.COOL],
|
||||
const.MODE_INSTANCE_MODE_AUTO: [climate.HVACMode.HEAT_COOL, climate.HVACMode.AUTO],
|
||||
const.MODE_INSTANCE_MODE_DRY: [climate.HVACMode.DRY],
|
||||
const.MODE_INSTANCE_MODE_FAN_ONLY: [climate.HVACMode.FAN_ONLY],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
if self.state.domain == climate.DOMAIN:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return climate.ATTR_HVAC_MODES
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class SwingCapability(ModeCapability):
|
||||
"""Swing functionality"""
|
||||
|
||||
instance = const.MODE_INSTANCE_SWING
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_VERTICAL: [climate.const.SWING_VERTICAL, 'ud'],
|
||||
const.MODE_INSTANCE_MODE_HORIZONTAL: [climate.const.SWING_HORIZONTAL, 'lr'],
|
||||
const.MODE_INSTANCE_MODE_STATIONARY: [climate.const.SWING_OFF],
|
||||
const.MODE_INSTANCE_MODE_AUTO: [climate.const.SWING_BOTH, 'all']
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == climate.DOMAIN and features & climate.ClimateEntityFeature.SWING_MODE:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return climate.ATTR_SWING_MODES
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return climate.ATTR_SWING_MODE
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
class ProgramCapability(ModeCapability, ABC):
|
||||
"""Program functionality"""
|
||||
|
||||
instance = const.MODE_INSTANCE_PROGRAM
|
||||
modes_map_index_fallback = {
|
||||
0: const.MODE_INSTANCE_MODE_ONE,
|
||||
1: const.MODE_INSTANCE_MODE_TWO,
|
||||
2: const.MODE_INSTANCE_MODE_THREE,
|
||||
3: const.MODE_INSTANCE_MODE_FOUR,
|
||||
4: const.MODE_INSTANCE_MODE_FIVE,
|
||||
5: const.MODE_INSTANCE_MODE_SIX,
|
||||
6: const.MODE_INSTANCE_MODE_SEVEN,
|
||||
7: const.MODE_INSTANCE_MODE_EIGHT,
|
||||
8: const.MODE_INSTANCE_MODE_NINE,
|
||||
9: const.MODE_INSTANCE_MODE_TEN,
|
||||
}
|
||||
|
||||
|
||||
@register_capability
|
||||
class ProgramCapabilityHumidifier(ProgramCapability):
|
||||
"""Program functionality"""
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_FAN_ONLY: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_FAN,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_AUTO: [
|
||||
humidifier.const.MODE_AUTO,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_ECO: [
|
||||
humidifier.const.MODE_ECO,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_IDLE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_QUIET: [
|
||||
humidifier.const.MODE_SLEEP,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_SILENT,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_LOW: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_LOW,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MIN: [
|
||||
humidifier.const.MODE_AWAY
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MEDIUM: [
|
||||
humidifier.const.MODE_COMFORT,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_MEDIUM,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_MIDDLE,
|
||||
const.XIAOMI_HUMIDIFIER_PRESET_MID,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_NORMAL: [
|
||||
humidifier.const.MODE_NORMAL,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_FAVORITE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MAX: [
|
||||
humidifier.const.MODE_HOME,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_HIGH: [
|
||||
humidifier.const.MODE_BABY,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_HIGH,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_TURBO: [
|
||||
humidifier.const.MODE_BOOST,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_STRONG,
|
||||
],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == humidifier.DOMAIN and features & humidifier.HumidifierEntityFeature.MODES:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return humidifier.ATTR_AVAILABLE_MODES
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return humidifier.ATTR_MODE
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class ProgramCapabilityFan(ProgramCapability):
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_AUTO: [
|
||||
climate.const.FAN_AUTO,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_ECO: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_IDLE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_QUIET: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_SILENT,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_1,
|
||||
const.XIAOMI_FAN_PRESET_NATURE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_LOW: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_LOW,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_2,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MEDIUM: [
|
||||
const.XIAOMI_HUMIDIFIER_PRESET_MID,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_3,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_NORMAL: [
|
||||
const.XIAOMI_FAN_PRESET_NORMAL,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_FAVORITE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_HIGH: [
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_4,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_TURBO: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_STRONG,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_5,
|
||||
],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == fan.DOMAIN:
|
||||
if features & fan.FanEntityFeature.PRESET_MODE:
|
||||
if features & fan.FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return fan.ATTR_PRESET_MODE
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return fan.ATTR_PRESET_MODES
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class InputSourceCapability(ModeCapability):
|
||||
"""Input Source functionality"""
|
||||
|
||||
instance = const.MODE_INSTANCE_INPUT_SOURCE
|
||||
modes_map_index_fallback = {
|
||||
0: const.MODE_INSTANCE_MODE_ONE,
|
||||
1: const.MODE_INSTANCE_MODE_TWO,
|
||||
2: const.MODE_INSTANCE_MODE_THREE,
|
||||
3: const.MODE_INSTANCE_MODE_FOUR,
|
||||
4: const.MODE_INSTANCE_MODE_FIVE,
|
||||
5: const.MODE_INSTANCE_MODE_SIX,
|
||||
6: const.MODE_INSTANCE_MODE_SEVEN,
|
||||
7: const.MODE_INSTANCE_MODE_EIGHT,
|
||||
8: const.MODE_INSTANCE_MODE_NINE,
|
||||
9: const.MODE_INSTANCE_MODE_TEN,
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == media_player.DOMAIN:
|
||||
if const.MEDIA_PLAYER_FEATURE_SELECT_SOURCE in self.entity_config.get(const.CONF_FEATURES, []):
|
||||
return True
|
||||
|
||||
if features & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_ha_modes(self) -> list[str]:
|
||||
"""Returns list of supported HA modes for this entity."""
|
||||
modes = self.state.attributes.get(self.modes_list_attribute, []) or []
|
||||
if modes or self.state.state not in (STATE_OFF, STATE_UNKNOWN):
|
||||
self._cache.save_attr_value(self.state.entity_id, self.modes_list_attribute, modes)
|
||||
return modes
|
||||
|
||||
return self._cache.get_attr_value(self.state.entity_id, self.modes_list_attribute) or []
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return media_player.ATTR_INPUT_SOURCE_LIST
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return media_player.ATTR_INPUT_SOURCE
|
||||
|
||||
def get_yandex_mode_by_ha_mode(self, ha_mode: str, hide_warnings=False) -> str | None:
|
||||
return super().get_yandex_mode_by_ha_mode(ha_mode, hide_warnings=True)
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
class FanSpeedCapability(ModeCapability, ABC):
|
||||
"""Fan speed functionality."""
|
||||
|
||||
instance = const.MODE_INSTANCE_FAN_SPEED
|
||||
|
||||
|
||||
@register_capability
|
||||
class FanSpeedCapabilityClimate(FanSpeedCapability):
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_AUTO: [
|
||||
climate.const.FAN_AUTO,
|
||||
climate.const.FAN_ON,
|
||||
const.SMARTTHINQ_FAN_PRESET_NATURE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_QUIET: [
|
||||
climate.const.FAN_OFF,
|
||||
climate.const.FAN_DIFFUSE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MIN: [
|
||||
const.TION_FAN_SPEED_1,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_LOW: [
|
||||
climate.const.FAN_LOW,
|
||||
const.FAN_SPEED_MIN,
|
||||
const.TION_FAN_SPEED_2,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MEDIUM: [
|
||||
climate.const.FAN_MEDIUM,
|
||||
climate.const.FAN_MIDDLE,
|
||||
const.FAN_SPEED_MID,
|
||||
const.TION_FAN_SPEED_3,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_HIGH: [
|
||||
climate.const.FAN_HIGH,
|
||||
const.FAN_SPEED_MAX,
|
||||
const.TION_FAN_SPEED_4,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_TURBO: [
|
||||
climate.const.FAN_FOCUS,
|
||||
const.FAN_SPEED_HIGHEST,
|
||||
const.TION_FAN_SPEED_5,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MAX: [
|
||||
const.TION_FAN_SPEED_6,
|
||||
],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == climate.DOMAIN and features & climate.ClimateEntityFeature.FAN_MODE:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return climate.ATTR_FAN_MODE
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return climate.ATTR_FAN_MODES
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class FanSpeedCapabilityFanViaPreset(FanSpeedCapability):
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_AUTO: [
|
||||
climate.const.FAN_AUTO,
|
||||
climate.const.FAN_ON,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_ECO: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_IDLE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_QUIET: [
|
||||
const.FAN_SPEED_OFF,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_SILENT,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_1,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_LOW: [
|
||||
const.FAN_SPEED_LOW,
|
||||
const.FAN_SPEED_MIN,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_2,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_MEDIUM: [
|
||||
const.FAN_SPEED_MEDIUM,
|
||||
const.XIAOMI_HUMIDIFIER_PRESET_MID,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_3,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_NORMAL: [
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_FAVORITE,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_HIGH: [
|
||||
const.FAN_SPEED_HIGH,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_4,
|
||||
],
|
||||
const.MODE_INSTANCE_MODE_TURBO: [
|
||||
const.FAN_SPEED_MAX,
|
||||
const.XIAOMI_AIRPURIFIER_PRESET_STRONG,
|
||||
const.XIAOMI_FAN_PRESET_LEVEL_5,
|
||||
],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == fan.DOMAIN:
|
||||
if features & fan.FanEntityFeature.PRESET_MODE:
|
||||
if features & fan.FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes:
|
||||
return False
|
||||
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return fan.ATTR_PRESET_MODE
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return fan.ATTR_PRESET_MODES
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class FanSpeedCapabilityFanViaPercentage(FanSpeedCapability):
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == fan.DOMAIN:
|
||||
if features & fan.FanEntityFeature.SET_SPEED and fan.ATTR_PERCENTAGE_STEP in self.state.attributes:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_ha_modes(self) -> list[str]:
|
||||
if self.modes_map:
|
||||
return list(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 = [const.MODE_INSTANCE_MODE_LOW, const.MODE_INSTANCE_MODE_HIGH]
|
||||
if speed_count >= 3:
|
||||
modes.insert(modes.index(const.MODE_INSTANCE_MODE_HIGH), const.MODE_INSTANCE_MODE_MEDIUM)
|
||||
if speed_count >= 4:
|
||||
modes.insert(modes.index(const.MODE_INSTANCE_MODE_MEDIUM), const.MODE_INSTANCE_MODE_NORMAL)
|
||||
if speed_count >= 5:
|
||||
modes.insert(0, const.MODE_INSTANCE_MODE_ECO)
|
||||
if speed_count >= 6:
|
||||
modes.insert(modes.index(const.MODE_INSTANCE_MODE_LOW), const.MODE_INSTANCE_MODE_QUIET)
|
||||
if speed_count >= 7:
|
||||
modes.append(const.MODE_INSTANCE_MODE_TURBO)
|
||||
|
||||
return modes
|
||||
|
||||
@property
|
||||
def supported_yandex_modes(self) -> list[str]:
|
||||
return self.supported_ha_modes
|
||||
|
||||
def get_value(self) -> str | None:
|
||||
value = self.state.attributes.get(fan.ATTR_PERCENTAGE)
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if self.modes_map:
|
||||
for yandex_mode, values in self.modes_map.items():
|
||||
for str_value in values:
|
||||
if int(value) == self._convert_mapping_speed_value(str_value):
|
||||
return yandex_mode
|
||||
|
||||
return None
|
||||
|
||||
return percentage_to_ordered_list_item(self.supported_ha_modes, value)
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
return None
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device state."""
|
||||
yandex_mode = state['value']
|
||||
|
||||
if self.modes_map:
|
||||
ha_modes = self.modes_map.get(yandex_mode)
|
||||
if not ha_modes:
|
||||
raise SmartHomeError(
|
||||
ERR_INVALID_VALUE,
|
||||
f'Unsupported mode "{yandex_mode}" for {self.instance} instance of {self.state.entity_id}. '
|
||||
f'Check \"modes\" setting for this entity'
|
||||
)
|
||||
|
||||
ha_mode = self._convert_mapping_speed_value(ha_modes[0])
|
||||
else:
|
||||
ha_mode = ordered_list_item_to_percentage(self.supported_ha_modes, yandex_mode)
|
||||
|
||||
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=True,
|
||||
context=data.context
|
||||
)
|
||||
|
||||
def _convert_mapping_speed_value(self, value: str) -> int:
|
||||
try:
|
||||
return int(value.replace('%', ''))
|
||||
except ValueError:
|
||||
raise SmartHomeError(
|
||||
ERR_INVALID_VALUE,
|
||||
f'Unsupported speed value {value!r} for {self.instance} '
|
||||
f'instance of {self.state.entity_id}.'
|
||||
)
|
||||
|
||||
|
||||
@register_capability
|
||||
class CleanupModeCapability(ModeCapability):
|
||||
"""Vacuum cleanup mode functionality."""
|
||||
|
||||
instance = const.MODE_INSTANCE_CLEANUP_MODE
|
||||
modes_map_default = {
|
||||
const.MODE_INSTANCE_MODE_AUTO: ['auto', 'automatic', '102'],
|
||||
const.MODE_INSTANCE_MODE_TURBO: ['turbo', 'high', 'performance', '104', 'full speed', 'max+'],
|
||||
const.MODE_INSTANCE_MODE_MIN: ['min', 'mop'],
|
||||
const.MODE_INSTANCE_MODE_LOW: ['gentle'],
|
||||
const.MODE_INSTANCE_MODE_MAX: ['max', 'strong'],
|
||||
const.MODE_INSTANCE_MODE_EXPRESS: ['express', '105'],
|
||||
const.MODE_INSTANCE_MODE_MEDIUM: ['medium', 'middle'],
|
||||
const.MODE_INSTANCE_MODE_NORMAL: ['normal', 'standard', 'basic', '103'],
|
||||
const.MODE_INSTANCE_MODE_QUIET: ['quiet', 'low', 'min', 'silent', 'eco', '101'],
|
||||
}
|
||||
|
||||
def supported(self) -> bool:
|
||||
"""Test if capability is supported."""
|
||||
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if self.state.domain == vacuum.DOMAIN and features & vacuum.VacuumEntityFeature.FAN_SPEED:
|
||||
return super().supported()
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def modes_list_attribute(self) -> str | None:
|
||||
"""Return HA attribute contains modes list for this entity."""
|
||||
return vacuum.ATTR_FAN_SPEED_LIST
|
||||
|
||||
@property
|
||||
def state_value_attribute(self) -> str | None:
|
||||
"""Return HA attribute for state of this entity."""
|
||||
return vacuum.ATTR_FAN_SPEED
|
||||
|
||||
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
||||
"""Set device 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=True,
|
||||
context=data.context
|
||||
)
|
||||
Reference in New Issue
Block a user