"""Support to interface with universal remote control devices.""" from collections.abc import Iterable from datetime import timedelta from enum import IntFlag import functools as ft import logging from typing import Any, final from propcache.api import cached_property import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey _LOGGER = logging.getLogger(__name__) DOMAIN = "remote" DATA_COMPONENT: HassKey[EntityComponent[RemoteEntity]] = HassKey(DOMAIN) ENTITY_ID_FORMAT = DOMAIN + ".{}" PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE SCAN_INTERVAL = timedelta(seconds=30) ATTR_ACTIVITY = "activity" ATTR_ACTIVITY_LIST = "activity_list" ATTR_CURRENT_ACTIVITY = "current_activity" ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE = "device" ATTR_NUM_REPEATS = "num_repeats" ATTR_DELAY_SECS = "delay_secs" ATTR_HOLD_SECS = "hold_secs" ATTR_ALTERNATIVE = "alternative" ATTR_TIMEOUT = "timeout" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SERVICE_SEND_COMMAND = "send_command" SERVICE_LEARN_COMMAND = "learn_command" SERVICE_DELETE_COMMAND = "delete_command" SERVICE_SYNC = "sync" DEFAULT_NUM_REPEATS = 1 DEFAULT_DELAY_SECS = 0.4 DEFAULT_HOLD_SECS = 0 class RemoteEntityFeature(IntFlag): """Supported features of the remote entity.""" LEARN_COMMAND = 1 DELETE_COMMAND = 2 ACTIVITY = 4 REMOTE_SERVICE_ACTIVITY_SCHEMA = cv.make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the remote is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" component = hass.data[DATA_COMPONENT] = EntityComponent[RemoteEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_OFF, REMOTE_SERVICE_ACTIVITY_SCHEMA, "async_turn_off" ) component.async_register_entity_service( SERVICE_TURN_ON, REMOTE_SERVICE_ACTIVITY_SCHEMA, "async_turn_on" ) component.async_register_entity_service( SERVICE_TOGGLE, REMOTE_SERVICE_ACTIVITY_SCHEMA, "async_toggle" ) component.async_register_entity_service( SERVICE_SEND_COMMAND, { vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_DEVICE): cv.string, vol.Optional( ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS ): cv.positive_int, vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), }, "async_send_command", ) component.async_register_entity_service( SERVICE_LEARN_COMMAND, { vol.Optional(ATTR_DEVICE): cv.string, vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_COMMAND_TYPE): cv.string, vol.Optional(ATTR_ALTERNATIVE): cv.boolean, vol.Optional(ATTR_TIMEOUT): cv.positive_int, }, "async_learn_command", ) component.async_register_entity_service( SERVICE_DELETE_COMMAND, { vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_DEVICE): cv.string, }, "async_delete_command", ) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await hass.data[DATA_COMPONENT].async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.data[DATA_COMPONENT].async_unload_entry(entry) class RemoteEntityDescription(ToggleEntityDescription, frozen_or_thawed=True): """A class that describes remote entities.""" CACHED_PROPERTIES_WITH_ATTR_ = { "supported_features", "current_activity", "activity_list", } class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Base class for remote entities.""" entity_description: RemoteEntityDescription _attr_activity_list: list[str] | None = None _attr_current_activity: str | None = None _attr_supported_features: RemoteEntityFeature = RemoteEntityFeature(0) @cached_property def supported_features(self) -> RemoteEntityFeature: """Flag supported features.""" return self._attr_supported_features @cached_property def current_activity(self) -> str | None: """Active activity.""" return self._attr_current_activity @cached_property def activity_list(self) -> list[str] | None: """List of available activities.""" return self._attr_activity_list @final @property def state_attributes(self) -> dict[str, Any] | None: """Return optional state attributes.""" if RemoteEntityFeature.ACTIVITY not in self.supported_features: return None return { ATTR_ACTIVITY_LIST: self.activity_list, ATTR_CURRENT_ACTIVITY: self.current_activity, } def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" raise NotImplementedError async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" await self.hass.async_add_executor_job( ft.partial(self.send_command, command, **kwargs) ) def learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" raise NotImplementedError async def async_learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs)) def delete_command(self, **kwargs: Any) -> None: """Delete commands from the database.""" raise NotImplementedError async def async_delete_command(self, **kwargs: Any) -> None: """Delete commands from the database.""" await self.hass.async_add_executor_job( ft.partial(self.delete_command, **kwargs) )