Home Assistant Unofficial Reference 2024.12.1
type_remotes.py
Go to the documentation of this file.
1 """Class to hold remote accessories."""
2 
3 from abc import ABC, abstractmethod
4 import logging
5 from typing import Any
6 
7 from pyhap.const import CATEGORY_TELEVISION
8 
10  ATTR_ACTIVITY,
11  ATTR_ACTIVITY_LIST,
12  ATTR_CURRENT_ACTIVITY,
13  DOMAIN as REMOTE_DOMAIN,
14  RemoteEntityFeature,
15 )
16 from homeassistant.const import (
17  ATTR_ENTITY_ID,
18  ATTR_SUPPORTED_FEATURES,
19  SERVICE_TURN_OFF,
20  SERVICE_TURN_ON,
21  STATE_ON,
22 )
23 from homeassistant.core import State, callback
24 
25 from .accessories import TYPES, HomeAccessory
26 from .const import (
27  ATTR_KEY_NAME,
28  CHAR_ACTIVE,
29  CHAR_ACTIVE_IDENTIFIER,
30  CHAR_CONFIGURED_NAME,
31  CHAR_CURRENT_VISIBILITY_STATE,
32  CHAR_IDENTIFIER,
33  CHAR_INPUT_SOURCE_TYPE,
34  CHAR_IS_CONFIGURED,
35  CHAR_NAME,
36  CHAR_REMOTE_KEY,
37  CHAR_SLEEP_DISCOVER_MODE,
38  EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
39  KEY_ARROW_DOWN,
40  KEY_ARROW_LEFT,
41  KEY_ARROW_RIGHT,
42  KEY_ARROW_UP,
43  KEY_BACK,
44  KEY_EXIT,
45  KEY_FAST_FORWARD,
46  KEY_INFORMATION,
47  KEY_NEXT_TRACK,
48  KEY_PLAY_PAUSE,
49  KEY_PREVIOUS_TRACK,
50  KEY_REWIND,
51  KEY_SELECT,
52  SERV_INPUT_SOURCE,
53  SERV_TELEVISION,
54 )
55 from .util import cleanup_name_for_homekit
56 
57 MAXIMUM_SOURCES = (
58  90 # Maximum services per accessory is 100. The base acccessory uses 9
59 )
60 
61 _LOGGER = logging.getLogger(__name__)
62 
63 REMOTE_KEYS = {
64  0: KEY_REWIND,
65  1: KEY_FAST_FORWARD,
66  2: KEY_NEXT_TRACK,
67  3: KEY_PREVIOUS_TRACK,
68  4: KEY_ARROW_UP,
69  5: KEY_ARROW_DOWN,
70  6: KEY_ARROW_LEFT,
71  7: KEY_ARROW_RIGHT,
72  8: KEY_SELECT,
73  9: KEY_BACK,
74  10: KEY_EXIT,
75  11: KEY_PLAY_PAUSE,
76  15: KEY_INFORMATION,
77 }
78 
79 
81  """Generate a InputSelect accessory."""
82 
83  def __init__(
84  self,
85  required_feature: int,
86  source_key: str,
87  source_list_key: str,
88  *args: Any,
89  category: int = CATEGORY_TELEVISION,
90  **kwargs: Any,
91  ) -> None:
92  """Initialize a InputSelect accessory object."""
93  super().__init__(*args, category=category, **kwargs)
94  state = self.hasshass.states.get(self.entity_identity_id)
95  assert state
96  features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
97  self._reload_on_change_attrs_reload_on_change_attrs.extend((source_list_key,))
98  self._mapped_sources_list: list[str] = []
99  self._mapped_sources_mapped_sources: dict[str, str] = {}
100  self.source_keysource_key = source_key
101  self.source_list_keysource_list_key = source_list_key
102  self.sourcessources = []
103  self.support_select_sourcesupport_select_source = False
104  if features & required_feature:
105  sources = self._get_ordered_source_list_from_state_get_ordered_source_list_from_state(state)
106  if len(sources) > MAXIMUM_SOURCES:
107  _LOGGER.warning(
108  "%s: Reached maximum number of sources (%s)",
109  self.entity_identity_id,
110  MAXIMUM_SOURCES,
111  )
112  self.sourcessources = sources[:MAXIMUM_SOURCES]
113  if self.sourcessources:
114  self.support_select_sourcesupport_select_source = True
115 
116  self.chars_tvchars_tv = [CHAR_REMOTE_KEY]
117  serv_tv = self.serv_tvserv_tv = self.add_preload_service(
118  SERV_TELEVISION, self.chars_tvchars_tv
119  )
120  self.char_remote_keychar_remote_key = self.serv_tvserv_tv.configure_char(
121  CHAR_REMOTE_KEY, setter_callback=self.set_remote_keyset_remote_key
122  )
123  self.set_primary_service(serv_tv)
124  serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name)
125  serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True)
126  self.char_activechar_active = serv_tv.configure_char(
127  CHAR_ACTIVE, setter_callback=self.set_on_offset_on_off
128  )
129 
130  if not self.support_select_sourcesupport_select_source:
131  return
132 
133  self.char_input_sourcechar_input_source = serv_tv.configure_char(
134  CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_sourceset_input_source
135  )
136  for index, source in enumerate(self.sourcessources):
137  serv_input = self.add_preload_service(
138  SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME], unique_id=source
139  )
140  serv_tv.add_linked_service(serv_input)
141  serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source)
142  serv_input.configure_char(CHAR_NAME, value=source)
143  serv_input.configure_char(CHAR_IDENTIFIER, value=index)
144  serv_input.configure_char(CHAR_IS_CONFIGURED, value=True)
145  input_type = 3 if "hdmi" in source.lower() else 0
146  serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type)
147  serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False)
148  _LOGGER.debug("%s: Added source %s", self.entity_identity_id, source)
149 
150  def _get_mapped_sources(self, state: State) -> dict[str, str]:
151  """Return a dict of sources mapped to their homekit safe name."""
152  source_list = state.attributes.get(self.source_list_keysource_list_key, [])
153  if self._mapped_sources_list != source_list:
154  self._mapped_sources_mapped_sources = {
155  cleanup_name_for_homekit(source): source for source in source_list
156  }
157  return self._mapped_sources_mapped_sources
158 
159  def _get_ordered_source_list_from_state(self, state: State) -> list[str]:
160  """Return ordered source list while preserving order with duplicates removed.
161 
162  Some integrations have duplicate sources in the source list
163  which will make the source list conflict as HomeKit requires
164  unique source names.
165  """
166  return list(self._get_mapped_sources_get_mapped_sources(state))
167 
168  @abstractmethod
169  def set_on_off(self, value: bool) -> None:
170  """Move switch state to value if call came from HomeKit."""
171 
172  @abstractmethod
173  def set_input_source(self, value: int) -> None:
174  """Send input set value if call came from HomeKit."""
175 
176  @abstractmethod
177  def set_remote_key(self, value: int) -> None:
178  """Send remote key value if call came from HomeKit."""
179 
180  @callback
181  def _async_update_input_state(self, hk_state: int, new_state: State) -> None:
182  """Update input state after state changed."""
183  # Set active input
184  if not self.support_select_sourcesupport_select_source or not self.sourcessources:
185  return
186  source = new_state.attributes.get(self.source_keysource_key)
187  source_name = cleanup_name_for_homekit(source)
188  _LOGGER.debug("%s: Set current input to %s", self.entity_identity_id, source_name)
189  if source_name in self.sourcessources:
190  index = self.sourcessources.index(source_name)
191  self.char_input_sourcechar_input_source.set_value(index)
192  return
193 
194  possible_sources = self._get_ordered_source_list_from_state_get_ordered_source_list_from_state(new_state)
195  if source_name in possible_sources:
196  index = possible_sources.index(source_name)
197  if index >= MAXIMUM_SOURCES:
198  _LOGGER.debug(
199  "%s: Source %s and above are not supported",
200  self.entity_identity_id,
201  MAXIMUM_SOURCES,
202  )
203  else:
204  _LOGGER.debug(
205  "%s: Sources out of sync. Rebuilding Accessory",
206  self.entity_identity_id,
207  )
208  return
209 
210  _LOGGER.debug(
211  "%s: Source %s does not exist the source list: %s",
212  self.entity_identity_id,
213  source,
214  possible_sources,
215  )
216  self.char_input_sourcechar_input_source.set_value(0)
217 
218 
219 @TYPES.register("ActivityRemote")
221  """Generate a Activity Remote accessory."""
222 
223  def __init__(self, *args: Any) -> None:
224  """Initialize a Activity Remote accessory object."""
225  super().__init__(
226  RemoteEntityFeature.ACTIVITY,
227  ATTR_CURRENT_ACTIVITY,
228  ATTR_ACTIVITY_LIST,
229  *args,
230  )
231  state = self.hasshass.states.get(self.entity_identity_id)
232  assert state
233  self.async_update_stateasync_update_stateasync_update_state(state)
234 
235  def set_on_off(self, value: bool) -> None:
236  """Move switch state to value if call came from HomeKit."""
237  _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_identity_id, value)
238  service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
239  params = {ATTR_ENTITY_ID: self.entity_identity_id}
240  self.async_call_serviceasync_call_service(REMOTE_DOMAIN, service, params)
241 
242  def set_input_source(self, value: int) -> None:
243  """Send input set value if call came from HomeKit."""
244  _LOGGER.debug("%s: Set current input to %s", self.entity_identity_id, value)
245  source = self._mapped_sources_mapped_sources[self.sourcessources[value]]
246  params = {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_ACTIVITY: source}
247  self.async_call_serviceasync_call_service(REMOTE_DOMAIN, SERVICE_TURN_ON, params)
248 
249  def set_remote_key(self, value: int) -> None:
250  """Send remote key value if call came from HomeKit."""
251  _LOGGER.debug("%s: Set remote key to %s", self.entity_identity_id, value)
252  if (key_name := REMOTE_KEYS.get(value)) is None:
253  _LOGGER.warning("%s: Unhandled key press for %s", self.entity_identity_id, value)
254  return
255  self.hasshass.bus.async_fire(
256  EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
257  {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_identity_id},
258  )
259 
260  @callback
261  def async_update_state(self, new_state: State) -> None:
262  """Update Television remote state after state changed."""
263  current_state = new_state.state
264  # Power state remote
265  hk_state = 1 if current_state == STATE_ON else 0
266  _LOGGER.debug("%s: Set current active state to %s", self.entity_identity_id, hk_state)
267  self.char_activechar_active.set_value(hk_state)
268 
269  self._async_update_input_state_async_update_input_state(hk_state, new_state)
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
None __init__(self, int required_feature, str source_key, str source_list_key, *Any args, int category=CATEGORY_TELEVISION, **Any kwargs)
Definition: type_remotes.py:91
None _async_update_input_state(self, int hk_state, State new_state)