Skip to content

Commit

Permalink
Add support for event platform (close #466)
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Nov 13, 2024
1 parent f97ba33 commit 437c268
Show file tree
Hide file tree
Showing 17 changed files with 763 additions and 139 deletions.
4 changes: 2 additions & 2 deletions custom_components/yandex_smart_home/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
from typing import Any

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.helpers import config_validation as cv
Expand Down Expand Up @@ -56,7 +57,6 @@
CONF_SUPPORT_SET_CHANNEL,
CONF_TURN_OFF,
CONF_TURN_ON,
DEVICE_CLASS_BUTTON,
MediaPlayerFeature,
PropertyInstanceType,
)
Expand Down Expand Up @@ -415,7 +415,7 @@ def custom_capability_state(value: ConfigType) -> ConfigType:
vol.Optional(CONF_TYPE): vol.All(cv.string, device_type),
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(DEVICE_CLASS_BUTTON),
vol.Optional(CONF_DEVICE_CLASS): vol.In(EventDeviceClass.BUTTON),
vol.Optional(CONF_FEATURES): vol.All(cv.ensure_list, entity_features),
vol.Optional(CONF_ENTITY_PROPERTIES): [ENTITY_PROPERTY_SCHEMA],
vol.Optional(CONF_SUPPORT_SET_CHANNEL): cv.boolean,
Expand Down
5 changes: 2 additions & 3 deletions custom_components/yandex_smart_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@
ATTR_CAPABILITY = "capability"
ATTR_ERROR_CODE = "error_code"

# Fake device class
DEVICE_CLASS_BUTTON = "button"

# Additional states
STATE_NONE = "none"
STATE_NONE_UI = "-"
Expand Down Expand Up @@ -105,6 +102,8 @@
# https://github.com/ClusterM/skykettle-ha/blob/c1b61c4a22693d6e2b7c2f57a989df418011f2c2/custom_components/skykettle/skykettle.py#L53
SKYKETTLE_MODE_BOIL = "Boil"

type EntityId = str


class ConnectionType(StrEnum):
"""Valid connection type."""
Expand Down
26 changes: 21 additions & 5 deletions custom_components/yandex_smart_home/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
camera,
climate,
cover,
event,
fan,
group,
humidifier,
Expand All @@ -34,6 +35,7 @@
)
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.event import EventDeviceClass
from homeassistant.components.media_player import MediaPlayerDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
Expand All @@ -45,6 +47,7 @@
CONF_ROOM,
CONF_TYPE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Context, HomeAssistant, State, callback
from homeassistant.helpers import area_registry, device_registry, entity_registry
Expand All @@ -59,7 +62,6 @@
CONF_ENTITY_CUSTOM_TOGGLES,
CONF_ENTITY_PROPERTIES,
CONF_ERROR_CODE_TEMPLATE,
DEVICE_CLASS_BUTTON,
)

