Skip to content

Commit

Permalink
Add on_off custom capability (close #538)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Nov 19, 2024
1 parent 451fa31 commit 2aa24b2
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 93 deletions.
46 changes: 43 additions & 3 deletions custom_components/yandex_smart_home/capability_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
from typing import TYPE_CHECKING, Any, Iterable, Protocol, Self, cast

from homeassistant.const import STATE_OFF, STATE_UNKNOWN
from homeassistant.const import CONF_STATE_TEMPLATE, STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.service import async_call_from_config
Expand All @@ -17,12 +17,12 @@
from .capability import Capability
from .capability_color import ColorSceneCapability
from .capability_mode import ModeCapability
from .capability_onoff import OnOffCapability, OnOffCapabilityInstanceActionState
from .capability_range import RangeCapability
from .capability_toggle import ToggleCapability
from .const import (
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE,
CONF_ENTITY_CUSTOM_MODE_SET_MODE,
CONF_ENTITY_CUSTOM_RANGE_DECREASE_VALUE,
CONF_ENTITY_CUSTOM_RANGE_INCREASE_VALUE,
Expand All @@ -34,6 +34,7 @@
CONF_ENTITY_RANGE_MAX,
CONF_ENTITY_RANGE_MIN,
CONF_ENTITY_RANGE_PRECISION,
CONF_STATE_UNKNOWN,
)
from .helpers import ActionNotAllowed, APIError
from .schema import (
Expand All @@ -44,6 +45,7 @@
ModeCapabilityInstance,
ModeCapabilityInstanceActionState,
ModeCapabilityMode,
OnOffCapabilityInstance,
RangeCapabilityInstance,
RangeCapabilityInstanceActionState,
RangeCapabilityRange,
Expand Down Expand Up @@ -130,6 +132,39 @@ def __repr__(self) -> str:
)


class CustomOnOffCapability(CustomCapability, OnOffCapability):
"""OnOff capability that user can set up using yaml configuration."""

instance: OnOffCapabilityInstance

@property
def supported(self) -> bool:
"""Test if the capability is supported."""
return True

@property
def retrievable(self) -> bool:
"""Test if the capability can return the current value."""
if self._entity_config.get(CONF_STATE_UNKNOWN):
return False

return True

def get_value(self) -> bool | None:
"""Return the current capability value."""
if not self.retrievable:
return None

if self._value_template is not None:
return bool(self._get_source_value() == STATE_ON)

return False

async def _set_instance_state(self, context: Context, state: OnOffCapabilityInstanceActionState) -> None:
"""Change the capability state (if wasn't overriden by the user)."""
raise ActionNotAllowed


class CustomModeCapability(CustomCapability, ModeCapability):
"""Mode capability that user can set up using yaml configuration."""

Expand Down Expand Up @@ -357,6 +392,11 @@ def get_custom_capability(
value_template = get_value_template(hass, device_id, capability_config)

match capability_type:
case CapabilityType.ON_OFF:
return CustomOnOffCapability(
hass, entry_data, capability_config, OnOffCapabilityInstance(instance), device_id, value_template
)

case CapabilityType.MODE:
if instance == ColorSettingCapabilityInstance.SCENE:
return CustomColorSceneCapability(
Expand All @@ -380,7 +420,7 @@ def get_custom_capability(

def get_value_template(hass: HomeAssistant, device_id: str, capability_config: ConfigType) -> Template | None:
"""Return capability value template from capability configuration."""
if template := capability_config.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE):
if template := capability_config.get(CONF_STATE_TEMPLATE):
return cast(Template, template)

entity_id = capability_config.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID)
Expand Down
12 changes: 6 additions & 6 deletions custom_components/yandex_smart_home/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from homeassistant.components.event import EventDeviceClass
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_ROOM, CONF_TYPE
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_ROOM, CONF_STATE_TEMPLATE, CONF_TYPE
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA
from homeassistant.helpers.typing import ConfigType
Expand All @@ -21,7 +21,6 @@
CONF_ENTITY_CONFIG,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID,
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE,
CONF_ENTITY_CUSTOM_MODE_SET_MODE,
CONF_ENTITY_CUSTOM_MODES,
CONF_ENTITY_CUSTOM_RANGE_DECREASE_VALUE,
Expand Down Expand Up @@ -305,7 +304,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
"""Validate keys for custom capability."""
state_entity_id = value.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID)
state_attribute = value.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE)
state_template = value.get(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE)
state_template = value.get(CONF_STATE_TEMPLATE)

