Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for functionality to interact with Android / Fire TV devices."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 import hashlib
7 import logging
8 
9 from androidtv.constants import APPS, KEYS
10 from androidtv.setup_async import AndroidTVAsync, FireTVAsync
11 import voluptuous as vol
12 
13 from homeassistant.components import persistent_notification
15  MediaPlayerDeviceClass,
16  MediaPlayerEntity,
17  MediaPlayerEntityFeature,
18  MediaPlayerState,
19 )
20 from homeassistant.const import ATTR_COMMAND
21 from homeassistant.core import HomeAssistant
22 from homeassistant.helpers import config_validation as cv, entity_platform
23 from homeassistant.helpers.dispatcher import async_dispatcher_connect
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.util.dt import utcnow
26 
27 from . import AndroidTVConfigEntry
28 from .const import (
29  CONF_APPS,
30  CONF_EXCLUDE_UNNAMED_APPS,
31  CONF_GET_SOURCES,
32  CONF_SCREENCAP_INTERVAL,
33  CONF_TURN_OFF_COMMAND,
34  CONF_TURN_ON_COMMAND,
35  DEFAULT_EXCLUDE_UNNAMED_APPS,
36  DEFAULT_GET_SOURCES,
37  DEFAULT_SCREENCAP_INTERVAL,
38  DEVICE_ANDROIDTV,
39  SIGNAL_CONFIG_ENTITY,
40 )
41 from .entity import AndroidTVEntity, adb_decorator
42 
43 _LOGGER = logging.getLogger(__name__)
44 
45 ATTR_ADB_RESPONSE = "adb_response"
46 ATTR_DEVICE_PATH = "device_path"
47 ATTR_HDMI_INPUT = "hdmi_input"
48 ATTR_LOCAL_PATH = "local_path"
49 
50 SERVICE_ADB_COMMAND = "adb_command"
51 SERVICE_DOWNLOAD = "download"
52 SERVICE_LEARN_SENDEVENT = "learn_sendevent"
53 SERVICE_UPLOAD = "upload"
54 
55 # Translate from `AndroidTV` / `FireTV` reported state to HA state.
56 ANDROIDTV_STATES = {
57  "off": MediaPlayerState.OFF,
58  "idle": MediaPlayerState.IDLE,
59  "standby": MediaPlayerState.STANDBY,
60  "playing": MediaPlayerState.PLAYING,
61  "paused": MediaPlayerState.PAUSED,
62 }
63 
64 
66  hass: HomeAssistant,
67  entry: AndroidTVConfigEntry,
68  async_add_entities: AddEntitiesCallback,
69 ) -> None:
70  """Set up the Android Debug Bridge entity."""
71  device_class = entry.runtime_data.aftv.DEVICE_CLASS
73  [
74  AndroidTVDevice(entry)
75  if device_class == DEVICE_ANDROIDTV
76  else FireTVDevice(entry)
77  ]
78  )
79 
80  platform = entity_platform.async_get_current_platform()
81  platform.async_register_entity_service(
82  SERVICE_ADB_COMMAND,
83  {vol.Required(ATTR_COMMAND): cv.string},
84  "adb_command",
85  )
86  platform.async_register_entity_service(
87  SERVICE_LEARN_SENDEVENT, None, "learn_sendevent"
88  )
89  platform.async_register_entity_service(
90  SERVICE_DOWNLOAD,
91  {
92  vol.Required(ATTR_DEVICE_PATH): cv.string,
93  vol.Required(ATTR_LOCAL_PATH): cv.string,
94  },
95  "service_download",
96  )
97  platform.async_register_entity_service(
98  SERVICE_UPLOAD,
99  {
100  vol.Required(ATTR_DEVICE_PATH): cv.string,
101  vol.Required(ATTR_LOCAL_PATH): cv.string,
102  },
103  "service_upload",
104  )
105 
106 
108  """Representation of an Android or Fire TV device."""
109 
110  _attr_device_class = MediaPlayerDeviceClass.TV
111  _attr_name = None
112 
113  def __init__(self, entry: AndroidTVConfigEntry) -> None:
114  """Initialize the Android / Fire TV device."""
115  super().__init__(entry)
116  self._entry_id_entry_id = entry.entry_id
117 
118  self._media_image_media_image: tuple[bytes | None, str | None] = None, None
119  self._attr_media_image_hash_attr_media_image_hash = None
120 
121  self._app_id_to_name_app_id_to_name: dict[str, str] = {}
122  self._app_name_to_id_app_name_to_id: dict[str, str] = {}
123  self._get_sources_get_sources = DEFAULT_GET_SOURCES
124  self._exclude_unnamed_apps_exclude_unnamed_apps = DEFAULT_EXCLUDE_UNNAMED_APPS
125  self._screencap_delta_screencap_delta: timedelta | None = None
126  self._last_screencap_last_screencap: datetime | None = None
127  self.turn_on_commandturn_on_command: str | None = None
128  self.turn_off_commandturn_off_command: str | None = None
129 
130  # Property attributes
131  self._attr_extra_state_attributes_attr_extra_state_attributes = {
132  ATTR_ADB_RESPONSE: None,
133  ATTR_HDMI_INPUT: None,
134  }
135 
136  # The number of consecutive failed connect attempts
137  self._failed_connect_count_failed_connect_count = 0
138 
139  def _process_config(self) -> None:
140  """Load the config options."""
141  _LOGGER.debug("Loading configuration options")
142  options = self._entry_runtime_data_entry_runtime_data.dev_opt
143 
144  apps = options.get(CONF_APPS, {})
145  self._app_id_to_name_app_id_to_name = APPS.copy()
146  self._app_id_to_name_app_id_to_name.update(apps)
147  self._app_name_to_id_app_name_to_id = {
148  value: key for key, value in self._app_id_to_name_app_id_to_name.items() if value
149  }
150 
151  # Make sure that apps overridden via the `apps` parameter are reflected
152  # in `self._app_name_to_id`
153  for key, value in apps.items():
154  self._app_name_to_id_app_name_to_id[value] = key
155 
156  self._get_sources_get_sources = options.get(CONF_GET_SOURCES, DEFAULT_GET_SOURCES)
157  self._exclude_unnamed_apps_exclude_unnamed_apps = options.get(
158  CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
159  )
160  screencap_interval: int = options.get(
161  CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
162  )
163  if screencap_interval > 0:
164  self._screencap_delta_screencap_delta = timedelta(minutes=screencap_interval)
165  else:
166  self._screencap_delta_screencap_delta = None
167  self.turn_off_commandturn_off_command = options.get(CONF_TURN_OFF_COMMAND)
168  self.turn_on_commandturn_on_command = options.get(CONF_TURN_ON_COMMAND)
169 
170  async def async_added_to_hass(self) -> None:
171  """Set config parameter when add to hass."""
172  await super().async_added_to_hass()
173  self._process_config_process_config()
174  self.async_on_removeasync_on_remove(
176  self.hasshass,
177  f"{SIGNAL_CONFIG_ENTITY}_{self._entry_id}",
178  self._process_config_process_config,
179  )
180  )
181 
182  @adb_decorator()
183  async def _adb_screencap(self) -> bytes | None:
184  """Take a screen capture from the device."""
185  return await self.aftvaftv.adb_screencap() # type: ignore[no-any-return]
186 
187  async def _async_get_screencap(self, prev_app_id: str | None = None) -> None:
188  """Take a screen capture from the device when enabled."""
189  if (
190  not self._screencap_delta_screencap_delta
191  or self.statestatestatestate in {MediaPlayerState.OFF, None}
192  or not self.availableavailable
193  ):
194  self._media_image_media_image = None, None
195  self._attr_media_image_hash_attr_media_image_hash = None
196  else:
197  force: bool = prev_app_id is not None
198  if force:
199  force = prev_app_id != self._attr_app_id
200  await self._adb_get_screencap_adb_get_screencap(force)
201 
202  async def _adb_get_screencap(self, force: bool = False) -> None:
203  """Take a screen capture from the device every configured minutes."""
204  time_elapsed = self._screencap_delta_screencap_delta is not None and (
205  self._last_screencap_last_screencap is None
206  or (utcnow() - self._last_screencap_last_screencap) >= self._screencap_delta_screencap_delta
207  )
208  if not (force or time_elapsed):
209  return
210 
211  self._last_screencap_last_screencap = utcnow()
212  if media_data := await self._adb_screencap_adb_screencap():
213  self._media_image_media_image = media_data, "image/png"
214  self._attr_media_image_hash_attr_media_image_hash = hashlib.sha256(media_data).hexdigest()[:16]
215  else:
216  self._media_image_media_image = None, None
217  self._attr_media_image_hash_attr_media_image_hash = None
218 
219  async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
220  """Fetch current playing image."""
221  return self._media_image_media_image
222 
223  @adb_decorator()
224  async def async_media_play(self) -> None:
225  """Send play command."""
226  await self.aftvaftv.media_play()
227 
228  @adb_decorator()
229  async def async_media_pause(self) -> None:
230  """Send pause command."""
231  await self.aftvaftv.media_pause()
232 
233  @adb_decorator()
234  async def async_media_play_pause(self) -> None:
235  """Send play/pause command."""
236  await self.aftvaftv.media_play_pause()
237 
238  @adb_decorator()
239  async def async_turn_on(self) -> None:
240  """Turn on the device."""
241  if self.turn_on_commandturn_on_command:
242  await self.aftvaftv.adb_shell(self.turn_on_commandturn_on_command)
243  else:
244  await self.aftvaftv.turn_on()
245 
246  @adb_decorator()
247  async def async_turn_off(self) -> None:
248  """Turn off the device."""
249  if self.turn_off_commandturn_off_command:
250  await self.aftvaftv.adb_shell(self.turn_off_commandturn_off_command)
251  else:
252  await self.aftvaftv.turn_off()
253 
254  @adb_decorator()
255  async def async_media_previous_track(self) -> None:
256  """Send previous track command (results in rewind)."""
257  await self.aftvaftv.media_previous_track()
258 
259  @adb_decorator()
260  async def async_media_next_track(self) -> None:
261  """Send next track command (results in fast-forward)."""
262  await self.aftvaftv.media_next_track()
263 
264  @adb_decorator()
265  async def async_select_source(self, source: str) -> None:
266  """Select input source.
267 
268  If the source starts with a '!', then it will close the app instead of
269  opening it.
270  """
271  if isinstance(source, str):
272  if not source.startswith("!"):
273  await self.aftvaftv.launch_app(self._app_name_to_id_app_name_to_id.get(source, source))
274  else:
275  source_ = source[1:].lstrip()
276  await self.aftvaftv.stop_app(self._app_name_to_id_app_name_to_id.get(source_, source_))
277 
278  @adb_decorator()
279  async def adb_command(self, command: str) -> None:
280  """Send an ADB command to an Android / Fire TV device."""
281  if key := KEYS.get(command):
282  await self.aftvaftv.adb_shell(f"input keyevent {key}")
283  return
284 
285  if command == "GET_PROPERTIES":
286  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_ADB_RESPONSE] = str(
287  await self.aftvaftv.get_properties_dict()
288  )
289  self.async_write_ha_stateasync_write_ha_state()
290  return
291 
292  try:
293  response = await self.aftvaftv.adb_shell(command)
294  except UnicodeDecodeError:
295  return
296 
297  if isinstance(response, str) and response.strip():
298  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_ADB_RESPONSE] = response.strip()
299  self.async_write_ha_stateasync_write_ha_state()
300 
301  return
302 
303  @adb_decorator()
304  async def learn_sendevent(self) -> None:
305  """Translate a key press on a remote to ADB 'sendevent' commands."""
306  output = await self.aftvaftv.learn_sendevent()
307  if output:
308  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_ADB_RESPONSE] = output
309  self.async_write_ha_stateasync_write_ha_state()
310 
311  msg = (
312  f"Output from service '{SERVICE_LEARN_SENDEVENT}' from"
313  f" {self.entity_id}: '{output}'"
314  )
315  persistent_notification.async_create(
316  self.hasshass,
317  msg,
318  title="Android Debug Bridge",
319  )
320  _LOGGER.debug("%s", msg)
321 
322  @adb_decorator()
323  async def service_download(self, device_path: str, local_path: str) -> None:
324  """Download a file from your Android / Fire TV device to your Home Assistant instance."""
325  if not self.hasshass.config.is_allowed_path(local_path):
326  _LOGGER.warning("'%s' is not secure to load data from!", local_path)
327  return
328 
329  await self.aftvaftv.adb_pull(local_path, device_path)
330 
331  @adb_decorator()
332  async def service_upload(self, device_path: str, local_path: str) -> None:
333  """Upload a file from your Home Assistant instance to an Android / Fire TV device."""
334  if not self.hasshass.config.is_allowed_path(local_path):
335  _LOGGER.warning("'%s' is not secure to load data from!", local_path)
336  return
337 
338  await self.aftvaftv.adb_push(local_path, device_path)
339 
340 
342  """Representation of an Android device."""
343 
344  _attr_supported_features = (
345  MediaPlayerEntityFeature.PAUSE
346  | MediaPlayerEntityFeature.PLAY
347  | MediaPlayerEntityFeature.TURN_ON
348  | MediaPlayerEntityFeature.TURN_OFF
349  | MediaPlayerEntityFeature.PREVIOUS_TRACK
350  | MediaPlayerEntityFeature.NEXT_TRACK
351  | MediaPlayerEntityFeature.SELECT_SOURCE
352  | MediaPlayerEntityFeature.STOP
353  | MediaPlayerEntityFeature.VOLUME_MUTE
354  | MediaPlayerEntityFeature.VOLUME_SET
355  | MediaPlayerEntityFeature.VOLUME_STEP
356  )
357  aftv: AndroidTVAsync
358 
359  @adb_decorator(override_available=True)
360  async def async_update(self) -> None:
361  """Update the device state and, if necessary, re-connect."""
362  # Check if device is disconnected.
363  if not self._attr_available_attr_available:
364  # Try to connect
365  if await self.aftvaftv.adb_connect(log_errors=self._failed_connect_count_failed_connect_count_failed_connect_count == 0):
366  self._failed_connect_count_failed_connect_count_failed_connect_count = 0
367  self._attr_available_attr_available = True
368  else:
369  self._failed_connect_count_failed_connect_count_failed_connect_count += 1
370 
371  # If the ADB connection is not intact, don't update.
372  if not self.availableavailable:
373  return
374 
375  prev_app_id = self._attr_app_id
376  # Get the updated state and attributes.
377  (
378  state,
379  self._attr_app_id,
380  running_apps,
381  _,
382  self._attr_is_volume_muted,
383  self._attr_volume_level_attr_volume_level,
384  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_HDMI_INPUT],
385  ) = await self.aftvaftv.update(self._get_sources_get_sources)
386 
387  self._attr_state_attr_state = ANDROIDTV_STATES.get(state)
388  if self._attr_state_attr_state is None:
389  self._attr_available_attr_available = False
390 
391  if running_apps and self._attr_app_id:
392  self._attr_source_attr_source = self._attr_app_name_attr_app_name = self._app_id_to_name_app_id_to_name.get(
393  self._attr_app_id, self._attr_app_id
394  )
395  sources = [
396  self._app_id_to_name_app_id_to_name.get(
397  app_id, app_id if not self._exclude_unnamed_apps_exclude_unnamed_apps else None
398  )
399  for app_id in running_apps
400  ]
401  self._attr_source_list_attr_source_list = [source for source in sources if source]
402  else:
403  self._attr_source_list_attr_source_list = None
404 
405  await self._async_get_screencap_async_get_screencap(prev_app_id)
406 
407  @adb_decorator()
408  async def async_media_stop(self) -> None:
409  """Send stop command."""
410  await self.aftvaftv.media_stop()
411 
412  @adb_decorator()
413  async def async_mute_volume(self, mute: bool) -> None:
414  """Mute the volume."""
415  is_muted = await self.aftvaftv.is_volume_muted()
416 
417  # `None` indicates that the muted status could not be determined
418  if is_muted is not None and is_muted != mute:
419  await self.aftvaftv.mute_volume()
420 
421  @adb_decorator()
422  async def async_set_volume_level(self, volume: float) -> None:
423  """Set the volume level."""
424  await self.aftvaftv.set_volume_level(volume)
425 
426  @adb_decorator()
427  async def async_volume_down(self) -> None:
428  """Send volume down command."""
429  self._attr_volume_level_attr_volume_level = await self.aftvaftv.volume_down(self._attr_volume_level_attr_volume_level)
430 
431  @adb_decorator()
432  async def async_volume_up(self) -> None:
433  """Send volume up command."""
434  self._attr_volume_level_attr_volume_level = await self.aftvaftv.volume_up(self._attr_volume_level_attr_volume_level)
435 
436 
438  """Representation of a Fire TV device."""
439 
440  _attr_supported_features = (
441  MediaPlayerEntityFeature.PAUSE
442  | MediaPlayerEntityFeature.PLAY
443  | MediaPlayerEntityFeature.TURN_ON
444  | MediaPlayerEntityFeature.TURN_OFF
445  | MediaPlayerEntityFeature.PREVIOUS_TRACK
446  | MediaPlayerEntityFeature.NEXT_TRACK
447  | MediaPlayerEntityFeature.SELECT_SOURCE
448  | MediaPlayerEntityFeature.STOP
449  )
450  aftv: FireTVAsync
451 
452  @adb_decorator(override_available=True)
453  async def async_update(self) -> None:
454  """Update the device state and, if necessary, re-connect."""
455  # Check if device is disconnected.
456  if not self._attr_available_attr_available:
457  # Try to connect
458  if await self.aftvaftv.adb_connect(log_errors=self._failed_connect_count_failed_connect_count_failed_connect_count == 0):
459  self._failed_connect_count_failed_connect_count_failed_connect_count = 0
460  self._attr_available_attr_available = True
461  else:
462  self._failed_connect_count_failed_connect_count_failed_connect_count += 1
463 
464  # If the ADB connection is not intact, don't update.
465  if not self.availableavailable:
466  return
467 
468  prev_app_id = self._attr_app_id
469  # Get the `state`, `current_app`, `running_apps` and `hdmi_input`.
470  (
471  state,
472  self._attr_app_id,
473  running_apps,
474  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_HDMI_INPUT],
475  ) = await self.aftvaftv.update(self._get_sources_get_sources)
476 
477  self._attr_state_attr_state = ANDROIDTV_STATES.get(state)
478  if self._attr_state_attr_state is None:
479  self._attr_available_attr_available = False
480 
481  if running_apps and self._attr_app_id:
482  self._attr_source_attr_source = self._app_id_to_name_app_id_to_name.get(
483  self._attr_app_id, self._attr_app_id
484  )
485  sources = [
486  self._app_id_to_name_app_id_to_name.get(
487  app_id, app_id if not self._exclude_unnamed_apps_exclude_unnamed_apps else None
488  )
489  for app_id in running_apps
490  ]
491  self._attr_source_list_attr_source_list = [source for source in sources if source]
492  else:
493  self._attr_source_list_attr_source_list = None
494 
495  await self._async_get_screencap_async_get_screencap(prev_app_id)
496 
497  @adb_decorator()
498  async def async_media_stop(self) -> None:
499  """Send stop (back) command."""
500  await self.aftvaftv.back()
tuple[bytes|None, str|None] async_get_media_image(self)
None service_download(self, str device_path, str local_path)
None service_upload(self, str device_path, str local_path)
None __init__(self, AndroidTVConfigEntry entry)
None _async_get_screencap(self, str|None prev_app_id=None)
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_setup_entry(HomeAssistant hass, AndroidTVConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:69
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103