from . import ( # noqa: F401
Expand All @@ -78,7 +80,7 @@
from .capability_custom import get_custom_capability
from .helpers import ActionNotAllowed, APIError
from .property import STATE_PROPERTIES_REGISTRY, Property, StateProperty
from .property_custom import get_custom_property
from .property_custom import get_custom_property, get_event_platform_custom_property_type
from .schema import (
CapabilityDescription,
CapabilityInstanceAction,
Expand Down Expand Up @@ -107,6 +109,7 @@
camera.DOMAIN: DeviceType.CAMERA,
climate.DOMAIN: DeviceType.THERMOSTAT,
cover.DOMAIN: DeviceType.OPENABLE,
event.DOMAIN: DeviceType.SENSOR,
fan.DOMAIN: DeviceType.VENTILATION_FAN,
group.DOMAIN: DeviceType.SWITCH,
humidifier.DOMAIN: DeviceType.HUMIDIFIER,
Expand Down Expand Up @@ -142,7 +145,7 @@
(cover.DOMAIN, CoverDeviceClass.CURTAIN): DeviceType.OPENABLE_CURTAIN,
(media_player.DOMAIN, MediaPlayerDeviceClass.RECEIVER): DeviceType.MEDIA_DEVICE_RECIEVER,
(media_player.DOMAIN, MediaPlayerDeviceClass.TV): DeviceType.MEDIA_DEVICE_TV,
(sensor.DOMAIN, DEVICE_CLASS_BUTTON): DeviceType.SENSOR_BUTTON,
(sensor.DOMAIN, EventDeviceClass.BUTTON): DeviceType.SENSOR_BUTTON,
(sensor.DOMAIN, SensorDeviceClass.CO): DeviceType.SENSOR_CLIMATE,
(sensor.DOMAIN, SensorDeviceClass.CO2): DeviceType.SENSOR_CLIMATE,
(sensor.DOMAIN, SensorDeviceClass.ENERGY): DeviceType.SMART_METER_ELECTRICITY,
Expand All @@ -157,8 +160,13 @@
(sensor.DOMAIN, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS): DeviceType.SENSOR_CLIMATE,
(sensor.DOMAIN, SensorDeviceClass.WATER): DeviceType.SMART_METER_COLD_WATER,
(switch.DOMAIN, SwitchDeviceClass.OUTLET): DeviceType.SOCKET,
(event.DOMAIN, EventDeviceClass.BUTTON): DeviceType.SENSOR_BUTTON,
(event.DOMAIN, EventDeviceClass.DOORBELL): DeviceType.SENSOR_BUTTON,
(event.DOMAIN, EventDeviceClass.MOTION): DeviceType.SENSOR_MOTION,
}

type DeviceId = str


class Device:
"""Represent user device."""
Expand Down Expand Up @@ -236,11 +244,19 @@ def get_properties(self) -> list[Property]:
_LOGGER.error(e)
continue

if custom_property.supported and custom_property not in properties:
if custom_property and custom_property.supported and custom_property not in properties:
properties.append(custom_property)
continue

if event_platform_property_type := get_event_platform_custom_property_type(property_config):
event_platform_property = event_platform_property_type(
self._hass, self._entry_data, self.id, State(self.id, STATE_UNKNOWN)
)
if event_platform_property.supported and event_platform_property not in properties:
properties.append(event_platform_property)

for PropertyT in STATE_PROPERTIES_REGISTRY:
device_property = PropertyT(self._hass, self._entry_data, self._state)
device_property = PropertyT(self._hass, self._entry_data, self.id, self._state)
if device_property.supported and device_property not in properties:
properties.append(device_property)

Expand Down
42 changes: 37 additions & 5 deletions custom_components/yandex_smart_home/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
CONF_ENTITY_CUSTOM_RANGES,
CONF_ENTITY_CUSTOM_TOGGLES,
CONF_ENTITY_PROPERTIES,
CONF_ENTITY_PROPERTY_ENTITY,
CONF_ENTRY_ALIASES,
CONF_LINKED_PLATFORMS,
CONF_NOTIFIER,
Expand All @@ -50,10 +51,13 @@
ISSUE_ID_DEPRECATED_YAML_SEVERAL_NOTIFIERS,
ISSUE_ID_MISSING_SKILL_DATA,
ConnectionType,
EntityId,
)
from .device import DeviceId
from .helpers import APIError, CacheStore, SmartHomePlatform
from .notifier import NotifierConfig, YandexCloudNotifier, YandexDirectNotifier, YandexNotifier
from .property_custom import CustomProperty, get_custom_property
from .property import StateProperty
from .property_custom import CustomProperty, get_custom_property, get_event_platform_custom_property_type
from .schema import CapabilityType

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -286,14 +290,17 @@ async def _async_setup_notifiers(self, *_: Any) -> None:
return

track_templates = self._get_trackable_templates()
track_entity_states = self._get_trackable_entity_states()
extended_log = len(self._hass.config_entries.async_entries(DOMAIN)) > 1

match self.connection_type:
case ConnectionType.CLOUD:
config = NotifierConfig(
user_id=self.cloud_instance_id, token=self.cloud_connection_token, extended_log=extended_log
)
self._notifiers.append(YandexCloudNotifier(self._hass, self, config, track_templates))
self._notifiers.append(
YandexCloudNotifier(self._hass, self, config, track_templates, track_entity_states)
)

case ConnectionType.CLOUD_PLUS:
if self.skill:
Expand All @@ -303,7 +310,9 @@ async def _async_setup_notifiers(self, *_: Any) -> None:
skill_id=self.skill.id,
extended_log=extended_log,
)
self._notifiers.append(YandexDirectNotifier(self._hass, self, config, track_templates))
self._notifiers.append(
YandexDirectNotifier(self._hass, self, config, track_templates, track_entity_states)
)

case ConnectionType.DIRECT:
if self.skill:
Expand All @@ -313,7 +322,9 @@ async def _async_setup_notifiers(self, *_: Any) -> None:
skill_id=self.skill.id,
extended_log=extended_log,
)
self._notifiers.append(YandexDirectNotifier(self._hass, self, config, track_templates))
self._notifiers.append(
YandexDirectNotifier(self._hass, self, config, track_templates, track_entity_states)
)

await asyncio.wait([asyncio.create_task(n.async_setup()) for n in self._notifiers])

