Home Assistant Unofficial Reference 2024.12.1
server.py
Go to the documentation of this file.
1 """Shared class to maintain Plex server instances."""
2 
3 from __future__ import annotations
4 
5 from copy import copy
6 import logging
7 import ssl
8 import time
9 from urllib.parse import urlparse
10 
11 from plexapi.client import PlexClient
12 from plexapi.exceptions import BadRequest, NotFound, Unauthorized
13 import plexapi.myplex
14 import plexapi.playqueue
15 import plexapi.server
16 from requests import Session
17 import requests.exceptions
18 
19 from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaType
20 from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
21 from homeassistant.core import callback
22 from homeassistant.helpers.debounce import Debouncer
23 from homeassistant.helpers.dispatcher import async_dispatcher_send
24 
25 from .const import (
26  CONF_IGNORE_NEW_SHARED_USERS,
27  CONF_IGNORE_PLEX_WEB_CLIENTS,
28  CONF_MONITORED_USERS,
29  CONF_SERVER,
30  CONF_SERVER_IDENTIFIER,
31  CONF_USE_EPISODE_ART,
32  DEBOUNCE_TIMEOUT,
33  DEFAULT_VERIFY_SSL,
34  GDM_DEBOUNCER,
35  GDM_SCANNER,
36  PLAYER_SOURCE,
37  PLEX_NEW_MP_SIGNAL,
38  PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
39  PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
40  PLEX_UPDATE_SENSOR_SIGNAL,
41  PLEXTV_THROTTLE,
42  X_PLEX_DEVICE_NAME,
43  X_PLEX_PLATFORM,
44  X_PLEX_PRODUCT,
45  X_PLEX_VERSION,
46 )
47 from .errors import (
48  MediaNotFound,
49  NoServersFound,
50  ServerNotSpecified,
51  ShouldUpdateConfigEntry,
52 )
53 from .helpers import get_plex_data
54 from .media_search import search_media
55 from .models import PlexSession
56 
57 _LOGGER = logging.getLogger(__name__)
58 
59 # Set default headers sent by plexapi
60 plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
61 plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
62 plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT
63 plexapi.X_PLEX_VERSION = X_PLEX_VERSION
64 
65 
66 class PlexServer:
67  """Manages a single Plex server connection."""
68 
69  def __init__(
70  self, hass, server_config, known_server_id=None, options=None, entry_id=None
71  ):
72  """Initialize a Plex server instance."""
73  self.hasshass = hass
74  self.entry_identry_id = entry_id
75  self.active_sessionsactive_sessions = {}
76  self._plex_account_plex_account = None
77  self._plex_server_plex_server = None
78  self._created_clients_created_clients = set()
79  self._known_clients_known_clients = set()
80  self._known_idle_known_idle = set()
81  self._url_url = server_config.get(CONF_URL)
82  self._token_token = server_config.get(CONF_TOKEN)
83  self._server_name_server_name = server_config.get(CONF_SERVER)
84  self._verify_ssl_verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
85  self._server_id_server_id = known_server_id or server_config.get(CONF_SERVER_IDENTIFIER)
86  self.optionsoptions = options
87  self.server_choiceserver_choice = None
88  self._accounts_accounts = []
89  self._owner_username_owner_username = None
90  self._plextv_clients_plextv_clients = None
91  self._plextv_client_timestamp_plextv_client_timestamp = 0
92  self._client_device_cache_client_device_cache = {}
93  self._use_plex_tv_use_plex_tv = self._token_token is not None
94  self._version_version = None
95  self.async_update_platformsasync_update_platforms = Debouncer(
96  hass,
97  _LOGGER,
98  cooldown=DEBOUNCE_TIMEOUT,
99  immediate=True,
100  function=self._async_update_platforms_async_update_platforms,
101  background=True,
102  ).async_call
103  self.thumbnail_cachethumbnail_cache = {}
104 
105  # Header conditionally added as it is not available in config entry v1
106  if CONF_CLIENT_ID in server_config:
107  plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_ID]
108  plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers()
109  plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
110 
111  @property
112  def account(self):
113  """Return a MyPlexAccount instance."""
114  if not self._plex_account_plex_account and self._use_plex_tv_use_plex_tv:
115  try:
116  self._plex_account_plex_account = plexapi.myplex.MyPlexAccount(token=self._token_token)
117  except (BadRequest, Unauthorized):
118  self._use_plex_tv_use_plex_tv = False
119  _LOGGER.error("Not authorized to access plex.tv with provided token")
120  raise
121  return self._plex_account_plex_account
122 
123  def plextv_clients(self):
124  """Return available clients linked to Plex account."""
125  if self.accountaccount is None:
126  return []
127 
128  now = time.time()
129  if now - self._plextv_client_timestamp_plextv_client_timestamp > PLEXTV_THROTTLE:
130  self._plextv_client_timestamp_plextv_client_timestamp = now
131  self._plextv_clients_plextv_clients = [
132  x
133  for x in self.accountaccount.resources()
134  if "player" in x.provides and x.presence and x.publicAddressMatches
135  ]
136  _LOGGER.debug(
137  "Current available clients from plex.tv: %s", self._plextv_clients_plextv_clients
138  )
139  return self._plextv_clients_plextv_clients
140 
141  def connect(self):
142  """Connect to a Plex server directly, obtaining direct URL if necessary."""
143  config_entry_update_needed = False
144 
145  def _connect_with_token():
146  all_servers = [
147  x for x in self.accountaccount.resources() if "server" in x.provides
148  ]
149  available_servers = [
150  (x.name, x.clientIdentifier, x.sourceTitle) for x in all_servers
151  ]
152 
153  if not all_servers:
154  raise NoServersFound
155  if not self._server_id_server_id and len(all_servers) > 1:
156  raise ServerNotSpecified(available_servers)
157 
158  self.server_choiceserver_choice = self._server_id_server_id or available_servers[0][1]
159  self._plex_server_plex_server = next(
160  x.connect(timeout=10)
161  for x in all_servers
162  if x.clientIdentifier == self.server_choiceserver_choice
163  )
164 
165  def _connect_with_url():
166  session = None
167  if self._url_url.startswith("https") and not self._verify_ssl_verify_ssl:
168  session = Session()
169  session.verify = False
170  self._plex_server_plex_server = plexapi.server.PlexServer(
171  self._url_url, self._token_token, session
172  )
173 
174  def _update_plexdirect_hostname():
175  matching_servers = [
176  x.name
177  for x in self.accountaccount.resources()
178  if x.clientIdentifier == self._server_id_server_id
179  ]
180  if matching_servers:
181  self._plex_server_plex_server = self.accountaccount.resource(matching_servers[0]).connect(
182  timeout=10
183  )
184  return True
185  _LOGGER.error("Attempt to update plex.direct hostname failed")
186  return False
187 
188  if self._url_url:
189  try:
190  _connect_with_url()
191  except requests.exceptions.SSLError as error:
192  while error and not isinstance(error, ssl.SSLCertVerificationError):
193  error = error.__context__
194  if isinstance(error, ssl.SSLCertVerificationError):
195  domain = urlparse(self._url_url).netloc.split(":")[0]
196  if domain.endswith("plex.direct") and error.args[0].startswith(
197  f"hostname '{domain}' doesn't match"
198  ):
199  _LOGGER.warning(
200  "Plex SSL certificate's hostname changed, updating"
201  )
202  if _update_plexdirect_hostname():
203  config_entry_update_needed = True
204  else:
205  # pylint: disable-next=raise-missing-from
206  raise Unauthorized( # noqa: TRY200
207  "New certificate cannot be validated"
208  " with provided token"
209  )
210  else:
211  raise
212  else:
213  raise
214  else:
215  _connect_with_token()
216 
217  try:
218  system_accounts = self._plex_server_plex_server.systemAccounts()
219  shared_users = self.accountaccount.users() if self.accountaccount else []
220  except Unauthorized:
221  _LOGGER.warning(
222  "Plex account has limited permissions,"
223  " shared account filtering will not be available"
224  )
225  else:
226  self._accounts_accounts = []
227  for user in shared_users:
228  for shared_server in user.servers:
229  if shared_server.machineIdentifier == self.machine_identifiermachine_identifier:
230  self._accounts_accounts.append(user.title)
231 
232  _LOGGER.debug("Linked accounts: %s", self.accountsaccounts)
233 
234  owner_account = next(
235  (account.name for account in system_accounts if account.accountID == 1),
236  None,
237  )
238  if owner_account:
239  self._owner_username_owner_username = owner_account
240  self._accounts_accounts.append(owner_account)
241  _LOGGER.debug("Server owner found: '%s'", self._owner_username_owner_username)
242 
243  self._version_version = self._plex_server_plex_server.version
244 
245  if config_entry_update_needed:
246  raise ShouldUpdateConfigEntry
247 
248  @callback
249  def async_refresh_entity(self, machine_identifier, device, session, source):
250  """Forward refresh dispatch to media_player."""
251  unique_id = f"{self.machine_identifier}:{machine_identifier}"
252  _LOGGER.debug("Refreshing %s", unique_id)
254  self.hasshass,
255  PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id),
256  device,
257  session,
258  source,
259  )
260 
261  async def async_update_session(self, payload):
262  """Process a session payload received from a websocket callback."""
263  session_payload = payload["PlaySessionStateNotification"][0]
264 
265  if (state := session_payload["state"]) == "buffering":
266  return
267 
268  session_key = int(session_payload["sessionKey"])
269  offset = int(session_payload["viewOffset"])
270  rating_key = int(session_payload["ratingKey"])
271 
272  unique_id, active_session = next(
273  (
274  (unique_id, session)
275  for unique_id, session in self.active_sessionsactive_sessions.items()
276  if session.session_key == session_key
277  ),
278  (None, None),
279  )
280 
281  if not active_session:
282  await self.async_update_platformsasync_update_platforms()
283  return
284 
285  if state == "stopped":
286  self.active_sessionsactive_sessions.pop(unique_id, None)
287  else:
288  active_session.state = state
289  active_session.media_position = offset
290 
291  def update_with_new_media():
292  """Update an existing session with new media details."""
293  media = self.fetch_itemfetch_item(rating_key)
294  active_session.update_media(media)
295 
296  if active_session.media_content_id != rating_key and state in (
297  "playing",
298  "paused",
299  ):
300  await self.hasshass.async_add_executor_job(update_with_new_media)
301 
303  self.hasshass,
304  PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(unique_id),
305  state,
306  )
307 
309  self.hasshass,
310  PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifiermachine_identifier),
311  )
312 
314  """Fetch all data from the Plex server in a single method."""
315  return (
316  self._plex_server_plex_server.clients(),
317  self._plex_server_plex_server.sessions(),
318  self.plextv_clientsplextv_clients(),
319  )
320 
321  async def _async_update_platforms(self): # noqa: C901
322  """Update the platform entities."""
323  _LOGGER.debug("Updating devices")
324 
325  await get_plex_data(self.hasshass)[GDM_DEBOUNCER]()
326 
327  available_clients = {}
328  ignored_clients = set()
329  new_clients = set()
330 
331  monitored_users = self.accountsaccounts
332  known_accounts = set(self.option_monitored_usersoption_monitored_users)
333  if known_accounts:
334  monitored_users = {
335  user
336  for user in self.option_monitored_usersoption_monitored_users
337  if self.option_monitored_usersoption_monitored_users[user]["enabled"]
338  }
339 
340  if not self.option_ignore_new_shared_usersoption_ignore_new_shared_users:
341  for new_user in self.accountsaccounts - known_accounts:
342  monitored_users.add(new_user)
343 
344  try:
345  devices, sessions, plextv_clients = await self.hasshass.async_add_executor_job(
346  self._fetch_platform_data_fetch_platform_data
347  )
348  except plexapi.exceptions.Unauthorized:
349  _LOGGER.debug(
350  "Token has expired for '%s', reloading integration", self.friendly_namefriendly_name
351  )
352  await self.hasshass.config_entries.async_reload(self.entry_identry_id)
353  return
354  except (
355  plexapi.exceptions.BadRequest,
356  requests.exceptions.RequestException,
357  ) as ex:
358  _LOGGER.error(
359  "Could not connect to Plex server: %s (%s)", self.friendly_namefriendly_name, ex
360  )
361  return
362 
363  def process_device(source, device):
364  self._known_idle_known_idle.discard(device.machineIdentifier)
365  available_clients.setdefault(device.machineIdentifier, {"device": device})
366  available_clients[device.machineIdentifier].setdefault(
367  PLAYER_SOURCE, source
368  )
369 
370  if (
371  device.machineIdentifier not in ignored_clients
372  and self.option_ignore_plexweb_clientsoption_ignore_plexweb_clients
373  and device.product == "Plex Web"
374  ):
375  ignored_clients.add(device.machineIdentifier)
376  if device.machineIdentifier not in self._known_clients_known_clients:
377  _LOGGER.debug(
378  "Ignoring %s %s: %s",
379  "Plex Web",
380  source,
381  device.machineIdentifier,
382  )
383  return
384 
385  if device.machineIdentifier not in (
386  self._created_clients_created_clients | ignored_clients | new_clients
387  ):
388  new_clients.add(device.machineIdentifier)
389  _LOGGER.debug(
390  "New %s from %s: %s",
391  device.product,
392  source,
393  device.machineIdentifier,
394  )
395 
396  def connect_to_client(source, baseurl, machine_identifier, name="Unknown"):
397  """Connect to a Plex client and return a PlexClient instance."""
398  try:
399  client = PlexClient(
400  server=self._plex_server_plex_server,
401  baseurl=baseurl,
402  identifier=machine_identifier,
403  token=self._plex_server_plex_server.createToken(),
404  )
405  except (NotFound, requests.exceptions.ConnectionError):
406  _LOGGER.error(
407  "Direct client connection failed, will try again: %s (%s)",
408  name,
409  baseurl,
410  )
411  except Unauthorized:
412  _LOGGER.error(
413  "Direct client connection unauthorized, ignoring: %s (%s)",
414  name,
415  baseurl,
416  )
417  self._client_device_cache_client_device_cache[machine_identifier] = None
418  else:
419  self._client_device_cache_client_device_cache[client.machineIdentifier] = client
420  process_device(source, client)
421 
422  def connect_to_resource(resource):
423  """Connect to a plex.tv resource and return a Plex client."""
424  try:
425  client = resource.connect(timeout=3)
426  _LOGGER.debug("Resource connection successful to plex.tv: %s", client)
427  except NotFound:
428  _LOGGER.error(
429  "Resource connection failed to plex.tv: %s", resource.name
430  )
431  else:
432  client.proxyThroughServer(value=False, server=self._plex_server_plex_server)
433  self._client_device_cache_client_device_cache[client.machineIdentifier] = client
434  process_device("plex.tv", client)
435 
436  def connect_new_clients():
437  """Create connections to newly discovered clients."""
438  for gdm_entry in get_plex_data(self.hasshass)[GDM_SCANNER].entries:
439  machine_identifier = gdm_entry["data"]["Resource-Identifier"]
440  if machine_identifier in self._client_device_cache_client_device_cache:
441  client = self._client_device_cache_client_device_cache[machine_identifier]
442  if client is not None:
443  process_device("GDM", client)
444  elif machine_identifier not in available_clients:
445  baseurl = (
446  f"http://{gdm_entry['from'][0]}:{gdm_entry['data']['Port']}"
447  )
448  name = gdm_entry["data"]["Name"]
449  connect_to_client("GDM", baseurl, machine_identifier, name)
450 
451  for plextv_client in plextv_clients:
452  if plextv_client.clientIdentifier in self._client_device_cache_client_device_cache:
453  client = self._client_device_cache_client_device_cache[plextv_client.clientIdentifier]
454  if client is not None:
455  process_device("plex.tv", client)
456  elif plextv_client.clientIdentifier not in available_clients:
457  connect_to_resource(plextv_client)
458 
459  def process_sessions():
460  live_session_keys = {x.sessionKey for x in sessions}
461  for unique_id, session in list(self.active_sessionsactive_sessions.items()):
462  if session.session_key not in live_session_keys:
463  _LOGGER.debug("Purging unknown session: %s", session.session_key)
464  self.active_sessionsactive_sessions.pop(unique_id)
465 
466  for session in sessions:
467  if session.TYPE == "photo":
468  _LOGGER.debug("Photo session detected, skipping: %s", session)
469  continue
470 
471  session_username = next(iter(session.usernames), None)
472  player = session.player
473  unique_id = f"{self.machine_identifier}:{player.machineIdentifier}"
474  if unique_id not in self.active_sessionsactive_sessions:
475  _LOGGER.debug("Creating new Plex session: %s", session)
476  self.active_sessionsactive_sessions[unique_id] = PlexSession(self, session)
477  if session_username and session_username not in monitored_users:
478  ignored_clients.add(player.machineIdentifier)
479  _LOGGER.debug(
480  "Ignoring %s client owned by '%s'",
481  player.product,
482  session_username,
483  )
484  continue
485 
486  process_device("session", player)
487  available_clients[player.machineIdentifier]["session"] = (
488  self.active_sessionsactive_sessions[unique_id]
489  )
490 
491  for device in devices:
492  process_device("PMS", device)
493 
494  def sync_tasks():
495  connect_new_clients()
496  process_sessions()
497 
498  await self.hasshass.async_add_executor_job(sync_tasks)
499 
500  new_entity_configs = []
501  for client_id, client_data in available_clients.items():
502  if client_id in ignored_clients:
503  continue
504  if client_id in new_clients:
505  new_entity_configs.append(client_data)
506  self._created_clients_created_clients.add(client_id)
507  else:
508  self.async_refresh_entityasync_refresh_entity(
509  client_id,
510  client_data["device"],
511  client_data.get("session"),
512  client_data.get(PLAYER_SOURCE),
513  )
514 
515  self._known_clients_known_clients.update(new_clients | ignored_clients)
516 
517  idle_clients = (
518  self._known_clients_known_clients - self._known_idle_known_idle - ignored_clients
519  ).difference(available_clients)
520  for client_id in idle_clients:
521  self.async_refresh_entityasync_refresh_entity(client_id, None, None, None)
522  self._known_idle_known_idle.add(client_id)
523  self._client_device_cache_client_device_cache.pop(client_id, None)
524 
525  if new_entity_configs:
527  self.hasshass,
528  PLEX_NEW_MP_SIGNAL.format(self.machine_identifiermachine_identifier),
529  new_entity_configs,
530  )
531 
533  self.hasshass,
534  PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifiermachine_identifier),
535  )
536 
537  @property
538  def plex_server(self):
539  """Return the plexapi PlexServer instance."""
540  return self._plex_server_plex_server
541 
542  @property
543  def has_token(self):
544  """Return if a token is used to connect to this Plex server."""
545  return self._token_token is not None
546 
547  @property
548  def accounts(self):
549  """Return accounts associated with the Plex server."""
550  return set(self._accounts_accounts)
551 
552  @property
553  def owner(self):
554  """Return the Plex server owner username."""
555  return self._owner_username_owner_username
556 
557  @property
558  def version(self):
559  """Return the version of the Plex server."""
560  return self._version_version
561 
562  @property
563  def friendly_name(self):
564  """Return name of connected Plex server."""
565  return self._plex_server_plex_server.friendlyName
566 
567  @property
569  """Return unique identifier of connected Plex server."""
570  return self._plex_server_plex_server.machineIdentifier
571 
572  @property
573  def url_in_use(self):
574  """Return URL used for connected Plex server."""
575  return self._plex_server_plex_server._baseurl # noqa: SLF001
576 
577  @property
579  """Return ignore_new_shared_users option."""
580  return self.optionsoptions[MP_DOMAIN].get(CONF_IGNORE_NEW_SHARED_USERS, False)
581 
582  @property
584  """Return use_episode_art option."""
585  return self.optionsoptions[MP_DOMAIN].get(CONF_USE_EPISODE_ART, False)
586 
587  @property
589  """Return dict of monitored users option."""
590  return self.optionsoptions[MP_DOMAIN].get(CONF_MONITORED_USERS, {})
591 
592  @property
594  """Return ignore_plex_web_clients option."""
595  return self.optionsoptions[MP_DOMAIN].get(CONF_IGNORE_PLEX_WEB_CLIENTS, False)
596 
597  @property
598  def library(self):
599  """Return library attribute from server object."""
600  return self._plex_server_plex_server.library
601 
602  def playlist(self, title):
603  """Return playlist from server object."""
604  return self._plex_server_plex_server.playlist(title)
605 
606  def playlists(self):
607  """Return available playlists from server object."""
608  return self._plex_server_plex_server.playlists()
609 
610  def create_playqueue(self, media, **kwargs):
611  """Create playqueue on Plex server."""
612  return plexapi.playqueue.PlayQueue.create(self._plex_server_plex_server, media, **kwargs)
613 
614  def create_station_playqueue(self, key):
615  """Create playqueue on Plex server using a radio station key."""
616  return plexapi.playqueue.PlayQueue.fromStationKey(self._plex_server_plex_server, key)
617 
618  def get_playqueue(self, playqueue_id):
619  """Retrieve existing playqueue from Plex server."""
620  return plexapi.playqueue.PlayQueue.get(self._plex_server_plex_server, playqueue_id)
621 
622  def fetch_item(self, item):
623  """Fetch item from Plex server."""
624  return self._plex_server_plex_server.fetchItem(item)
625 
626  def lookup_media(self, media_type, **kwargs):
627  """Lookup a piece of media."""
628  media_type = media_type.lower()
629 
630  if isinstance(kwargs.get("plex_key"), int):
631  key = kwargs["plex_key"]
632  try:
633  return self.fetch_itemfetch_item(key)
634  except NotFound as err:
635  raise MediaNotFound(f"Media for key {key} not found") from err
636 
637  if media_type == MediaType.PLAYLIST:
638  try:
639  playlist_name = kwargs["playlist_name"]
640  return self.playlistplaylist(playlist_name)
641  except KeyError as err:
642  raise MediaNotFound(
643  "Must specify 'playlist_name' for this search"
644  ) from err
645  except NotFound as err:
646  raise MediaNotFound(f"Playlist '{playlist_name}' not found") from err
647 
648  try:
649  library_name = kwargs.pop("library_name")
650  library_section = self.librarylibrary.section(library_name)
651  except KeyError as err:
652  raise MediaNotFound("Must specify 'library_name' for this search") from err
653  except NotFound as err:
654  library_sections = [section.title for section in self.librarylibrary.sections()]
655  raise MediaNotFound(
656  f"Library '{library_name}' not found in {library_sections}"
657  ) from err
658 
659  _LOGGER.debug(
660  "Searching for %s in %s using: %s", media_type, library_section, kwargs
661  )
662  return search_media(media_type, library_section, **kwargs)
663 
664  @property
665  def sensor_attributes(self):
666  """Return active session information for use in activity sensor."""
667  return {x.sensor_user: x.sensor_title for x in self.active_sessionsactive_sessions.values()}
668 
669  def set_plex_server(self, plex_server: PlexServer) -> None:
670  """Set the PlexServer instance."""
671  self._plex_server_plex_server = plex_server
672 
673  def switch_user(self, username: str) -> PlexServer:
674  """Return a shallow copy of a PlexServer as the provided user."""
675  new_server = copy(self)
676  new_server.set_plex_server(self.plex_serverplex_server.switchUser(username))
677 
678  return new_server
None set_plex_server(self, PlexServer plex_server)
Definition: server.py:669
def create_playqueue(self, media, **kwargs)
Definition: server.py:610
PlexServer switch_user(self, str username)
Definition: server.py:673
def async_refresh_entity(self, machine_identifier, device, session, source)
Definition: server.py:249
def __init__(self, hass, server_config, known_server_id=None, options=None, entry_id=None)
Definition: server.py:71
def lookup_media(self, media_type, **kwargs)
Definition: server.py:626
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
PlexData get_plex_data(HomeAssistant hass)
Definition: helpers.py:29
PlexObject|list[PlexObject] search_media(str media_type, LibrarySection library_section, bool allow_multiple=False, **kwargs)
Definition: media_search.py:43
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193