From fd310e1f4133d567513a09fe22b144c74a46faed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Mar 2021 18:55:20 -1000 Subject: [PATCH] Update homekit to improve representation of activity based remotes (#47261) --- homeassistant/components/homekit/__init__.py | 1 + .../components/homekit/accessories.py | 6 +- .../components/homekit/config_flow.py | 6 +- .../components/homekit/type_media_players.py | 128 ++--------- .../components/homekit/type_remotes.py | 214 ++++++++++++++++++ homeassistant/components/homekit/util.py | 3 + script/hassfest/dependencies.py | 1 + tests/components/homekit/test_type_remote.py | 148 ++++++++++++ 8 files changed, 398 insertions(+), 109 deletions(-) create mode 100644 homeassistant/components/homekit/type_remotes.py create mode 100644 tests/components/homekit/test_type_remote.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4fea31e7238..0e4bcc28aab 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -50,6 +50,7 @@ from . import ( # noqa: F401 type_lights, type_locks, type_media_players, + type_remotes, type_security_systems, type_sensors, type_switches, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index b6ff11aa26d..307dbf0e806 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_WINDOW, ) from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.remote import SUPPORT_ACTIVITY from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -103,6 +104,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = None name = config.get(CONF_NAME, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if state.domain == "alarm_control_panel": a_type = "SecuritySystem" @@ -115,7 +117,6 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "cover": device_class = state.attributes.get(ATTR_DEVICE_CLASS) - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE @@ -179,6 +180,9 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "vacuum": a_type = "Vacuum" + elif state.domain == "remote" and features & SUPPORT_ACTIVITY: + a_type = "ActivityRemote" + elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): a_type = "Switch" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 758e25b97bc..1fc99bb8585 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -53,7 +54,7 @@ MODE_EXCLUDE = "exclude" INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE] -DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN] +DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN] CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." @@ -74,7 +75,7 @@ SUPPORTED_DOMAINS = [ "lock", MEDIA_PLAYER_DOMAIN, "person", - "remote", + REMOTE_DOMAIN, "scene", "script", "sensor", @@ -93,6 +94,7 @@ DEFAULT_DOMAINS = [ "light", "lock", MEDIA_PLAYER_DOMAIN, + REMOTE_DOMAIN, "switch", "vacuum", "water_heater", diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index b54b62372f9..5cd27109bd8 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -1,7 +1,7 @@ """Class to hold all media player accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION +from pyhap.const import CATEGORY_SWITCH from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, @@ -42,17 +42,9 @@ from .accessories import TYPES, HomeAccessory from .const import ( ATTR_KEY_NAME, CHAR_ACTIVE, - CHAR_ACTIVE_IDENTIFIER, - CHAR_CONFIGURED_NAME, - CHAR_CURRENT_VISIBILITY_STATE, - CHAR_IDENTIFIER, - CHAR_INPUT_SOURCE_TYPE, - CHAR_IS_CONFIGURED, CHAR_MUTE, CHAR_NAME, CHAR_ON, - CHAR_REMOTE_KEY, - CHAR_SLEEP_DISCOVER_MODE, CHAR_VOLUME, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, @@ -62,43 +54,15 @@ from .const import ( FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - KEY_ARROW_DOWN, - KEY_ARROW_LEFT, - KEY_ARROW_RIGHT, - KEY_ARROW_UP, - KEY_BACK, - KEY_EXIT, - KEY_FAST_FORWARD, - KEY_INFORMATION, - KEY_NEXT_TRACK, KEY_PLAY_PAUSE, - KEY_PREVIOUS_TRACK, - KEY_REWIND, - KEY_SELECT, - SERV_INPUT_SOURCE, SERV_SWITCH, - SERV_TELEVISION, SERV_TELEVISION_SPEAKER, ) +from .type_remotes import REMOTE_KEYS, RemoteInputSelectAccessory from .util import get_media_player_features _LOGGER = logging.getLogger(__name__) -MEDIA_PLAYER_KEYS = { - 0: KEY_REWIND, - 1: KEY_FAST_FORWARD, - 2: KEY_NEXT_TRACK, - 3: KEY_PREVIOUS_TRACK, - 4: KEY_ARROW_UP, - 5: KEY_ARROW_DOWN, - 6: KEY_ARROW_LEFT, - 7: KEY_ARROW_RIGHT, - 8: KEY_SELECT, - 9: KEY_BACK, - 10: KEY_EXIT, - 11: KEY_PLAY_PAUSE, - 15: KEY_INFORMATION, -} # Names may not contain special characters # or emjoi (/ is a special character for Apple) @@ -250,22 +214,22 @@ class MediaPlayer(HomeAccessory): @TYPES.register("TelevisionMediaPlayer") -class TelevisionMediaPlayer(HomeAccessory): +class TelevisionMediaPlayer(RemoteInputSelectAccessory): """Generate a Television Media Player accessory.""" def __init__(self, *args): - """Initialize a Switch accessory object.""" - super().__init__(*args, category=CATEGORY_TELEVISION) + """Initialize a Television Media Player accessory object.""" + super().__init__( + SUPPORT_SELECT_SOURCE, + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + *args, + ) state = self.hass.states.get(self.entity_id) - - self.support_select_source = False - - self.sources = [] - - self.chars_tv = [CHAR_REMOTE_KEY] - self.chars_speaker = [] features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self.chars_speaker = [] + self._supports_play_pause = features & (SUPPORT_PLAY | SUPPORT_PAUSE) if features & SUPPORT_VOLUME_MUTE or features & SUPPORT_VOLUME_STEP: self.chars_speaker.extend( @@ -274,27 +238,11 @@ class TelevisionMediaPlayer(HomeAccessory): if features & SUPPORT_VOLUME_SET: self.chars_speaker.append(CHAR_VOLUME) - source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, []) - if source_list and features & SUPPORT_SELECT_SOURCE: - self.support_select_source = True - - serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv) - self.set_primary_service(serv_tv) - serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name) - serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True) - self.char_active = serv_tv.configure_char( - CHAR_ACTIVE, setter_callback=self.set_on_off - ) - - self.char_remote_key = serv_tv.configure_char( - CHAR_REMOTE_KEY, setter_callback=self.set_remote_key - ) - if CHAR_VOLUME_SELECTOR in self.chars_speaker: serv_speaker = self.add_preload_service( SERV_TELEVISION_SPEAKER, self.chars_speaker ) - serv_tv.add_linked_service(serv_speaker) + self.serv_tv.add_linked_service(serv_speaker) name = f"{self.display_name} Volume" serv_speaker.configure_char(CHAR_NAME, value=name) @@ -318,25 +266,6 @@ class TelevisionMediaPlayer(HomeAccessory): CHAR_VOLUME, setter_callback=self.set_volume ) - if self.support_select_source: - self.sources = source_list - self.char_input_source = serv_tv.configure_char( - CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source - ) - for index, source in enumerate(self.sources): - serv_input = self.add_preload_service( - SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] - ) - serv_tv.add_linked_service(serv_input) - serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) - serv_input.configure_char(CHAR_NAME, value=source) - serv_input.configure_char(CHAR_IDENTIFIER, value=index) - serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) - input_type = 3 if "hdmi" in source.lower() else 0 - serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) - serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) - _LOGGER.debug("%s: Added source %s", self.entity_id, source) - self.async_update_state(state) def set_on_off(self, value): @@ -377,7 +306,7 @@ class TelevisionMediaPlayer(HomeAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = MEDIA_PLAYER_KEYS.get(value) + key_name = REMOTE_KEYS.get(value) if key_name is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return @@ -393,12 +322,13 @@ class TelevisionMediaPlayer(HomeAccessory): service = SERVICE_MEDIA_PLAY_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} self.async_call_service(DOMAIN, service, params) - else: - # Unhandled keys can be handled by listening to the event bus - self.hass.bus.fire( - EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, - {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, - ) + return + + # Unhandled keys can be handled by listening to the event bus + self.hass.bus.async_fire( + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, + ) @callback def async_update_state(self, new_state): @@ -424,18 +354,4 @@ class TelevisionMediaPlayer(HomeAccessory): if self.char_mute.value != current_mute_state: self.char_mute.set_value(current_mute_state) - # Set active input - if self.support_select_source and self.sources: - source_name = new_state.attributes.get(ATTR_INPUT_SOURCE) - _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) - if source_name in self.sources: - index = self.sources.index(source_name) - if self.char_input_source.value != index: - self.char_input_source.set_value(index) - elif hk_state: - _LOGGER.warning( - "%s: Sources out of sync. Restart Home Assistant", - self.entity_id, - ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py new file mode 100644 index 00000000000..e4f18a7c16f --- /dev/null +++ b/homeassistant/components/homekit/type_remotes.py @@ -0,0 +1,214 @@ +"""Class to hold remote accessories.""" +from abc import abstractmethod +import logging + +from pyhap.const import CATEGORY_TELEVISION + +from homeassistant.components.remote import ( + ATTR_ACTIVITY, + ATTR_ACTIVITY_LIST, + ATTR_CURRENT_ACTIVITY, + DOMAIN as REMOTE_DOMAIN, + SUPPORT_ACTIVITY, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import callback + +from .accessories import TYPES, HomeAccessory +from .const import ( + ATTR_KEY_NAME, + CHAR_ACTIVE, + CHAR_ACTIVE_IDENTIFIER, + CHAR_CONFIGURED_NAME, + CHAR_CURRENT_VISIBILITY_STATE, + CHAR_IDENTIFIER, + CHAR_INPUT_SOURCE_TYPE, + CHAR_IS_CONFIGURED, + CHAR_NAME, + CHAR_REMOTE_KEY, + CHAR_SLEEP_DISCOVER_MODE, + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + KEY_ARROW_DOWN, + KEY_ARROW_LEFT, + KEY_ARROW_RIGHT, + KEY_ARROW_UP, + KEY_BACK, + KEY_EXIT, + KEY_FAST_FORWARD, + KEY_INFORMATION, + KEY_NEXT_TRACK, + KEY_PLAY_PAUSE, + KEY_PREVIOUS_TRACK, + KEY_REWIND, + KEY_SELECT, + SERV_INPUT_SOURCE, + SERV_TELEVISION, +) + +_LOGGER = logging.getLogger(__name__) + +REMOTE_KEYS = { + 0: KEY_REWIND, + 1: KEY_FAST_FORWARD, + 2: KEY_NEXT_TRACK, + 3: KEY_PREVIOUS_TRACK, + 4: KEY_ARROW_UP, + 5: KEY_ARROW_DOWN, + 6: KEY_ARROW_LEFT, + 7: KEY_ARROW_RIGHT, + 8: KEY_SELECT, + 9: KEY_BACK, + 10: KEY_EXIT, + 11: KEY_PLAY_PAUSE, + 15: KEY_INFORMATION, +} + + +class RemoteInputSelectAccessory(HomeAccessory): + """Generate a InputSelect accessory.""" + + def __init__( + self, + required_feature, + source_key, + source_list_key, + *args, + **kwargs, + ): + """Initialize a InputSelect accessory object.""" + super().__init__(*args, category=CATEGORY_TELEVISION, **kwargs) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + self.source_key = source_key + self.sources = [] + self.support_select_source = False + if features & required_feature: + self.sources = state.attributes.get(source_list_key, []) + if self.sources: + self.support_select_source = True + + self.chars_tv = [CHAR_REMOTE_KEY] + serv_tv = self.serv_tv = self.add_preload_service( + SERV_TELEVISION, self.chars_tv + ) + self.char_remote_key = self.serv_tv.configure_char( + CHAR_REMOTE_KEY, setter_callback=self.set_remote_key + ) + self.set_primary_service(serv_tv) + serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name) + serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True) + self.char_active = serv_tv.configure_char( + CHAR_ACTIVE, setter_callback=self.set_on_off + ) + + if not self.support_select_source: + return + + self.char_input_source = serv_tv.configure_char( + CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source + ) + for index, source in enumerate(self.sources): + serv_input = self.add_preload_service( + SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] + ) + serv_tv.add_linked_service(serv_input) + serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) + serv_input.configure_char(CHAR_NAME, value=source) + serv_input.configure_char(CHAR_IDENTIFIER, value=index) + serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) + input_type = 3 if "hdmi" in source.lower() else 0 + serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) + serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) + _LOGGER.debug("%s: Added source %s", self.entity_id, source) + + @abstractmethod + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + + @abstractmethod + def set_input_source(self, value): + """Send input set value if call came from HomeKit.""" + + @abstractmethod + def set_remote_key(self, value): + """Send remote key value if call came from HomeKit.""" + + @callback + def _async_update_input_state(self, hk_state, new_state): + """Update input state after state changed.""" + # Set active input + if not self.support_select_source or not self.sources: + return + source_name = new_state.attributes.get(self.source_key) + _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) + if source_name in self.sources: + index = self.sources.index(source_name) + if self.char_input_source.value != index: + self.char_input_source.set_value(index) + elif hk_state: + _LOGGER.warning( + "%s: Sources out of sync. Restart Home Assistant", + self.entity_id, + ) + if self.char_input_source.value != 0: + self.char_input_source.set_value(0) + + +@TYPES.register("ActivityRemote") +class ActivityRemote(RemoteInputSelectAccessory): + """Generate a Activity Remote accessory.""" + + def __init__(self, *args): + """Initialize a Activity Remote accessory object.""" + super().__init__( + SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY, + ATTR_ACTIVITY_LIST, + *args, + ) + self.async_update_state(self.hass.states.get(self.entity_id)) + + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.async_call_service(REMOTE_DOMAIN, service, params) + + def set_input_source(self, value): + """Send input set value if call came from HomeKit.""" + _LOGGER.debug("%s: Set current input to %s", self.entity_id, value) + source = self.sources[value] + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_ACTIVITY: source} + self.async_call_service(REMOTE_DOMAIN, SERVICE_TURN_ON, params) + + def set_remote_key(self, value): + """Send remote key value if call came from HomeKit.""" + _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) + key_name = REMOTE_KEYS.get(value) + if key_name is None: + _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) + return + self.hass.bus.async_fire( + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, + ) + + @callback + def async_update_state(self, new_state): + """Update Television remote state after state changed.""" + current_state = new_state.state + # Power state remote + hk_state = 1 if current_state == STATE_ON else 0 + _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) + if self.char_active.value != hk_state: + self.char_active.set_value(hk_state) + + self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index cab1a28892a..d0a83b1a12a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -16,6 +16,7 @@ from homeassistant.components.media_player import ( DEVICE_CLASS_TV, DOMAIN as MEDIA_PLAYER_DOMAIN, ) +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN, SUPPORT_ACTIVITY from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -503,4 +504,6 @@ def state_needs_accessory_mode(state): return ( state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV + or state.domain == REMOTE_DOMAIN + and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY ) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index ed780ed067b..5f885c59a1d 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -107,6 +107,7 @@ ALLOWED_USED_COMPONENTS = { "onboarding", "persistent_notification", "person", + "remote", "script", "shopping_list", "sun", diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py new file mode 100644 index 00000000000..e69ebfb29fb --- /dev/null +++ b/tests/components/homekit/test_type_remote.py @@ -0,0 +1,148 @@ +"""Test different accessory types: Remotes.""" + +from homeassistant.components.homekit.const import ( + ATTR_KEY_NAME, + ATTR_VALUE, + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + KEY_ARROW_RIGHT, +) +from homeassistant.components.homekit.type_remotes import ActivityRemote +from homeassistant.components.remote import ( + ATTR_ACTIVITY, + ATTR_ACTIVITY_LIST, + ATTR_CURRENT_ACTIVITY, + DOMAIN, + SUPPORT_ACTIVITY, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_STANDBY, +) + +from tests.common import async_mock_service + + +async def test_activity_remote(hass, hk_driver, events, caplog): + """Test if remote accessory and HA are updated accordingly.""" + entity_id = "remote.harmony" + hass.states.async_set( + entity_id, + None, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "Apple TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], + }, + ) + await hass.async_block_till_done() + acc = ActivityRemote(hass, hk_driver, "ActivityRemote", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 31 # Television + + assert acc.char_active.value == 0 + assert acc.char_remote_key.value == 0 + assert acc.char_input_source.value == 1 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "Apple TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], + }, + ) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_STANDBY) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], + }, + ) + await hass.async_block_till_done() + assert acc.char_input_source.value == 0 + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY: "Apple TV", + ATTR_ACTIVITY_LIST: ["TV", "Apple TV"], + }, + ) + await hass.async_block_till_done() + assert acc.char_input_source.value == 1 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") + + acc.char_active.client_update_value(1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + acc.char_active.client_update_value(0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None + + acc.char_input_source.client_update_value(1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[1].data[ATTR_ACTIVITY] == "Apple TV" + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] is None + + acc.char_input_source.client_update_value(0) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[2].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[2].data[ATTR_ACTIVITY] == "TV" + assert len(events) == 4 + assert events[-1].data[ATTR_VALUE] is None + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) + + acc.char_remote_key.client_update_value(20) + await hass.async_block_till_done() + + acc.char_remote_key.client_update_value(7) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[ATTR_KEY_NAME] == KEY_ARROW_RIGHT