Expand Down Expand Up @@ -368,10 +379,31 @@ def _get_trackable_templates(self) -> dict[Template, list[CustomCapability | Cus

for property_config in entity_config.get(CONF_ENTITY_PROPERTIES, []):
try:
if not (custom_property := get_custom_property(self._hass, self, property_config, device_id)):
continue
template = property_custom.get_value_template(self._hass, device_id, property_config)
templates.setdefault(template, [])
templates[template].append(get_custom_property(self._hass, self, property_config, device_id))
templates[template].append(custom_property)
except APIError as e:
_LOGGER.debug(f"Failed to track custom property: {e}")

return templates

def _get_trackable_entity_states(self) -> dict[EntityId, list[tuple[DeviceId, type[StateProperty]]]]:
"""Return entity capability and property class types to track state changes."""
states: dict[EntityId, list[tuple[DeviceId, type[StateProperty]]]] = {}

def _states_append(_entity_id: str, _device_id: str, t: type[StateProperty]) -> None:
states.setdefault(_entity_id, [])
states[_entity_id].append((_device_id, t))

for device_id, entity_config in self.entity_config.items():
if not self.should_expose(device_id):
continue

for property_config in entity_config.get(CONF_ENTITY_PROPERTIES, []):
if event_platform_property := get_event_platform_custom_property_type(property_config):
entity_id: str = property_config[CONF_ENTITY_PROPERTY_ENTITY]
_states_append(entity_id, device_id, event_platform_property)

return states
40 changes: 27 additions & 13 deletions custom_components/yandex_smart_home/notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

from . import DOMAIN
from .capability import Capability
from .const import CLOUD_BASE_URL
from .device import Device
from .const import CLOUD_BASE_URL, EntityId
from .device import Device, DeviceId
from .helpers import APIError
from .property import Property
from .schema import (
Expand Down Expand Up @@ -92,6 +92,13 @@ def get_instance_state(self) -> CapabilityInstanceState | PropertyInstanceState
...


class ReportableDeviceStateFromEntityState(ReportableDeviceState, Protocol):
@abstractmethod
def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, device_id: str, state: State):
"""Initialize a capability or property for the state."""
...


class ReportableTemplateDeviceState(ReportableDeviceState, Protocol):
"""Protocol type for custom properties and capabilities."""

Expand Down Expand Up @@ -167,6 +174,7 @@ def __init__(
entry_data: ConfigEntryData,
config: NotifierConfig,
track_templates: Mapping[Template, Sequence[ReportableTemplateDeviceState]],
track_entity_states: Mapping[EntityId, Sequence[tuple[DeviceId, type[ReportableDeviceStateFromEntityState]]]],
):
"""Initialize."""
self._hass = hass
Expand All @@ -176,6 +184,7 @@ def __init__(

self._pending = PendingStates()

self._track_entity_states = track_entity_states
self._track_templates = track_templates
self._template_changes_tracker: TrackTemplateResultInfo | None = None

Expand Down Expand Up @@ -374,29 +383,34 @@ async def _async_template_result_changed(

async def _async_state_changed(self, event: Event[EventStateChangedData]) -> None:
"""Handle state changes."""
device_id = str(event.data.get(ATTR_ENTITY_ID))
entity_id = str(event.data.get(ATTR_ENTITY_ID))
old_state: State | None = event.data.get("old_state")
new_state: State | None = event.data.get("new_state")

if not new_state:
return None

new_device = Device(self._hass, self._entry_data, device_id, new_state)
old_device_states: list[ReportableDeviceState] = []
new_device_states: list[ReportableDeviceState] = []

for device_id, cls in self._track_entity_states.get(entity_id, []):
new_device_states.append(cls(self._hass, self._entry_data, device_id, new_state))
if old_state:
old_device_states.append(cls(self._hass, self._entry_data, device_id, old_state))

new_device = Device(self._hass, self._entry_data, entity_id, new_state)
if not new_device.should_expose:
return None

old_states: list[ReportableDeviceState] = []
new_states: list[ReportableDeviceState] = []

new_states.extend(new_device.get_state_capabilities())
new_states.extend(new_device.get_state_properties())
new_device_states.extend(new_device.get_state_capabilities())
new_device_states.extend(new_device.get_state_properties())

if old_state:
old_device = Device(self._hass, self._entry_data, device_id, old_state)
old_states.extend(old_device.get_state_capabilities())
old_states.extend(old_device.get_state_properties())
old_device = Device(self._hass, self._entry_data, entity_id, old_state)
old_device_states.extend(old_device.get_state_capabilities())
old_device_states.extend(old_device.get_state_properties())

for pending_state in await self._pending.async_add(new_states, old_states):
for pending_state in await self._pending.async_add(new_device_states, old_device_states):
self._debug_log(f"State report with value '{pending_state.get_value()}' scheduled for {pending_state!r}")

return self._schedule_report_states()
Expand Down
4 changes: 2 additions & 2 deletions custom_components/yandex_smart_home/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ class StateProperty(Property, Protocol):
_hass: HomeAssistant
_entry_data: ConfigEntryData

def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, state: State):
def __init__(self, hass: HomeAssistant, entry_data: ConfigEntryData, device_id: str, state: State):
"""Initialize a property for the state."""
self._hass = hass
self._entry_data = entry_data

self.state = state
self.device_id = state.entity_id
self.device_id = device_id

@property
def _state_device_class(self) -> str | None:
Expand Down
Loading

0 comments on commit 437c268

Please sign in to comment.