if state_template and (state_entity_id or state_attribute):
raise vol.Invalid("state_entity_id/state_attribute and state_template are mutually exclusive")
Expand Down Expand Up @@ -358,7 +357,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_ENTITY_CUSTOM_MODE_SET_MODE): cv.SERVICE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
Expand All @@ -378,7 +377,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_ENTITY_RANGE): ENTITY_RANGE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
Expand All @@ -397,7 +396,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_ENTITY_CUSTOM_TOGGLE_TURN_OFF): cv.SERVICE_SCHEMA,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE): cv.string,
vol.Optional(CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
},
custom_capability_state,
),
Expand All @@ -413,6 +412,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ROOM): cv.string,
vol.Optional(CONF_TYPE): vol.All(cv.string, device_type),
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_TURN_ON): vol.Any(cv.SERVICE_SCHEMA, cv.boolean),
vol.Optional(CONF_TURN_OFF): vol.Any(cv.SERVICE_SCHEMA, cv.boolean),
vol.Optional(CONF_DEVICE_CLASS): vol.In(EventDeviceClass.BUTTON),
Expand Down
1 change: 0 additions & 1 deletion custom_components/yandex_smart_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
CONF_ENTITY_EVENT_MAP = "events"
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ENTITY_ID = "state_entity_id"
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_ATTRIBUTE = "state_attribute"
CONF_ENTITY_CUSTOM_CAPABILITY_STATE_TEMPLATE = "state_template"
CONF_ENTITY_CUSTOM_MODES = "custom_modes"
CONF_ENTITY_CUSTOM_MODE_SET_MODE = "set_mode"
CONF_ENTITY_CUSTOM_TOGGLES = "custom_toggles"
Expand Down
19 changes: 19 additions & 0 deletions custom_components/yandex_smart_home/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_ROOM,
CONF_STATE_TEMPLATE,
CONF_TYPE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Expand Down Expand Up @@ -91,6 +92,7 @@
DeviceInfo,
DeviceState,
DeviceType,
OnOffCapabilityInstance,
PropertyDescription,
PropertyInstanceState,
ResponseCode,
Expand Down Expand Up @@ -190,6 +192,18 @@ def get_capabilities(self) -> list[Capability[Any]]:
capabilities: list[Capability[Any]] = []
disabled_capabilities: list[Capability[Any]] = []

if (state_template := self._config.get(CONF_STATE_TEMPLATE)) is not None:
capabilities.append(
get_custom_capability(
self._hass,
self._entry_data,
{CONF_STATE_TEMPLATE: state_template},
CapabilityType.ON_OFF,
OnOffCapabilityInstance.ON,
self.id,
)
)

for capability_type, config_key in (
(CapabilityType.MODE, CONF_ENTITY_CUSTOM_MODES),
(CapabilityType.TOGGLE, CONF_ENTITY_CUSTOM_TOGGLES),
Expand Down Expand Up @@ -282,8 +296,13 @@ def should_expose(self) -> bool:
return self._entry_data.should_expose(self.id)

@property
@callback
def unavailable(self) -> bool:
"""Test if the device is unavailable."""
state_template: Template | None
if (state_template := self._config.get(CONF_STATE_TEMPLATE)) is not None:
return bool(state_template.async_render() == STATE_UNAVAILABLE)

return self._state.state == STATE_UNAVAILABLE

@property
Expand Down
63 changes: 42 additions & 21 deletions custom_components/yandex_smart_home/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.const import (
CONF_ID,
CONF_PLATFORM,
CONF_STATE_TEMPLATE,
CONF_TOKEN,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
Expand Down Expand Up @@ -58,7 +59,7 @@
from .notifier import CloudNotifier, Notifier, NotifierConfig, YandexDirectNotifier
from .property import StateProperty
from .property_custom import CustomProperty, get_custom_property, get_event_platform_custom_property_type
from .schema import CapabilityType
from .schema import CapabilityType, OnOffCapabilityInstance

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -346,6 +347,34 @@ async def _async_setup_cloud_connection(self) -> None:
self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._cloud_manager.async_disconnect)
)

