460 lines
13 KiB
Python
460 lines
13 KiB
Python
"""Implement the Yandex Smart Home on_off capability."""
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
import logging
|
|
from typing import Any
|
|
|
|
from homeassistant.components import (
|
|
automation,
|
|
button,
|
|
climate,
|
|
cover,
|
|
fan,
|
|
group,
|
|
humidifier,
|
|
input_boolean,
|
|
input_button,
|
|
light,
|
|
lock,
|
|
media_player,
|
|
scene,
|
|
script,
|
|
switch,
|
|
vacuum,
|
|
water_heater,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
ATTR_SUPPORTED_FEATURES,
|
|
SERVICE_CLOSE_COVER,
|
|
SERVICE_LOCK,
|
|
SERVICE_OPEN_COVER,
|
|
SERVICE_TURN_OFF,
|
|
SERVICE_TURN_ON,
|
|
SERVICE_UNLOCK,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
)
|
|
from homeassistant.core import DOMAIN as HA_DOMAIN
|
|
from homeassistant.exceptions import ServiceNotFound
|
|
from homeassistant.helpers.service import async_call_from_config
|
|
|
|
from . import const
|
|
from .capability import PREFIX_CAPABILITIES, AbstractCapability, register_capability
|
|
from .const import ERR_NOT_SUPPORTED_IN_CURRENT_MODE
|
|
from .error import SmartHomeError
|
|
from .helpers import RequestData
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CAPABILITIES_ONOFF = PREFIX_CAPABILITIES + 'on_off'
|
|
|
|
|
|
class OnOffCapability(AbstractCapability, ABC):
|
|
"""On_off to offer basic on and off functionality.
|
|
|
|
https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/on_off-docpage/
|
|
"""
|
|
|
|
type = CAPABILITIES_ONOFF
|
|
instance = const.ON_OFF_INSTANCE_ON
|
|
|
|
@property
|
|
def retrievable(self) -> bool:
|
|
if self.entity_config.get(const.CONF_STATE_UNKNOWN):
|
|
return False
|
|
|
|
return True
|
|
|
|
def parameters(self) -> dict[str, Any] | None:
|
|
"""Return parameters for a devices request."""
|
|
if not self.retrievable:
|
|
return {'split': True}
|
|
|
|
return None
|
|
|
|
def get_value(self) -> bool | None:
|
|
return self.state.state != STATE_OFF
|
|
|
|
async def set_state(self, data: RequestData, state: dict[str, Any]):
|
|
for key, call in ((const.CONF_TURN_ON, state['value']), (const.CONF_TURN_OFF, not state['value'])):
|
|
if key in self.entity_config and call:
|
|
return await async_call_from_config(
|
|
self.hass,
|
|
self.entity_config[key],
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
await self._set_state(data, state)
|
|
|
|
@abstractmethod
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
pass
|
|
|
|
|
|
class OnlyOnCapability(OnOffCapability, ABC):
|
|
@property
|
|
def retrievable(self) -> bool:
|
|
return False
|
|
|
|
def parameters(self) -> dict[str, Any] | None:
|
|
return None
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityBasic(OnOffCapability):
|
|
def supported(self) -> bool:
|
|
return self.state.domain in (light.DOMAIN, fan.DOMAIN, switch.DOMAIN, humidifier.DOMAIN, input_boolean.DOMAIN)
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_TURN_ON
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
self.state.domain,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityAutomation(OnOffCapability):
|
|
def get_value(self) -> bool:
|
|
return self.state.state == STATE_ON
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == automation.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_TURN_ON
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
automation.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityGroup(OnOffCapability):
|
|
def supported(self) -> bool:
|
|
return self.state.domain in group.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_TURN_ON
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
HA_DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityScript(OnlyOnCapability):
|
|
def get_value(self) -> bool | None:
|
|
return None
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain in (scene.DOMAIN, script.DOMAIN)
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
await self.hass.services.async_call(
|
|
self.state.domain,
|
|
SERVICE_TURN_ON, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=self.state.domain != script.DOMAIN,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityButton(OnlyOnCapability):
|
|
def get_value(self) -> bool | None:
|
|
return None
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == button.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
await self.hass.services.async_call(
|
|
self.state.domain,
|
|
button.SERVICE_PRESS, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityInputButton(OnlyOnCapability):
|
|
def get_value(self) -> bool | None:
|
|
return None
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == input_button.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
await self.hass.services.async_call(
|
|
self.state.domain,
|
|
input_button.SERVICE_PRESS, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityLock(OnOffCapability):
|
|
def get_value(self) -> bool:
|
|
return self.state.state == lock.STATE_UNLOCKED
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == lock.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_UNLOCK
|
|
else:
|
|
service = SERVICE_LOCK
|
|
|
|
await self.hass.services.async_call(
|
|
lock.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityCover(OnOffCapability):
|
|
def get_value(self) -> bool:
|
|
return self.state.state == cover.STATE_OPEN
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == cover.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_OPEN_COVER
|
|
else:
|
|
service = SERVICE_CLOSE_COVER
|
|
|
|
await self.hass.services.async_call(
|
|
cover.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityMediaPlayer(OnOffCapability):
|
|
def supported(self) -> bool:
|
|
if self.state.domain == media_player.DOMAIN:
|
|
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if const.CONF_TURN_ON in self.entity_config or const.CONF_TURN_OFF in self.entity_config:
|
|
return True
|
|
|
|
if const.MEDIA_PLAYER_FEATURE_TURN_ON_OFF in self.entity_config.get(const.CONF_FEATURES, []):
|
|
return True
|
|
|
|
return features & media_player.MediaPlayerEntityFeature.TURN_ON or \
|
|
features & media_player.MediaPlayerEntityFeature.TURN_OFF
|
|
|
|
return False
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = SERVICE_TURN_ON
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
media_player.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityVacuum(OnOffCapability):
|
|
def get_value(self) -> bool | None:
|
|
return self.state.state in [STATE_ON, vacuum.STATE_CLEANING]
|
|
|
|
def supported(self) -> bool:
|
|
if self.state.domain != vacuum.DOMAIN:
|
|
return False
|
|
|
|
if const.CONF_TURN_ON in self.entity_config:
|
|
return True
|
|
|
|
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
|
|
|
if features & vacuum.VacuumEntityFeature.TURN_ON and features & vacuum.VacuumEntityFeature.TURN_OFF:
|
|
return True
|
|
|
|
if features & vacuum.VacuumEntityFeature.START:
|
|
if features & vacuum.VacuumEntityFeature.RETURN_HOME or features & vacuum.VacuumEntityFeature.STOP:
|
|
return True
|
|
|
|
return False
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
|
|
|
if state['value']:
|
|
if features & vacuum.VacuumEntityFeature.START:
|
|
service = vacuum.SERVICE_START
|
|
else:
|
|
service = SERVICE_TURN_ON
|
|
else:
|
|
if features & vacuum.VacuumEntityFeature.RETURN_HOME:
|
|
service = vacuum.SERVICE_RETURN_TO_BASE
|
|
elif features & vacuum.VacuumEntityFeature.STOP:
|
|
service = vacuum.SERVICE_STOP
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
vacuum.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityClimate(OnOffCapability):
|
|
def get_value(self) -> bool | None:
|
|
return self.state.state != climate.HVAC_MODE_OFF
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == climate.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
service_data = {
|
|
ATTR_ENTITY_ID: self.state.entity_id
|
|
}
|
|
|
|
if state['value']:
|
|
service = SERVICE_TURN_ON
|
|
|
|
hvac_modes = self.state.attributes.get(climate.ATTR_HVAC_MODES)
|
|
for mode in (climate.HVACMode.HEAT_COOL,
|
|
climate.HVACMode.AUTO):
|
|
if mode not in hvac_modes:
|
|
continue
|
|
|
|
service_data[climate.ATTR_HVAC_MODE] = mode
|
|
service = climate.SERVICE_SET_HVAC_MODE
|
|
break
|
|
else:
|
|
service = SERVICE_TURN_OFF
|
|
|
|
await self.hass.services.async_call(
|
|
climate.DOMAIN,
|
|
service,
|
|
service_data,
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
|
|
@register_capability
|
|
class OnOffCapabilityWaterHeater(OnOffCapability):
|
|
water_heater_operations = {
|
|
STATE_ON: [STATE_ON, 'On', 'ON', water_heater.STATE_ELECTRIC],
|
|
STATE_OFF: [STATE_OFF, 'Off', 'OFF'],
|
|
}
|
|
|
|
def get_value(self) -> bool | None:
|
|
return self.state.state.lower() != water_heater.STATE_OFF
|
|
|
|
def get_water_heater_operation(self, required_mode: str, operations_list: list[str]) -> str | None:
|
|
for operation in self.water_heater_operations[required_mode]:
|
|
if operation in operations_list:
|
|
return operation
|
|
|
|
return None
|
|
|
|
def supported(self) -> bool:
|
|
return self.state.domain == water_heater.DOMAIN
|
|
|
|
async def _set_state(self, data: RequestData, state: dict[str, Any]):
|
|
if state['value']:
|
|
service = water_heater.SERVICE_TURN_ON
|
|
else:
|
|
service = water_heater.SERVICE_TURN_OFF
|
|
|
|
try:
|
|
await self.hass.services.async_call(
|
|
water_heater.DOMAIN,
|
|
service, {
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|
|
|
|
return
|
|
except (AttributeError, ServiceNotFound):
|
|
# turn_on/turn_off is not supported
|
|
pass
|
|
|
|
operation_list = self.state.attributes.get(water_heater.ATTR_OPERATION_LIST)
|
|
|
|
if state['value']:
|
|
mode = self.get_water_heater_operation(STATE_ON, operation_list)
|
|
else:
|
|
mode = self.get_water_heater_operation(STATE_OFF, operation_list)
|
|
|
|
if not mode:
|
|
target_state_text = 'on' if state['value'] else 'off'
|
|
raise SmartHomeError(
|
|
ERR_NOT_SUPPORTED_IN_CURRENT_MODE,
|
|
f'Unable to determine operation mode for {target_state_text} state'
|
|
)
|
|
|
|
await self.hass.services.async_call(
|
|
water_heater.DOMAIN,
|
|
water_heater.SERVICE_SET_OPERATION_MODE, {
|
|
ATTR_ENTITY_ID: self.state.entity_id,
|
|
water_heater.ATTR_OPERATION_MODE: mode
|
|
},
|
|
blocking=True,
|
|
context=data.context
|
|
)
|