Home Assistant Unofficial Reference 2024.12.1
type_media_players.py
Go to the documentation of this file.
1 """Class to hold all media player accessories."""
2 
3 import logging
4 from typing import Any
5 
6 from pyhap.characteristic import Characteristic
7 from pyhap.const import CATEGORY_SWITCH
8 
10  ATTR_INPUT_SOURCE,
11  ATTR_INPUT_SOURCE_LIST,
12  ATTR_MEDIA_VOLUME_LEVEL,
13  ATTR_MEDIA_VOLUME_MUTED,
14  DOMAIN as MEDIA_PLAYER_DOMAIN,
15  SERVICE_SELECT_SOURCE,
16  MediaPlayerEntityFeature,
17 )
18 from homeassistant.const import (
19  ATTR_ENTITY_ID,
20  ATTR_SUPPORTED_FEATURES,
21  SERVICE_MEDIA_PAUSE,
22  SERVICE_MEDIA_PLAY,
23  SERVICE_MEDIA_PLAY_PAUSE,
24  SERVICE_MEDIA_STOP,
25  SERVICE_TURN_OFF,
26  SERVICE_TURN_ON,
27  SERVICE_VOLUME_DOWN,
28  SERVICE_VOLUME_MUTE,
29  SERVICE_VOLUME_SET,
30  SERVICE_VOLUME_UP,
31  STATE_OFF,
32  STATE_PAUSED,
33  STATE_PLAYING,
34  STATE_STANDBY,
35  STATE_UNKNOWN,
36 )
37 from homeassistant.core import State, callback
38 
39 from .accessories import TYPES, HomeAccessory
40 from .const import (
41  ATTR_KEY_NAME,
42  CATEGORY_RECEIVER,
43  CHAR_ACTIVE,
44  CHAR_MUTE,
45  CHAR_NAME,
46  CHAR_ON,
47  CHAR_VOLUME,
48  CHAR_VOLUME_CONTROL_TYPE,
49  CHAR_VOLUME_SELECTOR,
50  CONF_FEATURE_LIST,
51  EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
52  FEATURE_ON_OFF,
53  FEATURE_PLAY_PAUSE,
54  FEATURE_PLAY_STOP,
55  FEATURE_TOGGLE_MUTE,
56  KEY_PLAY_PAUSE,
57  SERV_SWITCH,
58  SERV_TELEVISION_SPEAKER,
59 )
60 from .type_remotes import REMOTE_KEYS, RemoteInputSelectAccessory
61 from .util import cleanup_name_for_homekit, get_media_player_features
62 
63 _LOGGER = logging.getLogger(__name__)
64 
65 
66 # Names may not contain special characters
67 # or emjoi (/ is a special character for Apple)
68 MODE_FRIENDLY_NAME = {
69  FEATURE_ON_OFF: "Power",
70  FEATURE_PLAY_PAUSE: "Play-Pause",
71  FEATURE_PLAY_STOP: "Play-Stop",
72  FEATURE_TOGGLE_MUTE: "Mute",
73 }
74 
75 MEDIA_PLAYER_OFF_STATES = (
76  STATE_OFF,
77  STATE_UNKNOWN,
78  STATE_STANDBY,
79  "None",
80 )
81 
82 
83 @TYPES.register("MediaPlayer")
85  """Generate a Media Player accessory."""
86 
87  def __init__(self, *args: Any) -> None:
88  """Initialize a Switch accessory object."""
89  super().__init__(*args, category=CATEGORY_SWITCH)
90  state = self.hasshass.states.get(self.entity_identity_id)
91  assert state
92  self.chars: dict[str, Characteristic | None] = {
93  FEATURE_ON_OFF: None,
94  FEATURE_PLAY_PAUSE: None,
95  FEATURE_PLAY_STOP: None,
96  FEATURE_TOGGLE_MUTE: None,
97  }
98  feature_list = self.configconfig.get(
99  CONF_FEATURE_LIST, get_media_player_features(state)
100  )
101 
102  if FEATURE_ON_OFF in feature_list:
103  name = self.generate_service_namegenerate_service_name(FEATURE_ON_OFF)
104  serv_on_off = self.add_preload_service(
105  SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_ON_OFF
106  )
107  serv_on_off.configure_char(CHAR_NAME, value=name)
108  self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char(
109  CHAR_ON, value=False, setter_callback=self.set_on_offset_on_off
110  )
111 
112  if FEATURE_PLAY_PAUSE in feature_list:
113  name = self.generate_service_namegenerate_service_name(FEATURE_PLAY_PAUSE)
114  serv_play_pause = self.add_preload_service(
115  SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_PAUSE
116  )
117  serv_play_pause.configure_char(CHAR_NAME, value=name)
118  self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char(
119  CHAR_ON, value=False, setter_callback=self.set_play_pauseset_play_pause
120  )
121 
122  if FEATURE_PLAY_STOP in feature_list:
123  name = self.generate_service_namegenerate_service_name(FEATURE_PLAY_STOP)
124  serv_play_stop = self.add_preload_service(
125  SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_STOP
126  )
127  serv_play_stop.configure_char(CHAR_NAME, value=name)
128  self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char(
129  CHAR_ON, value=False, setter_callback=self.set_play_stopset_play_stop
130  )
131 
132  if FEATURE_TOGGLE_MUTE in feature_list:
133  name = self.generate_service_namegenerate_service_name(FEATURE_TOGGLE_MUTE)
134  serv_toggle_mute = self.add_preload_service(
135  SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_TOGGLE_MUTE
136  )
137  serv_toggle_mute.configure_char(CHAR_NAME, value=name)
138  self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char(
139  CHAR_ON, value=False, setter_callback=self.set_toggle_muteset_toggle_mute
140  )
141  self.async_update_stateasync_update_stateasync_update_state(state)
142 
143  def generate_service_name(self, mode: str) -> str:
144  """Generate name for individual service."""
145  return cleanup_name_for_homekit(
146  f"{self.display_name} {MODE_FRIENDLY_NAME[mode]}"
147  )
148 
149  def set_on_off(self, value: bool) -> None:
150  """Move switch state to value if call came from HomeKit."""
151  _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_identity_id, value)
152  service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
153  params = {ATTR_ENTITY_ID: self.entity_identity_id}
154  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
155 
156  def set_play_pause(self, value: bool) -> None:
157  """Move switch state to value if call came from HomeKit."""
158  _LOGGER.debug(
159  '%s: Set switch state for "play_pause" to %s', self.entity_identity_id, value
160  )
161  service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
162  params = {ATTR_ENTITY_ID: self.entity_identity_id}
163  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
164 
165  def set_play_stop(self, value: bool) -> None:
166  """Move switch state to value if call came from HomeKit."""
167  _LOGGER.debug(
168  '%s: Set switch state for "play_stop" to %s', self.entity_identity_id, value
169  )
170  service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
171  params = {ATTR_ENTITY_ID: self.entity_identity_id}
172  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
173 
174  def set_toggle_mute(self, value: bool) -> None:
175  """Move switch state to value if call came from HomeKit."""
176  _LOGGER.debug(
177  '%s: Set switch state for "toggle_mute" to %s', self.entity_identity_id, value
178  )
179  params = {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_MEDIA_VOLUME_MUTED: value}
180  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, params)
181 
182  @callback
183  def async_update_state(self, new_state: State) -> None:
184  """Update switch state after state changed."""
185  current_state = new_state.state
186 
187  if on_off_char := self.chars[FEATURE_ON_OFF]:
188  hk_state = current_state not in MEDIA_PLAYER_OFF_STATES
189  _LOGGER.debug(
190  '%s: Set current state for "on_off" to %s', self.entity_identity_id, hk_state
191  )
192  on_off_char.set_value(hk_state)
193 
194  if play_pause_char := self.chars[FEATURE_PLAY_PAUSE]:
195  hk_state = current_state == STATE_PLAYING
196  _LOGGER.debug(
197  '%s: Set current state for "play_pause" to %s',
198  self.entity_identity_id,
199  hk_state,
200  )
201  play_pause_char.set_value(hk_state)
202 
203  if play_stop_char := self.chars[FEATURE_PLAY_STOP]:
204  hk_state = current_state == STATE_PLAYING
205  _LOGGER.debug(
206  '%s: Set current state for "play_stop" to %s',
207  self.entity_identity_id,
208  hk_state,
209  )
210  play_stop_char.set_value(hk_state)
211 
212  if toggle_mute_char := self.chars[FEATURE_TOGGLE_MUTE]:
213  mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
214  _LOGGER.debug(
215  '%s: Set current state for "toggle_mute" to %s',
216  self.entity_identity_id,
217  mute_state,
218  )
219  toggle_mute_char.set_value(mute_state)
220 
221 
222 @TYPES.register("TelevisionMediaPlayer")
224  """Generate a Television Media Player accessory."""
225 
226  def __init__(self, *args: Any, **kwargs: Any) -> None:
227  """Initialize a Television Media Player accessory object."""
228  super().__init__(
229  MediaPlayerEntityFeature.SELECT_SOURCE,
230  ATTR_INPUT_SOURCE,
231  ATTR_INPUT_SOURCE_LIST,
232  *args,
233  **kwargs,
234  )
235  state = self.hasshass.states.get(self.entity_identity_id)
236  assert state
237  features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
238 
239  self.chars_speaker: list[str] = []
240 
241  self._supports_play_pause_supports_play_pause = features & (
242  MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
243  )
244  if (
245  features & MediaPlayerEntityFeature.VOLUME_MUTE
246  or features & MediaPlayerEntityFeature.VOLUME_STEP
247  ):
248  self.chars_speaker.extend(
249  (CHAR_NAME, CHAR_ACTIVE, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR)
250  )
251  if features & MediaPlayerEntityFeature.VOLUME_SET:
252  self.chars_speaker.append(CHAR_VOLUME)
253 
254  if CHAR_VOLUME_SELECTOR in self.chars_speaker:
255  serv_speaker = self.add_preload_service(
256  SERV_TELEVISION_SPEAKER, self.chars_speaker
257  )
258  self.serv_tvserv_tv.add_linked_service(serv_speaker)
259 
260  name = f"{self.display_name} Volume"
261  serv_speaker.configure_char(CHAR_NAME, value=name)
262  serv_speaker.configure_char(CHAR_ACTIVE, value=1)
263 
264  self.char_mutechar_mute = serv_speaker.configure_char(
265  CHAR_MUTE, value=False, setter_callback=self.set_muteset_mute
266  )
267 
268  volume_control_type = 1 if CHAR_VOLUME in self.chars_speaker else 2
269  serv_speaker.configure_char(
270  CHAR_VOLUME_CONTROL_TYPE, value=volume_control_type
271  )
272 
273  self.char_volume_selectorchar_volume_selector = serv_speaker.configure_char(
274  CHAR_VOLUME_SELECTOR, setter_callback=self.set_volume_stepset_volume_step
275  )
276 
277  if CHAR_VOLUME in self.chars_speaker:
278  self.char_volumechar_volume = serv_speaker.configure_char(
279  CHAR_VOLUME, setter_callback=self.set_volumeset_volume
280  )
281 
282  self.async_update_stateasync_update_stateasync_update_state(state)
283 
284  def set_on_off(self, value: bool) -> None:
285  """Move switch state to value if call came from HomeKit."""
286  _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_identity_id, value)
287  service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
288  params = {ATTR_ENTITY_ID: self.entity_identity_id}
289  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
290 
291  def set_mute(self, value: bool) -> None:
292  """Move switch state to value if call came from HomeKit."""
293  _LOGGER.debug(
294  '%s: Set switch state for "toggle_mute" to %s', self.entity_identity_id, value
295  )
296  params = {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_MEDIA_VOLUME_MUTED: value}
297  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, params)
298 
299  def set_volume(self, value: bool) -> None:
300  """Send volume step value if call came from HomeKit."""
301  _LOGGER.debug("%s: Set volume to %s", self.entity_identity_id, value)
302  params = {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_MEDIA_VOLUME_LEVEL: value}
303  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, params)
304 
305  def set_volume_step(self, value: bool) -> None:
306  """Send volume step value if call came from HomeKit."""
307  _LOGGER.debug("%s: Step volume by %s", self.entity_identity_id, value)
308  service = SERVICE_VOLUME_DOWN if value else SERVICE_VOLUME_UP
309  params = {ATTR_ENTITY_ID: self.entity_identity_id}
310  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
311 
312  def set_input_source(self, value: int) -> None:
313  """Send input set value if call came from HomeKit."""
314  _LOGGER.debug("%s: Set current input to %s", self.entity_identity_id, value)
315  source_name = self._mapped_sources_mapped_sources[self.sourcessources[value]]
316  params = {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_INPUT_SOURCE: source_name}
317  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, params)
318 
319  def set_remote_key(self, value: int) -> None:
320  """Send remote key value if call came from HomeKit."""
321  _LOGGER.debug("%s: Set remote key to %s", self.entity_identity_id, value)
322  if (key_name := REMOTE_KEYS.get(value)) is None:
323  _LOGGER.warning("%s: Unhandled key press for %s", self.entity_identity_id, value)
324  return
325 
326  if key_name == KEY_PLAY_PAUSE and self._supports_play_pause_supports_play_pause:
327  # Handle Play Pause by directly updating the media player entity.
328  state_obj = self.hasshass.states.get(self.entity_identity_id)
329  assert state_obj
330  state = state_obj.state
331  if state in (STATE_PLAYING, STATE_PAUSED):
332  service = (
333  SERVICE_MEDIA_PLAY if state == STATE_PAUSED else SERVICE_MEDIA_PAUSE
334  )
335  else:
336  service = SERVICE_MEDIA_PLAY_PAUSE
337  params = {ATTR_ENTITY_ID: self.entity_identity_id}
338  self.async_call_serviceasync_call_service(MEDIA_PLAYER_DOMAIN, service, params)
339  return
340 
341  # Unhandled keys can be handled by listening to the event bus
342  self.hasshass.bus.async_fire(
343  EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED,
344  {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_identity_id},
345  )
346 
347  @callback
348  def async_update_state(self, new_state: State) -> None:
349  """Update Television state after state changed."""
350  current_state = new_state.state
351 
352  # Power state television
353  hk_state = 0
354  if current_state not in MEDIA_PLAYER_OFF_STATES:
355  hk_state = 1
356  _LOGGER.debug("%s: Set current active state to %s", self.entity_identity_id, hk_state)
357  self.char_activechar_active.set_value(hk_state)
358 
359  # Set mute state
360  if CHAR_VOLUME_SELECTOR in self.chars_speaker:
361  current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
362  _LOGGER.debug(
363  "%s: Set current mute state to %s",
364  self.entity_identity_id,
365  current_mute_state,
366  )
367  self.char_mutechar_mute.set_value(current_mute_state)
368 
369  self._async_update_input_state_async_update_input_state(hk_state, new_state)
370 
371 
372 @TYPES.register("ReceiverMediaPlayer")
374  """Generate a Receiver Media Player accessory.
375 
376  For HomeKit, a Receiver Media Player is exactly the same as a
377  Television Media Player except it has a different category
378  which will tell HomeKit how to render the device.
379  """
380 
381  def __init__(self, *args: Any) -> None:
382  """Initialize a Receiver Media Player accessory object."""
383  super().__init__(*args, category=CATEGORY_RECEIVER)
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
None _async_update_input_state(self, int hk_state, State new_state)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88