first commit
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
"""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
|
||||
)
|
||||
Reference in New Issue
Block a user