1
mirror of https://github.com/home-assistant/core synced 2024-09-25 00:41:32 +02:00

Update homekit to improve representation of activity based remotes (#47261)

This commit is contained in:
J. Nick Koston 2021-03-21 18:55:20 -10:00 committed by GitHub
parent 3f2ca16ad7
commit fd310e1f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 398 additions and 109 deletions

View File

@ -50,6 +50,7 @@ from . import ( # noqa: F401
type_lights,
type_locks,
type_media_players,
type_remotes,
type_security_systems,
type_sensors,
type_switches,

View File

@ -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"

View File

@ -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",

View File

@ -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)

View File

@ -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)

View File

@ -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
)

View File

@ -107,6 +107,7 @@ ALLOWED_USED_COMPONENTS = {
"onboarding",
"persistent_notification",
"person",
"remote",
"script",
"shopping_list",
"sun",

View File

@ -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