def _append_trackable_templates_with_capability(
self,
templates: dict[Template, list[CustomCapability | CustomProperty]],
capability_config: ConfigType,
capability_type: CapabilityType,
instance: str,
device_id: str,
) -> None:
"""Append custom capability to list of templates."""
try:
capability = get_custom_capability(
self._hass,
self,
capability_config,
capability_type,
instance,
device_id,
)
except APIError as e:
_LOGGER.debug(f"Failed to track custom capability: {e}")
return

template = capability_custom.get_value_template(self._hass, device_id, capability_config)

if template:
templates.setdefault(template, [])
templates[template].append(capability)

def _get_trackable_templates(self) -> dict[Template, list[CustomCapability | CustomProperty]]:
"""Return templates for track changes."""
templates: dict[Template, list[CustomCapability | CustomProperty]] = {}
Expand All @@ -354,6 +383,15 @@ def _get_trackable_templates(self) -> dict[Template, list[CustomCapability | Cus
if not self.should_expose(device_id):
continue

if (state_template := entity_config.get(CONF_STATE_TEMPLATE)) is not None:
self._append_trackable_templates_with_capability(
templates,
{CONF_STATE_TEMPLATE: state_template},
CapabilityType.ON_OFF,
OnOffCapabilityInstance.ON,
device_id,
)

for capability_type, config_key in (
(CapabilityType.MODE, CONF_ENTITY_CUSTOM_MODES),
(CapabilityType.TOGGLE, CONF_ENTITY_CUSTOM_TOGGLES),
Expand All @@ -362,27 +400,10 @@ def _get_trackable_templates(self) -> dict[Template, list[CustomCapability | Cus
if config_key in entity_config:
for instance in entity_config[config_key]:
capability_config = entity_config[config_key][instance]
if not isinstance(capability_config, dict):
continue

try:
capability = get_custom_capability(
self._hass,
self,
capability_config,
capability_type,
instance,
device_id,
if isinstance(capability_config, dict):
self._append_trackable_templates_with_capability(
templates, capability_config, capability_type, instance, device_id
)
except APIError as e:
_LOGGER.debug(f"Failed to track custom capability: {e}")
continue

template = capability_custom.get_value_template(self._hass, device_id, capability_config)

if template:
templates.setdefault(template, [])
templates[template].append(capability)

for property_config in entity_config.get(CONF_ENTITY_PROPERTIES, []):
try:
Expand Down
22 changes: 22 additions & 0 deletions docs/config/entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
turn_on: false
```

Для добавления функции включения тем объектам, которые изначально её не поддерживают, кроме параметров `turn_on` или `turn_off` необходимо дополнительно добавить параметр [`state_template`](#state_template).

## Поддерживаемые функции (media_player) { id=features }

> Параметр: `features` (только для `media_player`)
Expand Down Expand Up @@ -145,6 +147,26 @@
state_unknown: true
```

## Состояние из шаблона { id=state_template }

> Параметр: `state_unknown`
Включает вычисление состояния устройства не из состояния объекта, а из шаблона. Полезно использовать с параметрами `turn_on` и `turn_off` для добавления функции включения тем устройствам, которые её не поддерживают (например камерам).

!!! example "Пример управления розеткой из карточки камеры"
```yaml
yandex_smart_home:
entity_config:
camera.aquarium:
state_template: '{{ states("switch.camera_aquarium") }}'
turn_on:
service: switch.turn_on
entity_id: switch.camera_aquarium
turn_off:
service: switch.turn_off
entity_id: switch.camera_aquarium
```

## Ограничение уровня громкости { id=range }

> Параметр: `range`
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/valid-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,11 @@ yandex_smart_home:
value_template: '{{ 0 }}'
unit_of_measurement: mmHg
target_unit_of_measurement: bar
camera.pet:
state_template: '{{ states("switch.pet_camera") }}'
turn_on:
service: switch.turn_on
entity_id: switch.pet_camera
turn_off:
service: switch.turn_off
entity_id: switch.pet_camera
Loading

0 comments on commit 2aa24b2

Please sign in to comment.