diff --git a/.coveragerc b/.coveragerc index 960850fa99e..e5ee7b3dc92 100644 --- a/.coveragerc +++ b/.coveragerc @@ -690,6 +690,7 @@ omit = homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* homeassistant/components/plex/media_player.py + homeassistant/components/plex/models.py homeassistant/components/plex/sensor.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index e4f4f80dcfa..de8b278f3cf 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,6 +1,5 @@ """Support to embed Plex.""" import asyncio -import functools from functools import partial import logging @@ -35,10 +34,7 @@ from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CONF_SERVER, @@ -176,6 +172,7 @@ async def async_setup_entry(hass, entry): if data == STATE_CONNECTED: _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER]) + hass.async_create_task(async_update_plex()) elif data == STATE_DISCONNECTED: _LOGGER.debug( "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER] @@ -190,7 +187,7 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) elif signal == SIGNAL_DATA: - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + hass.async_create_task(plex_server.async_update_session(data)) session = async_get_clientsession(hass) verify_ssl = server_config.get(CONF_VERIFY_SSL) @@ -219,7 +216,7 @@ async def async_setup_entry(hass, entry): task = hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) - task.add_done_callback(functools.partial(start_websocket_session, platform)) + task.add_done_callback(partial(start_websocket_session, platform)) async def async_play_on_sonos_service(service_call): await hass.async_add_executor_job(play_on_sonos, hass, service_call) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index f54c376c667..c13be439be7 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -24,6 +24,7 @@ WEBSOCKETS = "websockets" PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" +PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL = "plex_update_session_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 295254aa612..4d765cc0508 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,4 +1,5 @@ """Support to interface with the Plex API.""" +from functools import wraps import json import logging @@ -7,10 +8,7 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -22,12 +20,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.network import is_internal_request -from homeassistant.util import dt as dt_util from .const import ( COMMON_PLAYERS, @@ -36,23 +36,28 @@ from .const import ( DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, + PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) from .media_browser import browse_media -LIVE_TV_SECTION = "-4" -PLAYLISTS_BROWSE_PAYLOAD = { - "title": "Playlists", - "media_content_id": "all", - "media_content_type": "playlists", - "can_play": False, - "can_expand": True, -} - _LOGGER = logging.getLogger(__name__) +def needs_session(func): + """Ensure session is available for certain attributes.""" + + @wraps(func) + def get_session_attribute(self, *args): + if self.session is None: + return None + return func(self, *args) + + return get_session_attribute + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] @@ -60,9 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_new_media_players(new_entities): - _async_add_entities( - hass, registry, config_entry, async_add_entities, server_id, new_entities - ) + _async_add_entities(hass, registry, async_add_entities, server_id, new_entities) unsub = async_dispatcher_connect( hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players @@ -72,9 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def _async_add_entities( - hass, registry, config_entry, async_add_entities, server_id, new_entities -): +def _async_add_entities(hass, registry, async_add_entities, server_id, new_entities): """Set up Plex media_player entities.""" _LOGGER.debug("New entities: %s", new_entities) entities = [] @@ -106,258 +107,113 @@ class PlexMediaPlayer(MediaPlayerEntity): """Initialize the Plex device.""" self.plex_server = plex_server self.device = device - self.session = session self.player_source = player_source - self._app_name = "" + + self.device_make = None + self.device_platform = None + self.device_product = None + self.device_title = None + self.device_version = None + self.machine_identifier = device.machineIdentifier + self.session_device = None + self._available = False self._device_protocol_capabilities = None - self._is_player_active = False - self._machine_identifier = device.machineIdentifier - self._make = "" - self._device_platform = None - self._device_product = None - self._device_title = None - self._device_version = None self._name = None - self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting - self._session_type = None - self._session_username = None self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - # General - self._media_content_id = None - self._media_content_rating = None - self._media_content_type = None - self._media_duration = None - self._media_image_url = None - self._media_summary = None - self._media_title = None - self._media_position = None - self._media_position_updated_at = None - # Music - self._media_album_artist = None - self._media_album_name = None - self._media_artist = None - self._media_track = None - # TV Show - self._media_episode = None - self._media_season = None - self._media_series_title = None + + # Initializes other attributes + self.session = session async def async_added_to_hass(self): """Run when about to be added to hass.""" - server_id = self.plex_server.machine_identifier - _LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id) - unsub = async_dispatcher_connect( - self.hass, - PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), - self.async_refresh_media_player, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), + self.async_refresh_media_player, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(self.unique_id), + self.async_update_from_websocket, + ) ) - self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) @callback - def async_refresh_media_player(self, device, session): + def async_refresh_media_player(self, device, session, source): """Set instance objects and trigger an entity state update.""" _LOGGER.debug("Refreshing %s [%s / %s]", self.entity_id, device, session) self.device = device self.session = session + if source: + self.player_source = source self.async_schedule_update_ha_state(True) - def _clear_media_details(self): - """Set all Media Items to None.""" - # General - self._media_content_id = None - self._media_content_rating = None - self._media_content_type = None - self._media_duration = None - self._media_image_url = None - self._media_summary = None - self._media_title = None - # Music - self._media_album_artist = None - self._media_album_name = None - self._media_artist = None - self._media_track = None - # TV Show - self._media_episode = None - self._media_season = None - self._media_series_title = None + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier), + ) - # Clear library Name - self._app_name = "" + @callback + def async_update_from_websocket(self, state): + """Update the entity based on new websocket data.""" + self.update_state(state) + self.async_write_ha_state() + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier), + ) def update(self): """Refresh key device data.""" - self._clear_media_details() - - self._available = self.device or self.session - - if self.device: - try: - device_url = self.device.url("/") - except plexapi.exceptions.BadRequest: - device_url = "127.0.0.1" - if "127.0.0.1" in device_url: - self.device.proxyThroughServer() - self._device_platform = self.device.platform - self._device_product = self.device.product - self._device_title = self.device.title - self._device_version = self.device.version - self._device_protocol_capabilities = self.device.protocolCapabilities - self._player_state = self.device.state - if not self.session: self.force_idle() - else: - session_device = next( - ( - p - for p in self.session.players - if p.machineIdentifier == self.device.machineIdentifier - ), - None, - ) - if session_device: - self._make = session_device.device or "" - self._player_state = session_device.state - self._device_platform = self._device_platform or session_device.platform - self._device_product = self._device_product or session_device.product - self._device_title = self._device_title or session_device.title - self._device_version = self._device_version or session_device.version - else: - _LOGGER.warning("No player associated with active session") + if not self.device: + self._available = False + return - if self.session.usernames: - self._session_username = self.session.usernames[0] + self._available = True - # Calculate throttled position for proper progress display. - position = int(self.session.viewOffset / 1000) - now = dt_util.utcnow() - if self._media_position is not None: - pos_diff = position - self._media_position - time_diff = now - self._media_position_updated_at - if pos_diff != 0 and abs(time_diff.total_seconds() - pos_diff) > 5: - self._media_position_updated_at = now - self._media_position = position - else: - self._media_position_updated_at = now - self._media_position = position + try: + device_url = self.device.url("/") + except plexapi.exceptions.BadRequest: + device_url = "127.0.0.1" + if "127.0.0.1" in device_url: + self.device.proxyThroughServer() + self._device_protocol_capabilities = self.device.protocolCapabilities - self._media_content_id = self.session.ratingKey - self._media_content_rating = getattr(self.session, "contentRating", None) + for device in filter(None, [self.device, self.session_device]): + self.device_make = self.device_make or device.device + self.device_platform = self.device_platform or device.platform + self.device_product = self.device_product or device.product + self.device_title = self.device_title or device.title + self.device_version = self.device_version or device.version - name_parts = [self._device_product, self._device_title or self._device_platform] - if (self._device_product in COMMON_PLAYERS) and self.make: + name_parts = [self.device_product, self.device_title or self.device_platform] + if (self.device_product in COMMON_PLAYERS) and self.device_make: # Add more context in name for likely duplicates - name_parts.append(self.make) + name_parts.append(self.device_make) if self.username and self.username != self.plex_server.owner: # Prepend username for shared/managed clients name_parts.insert(0, self.username) self._name = NAME_FORMAT.format(" - ".join(name_parts)) - self._set_player_state() - - if self._is_player_active and self.session is not None: - self._session_type = self.session.type - if self.session.duration: - self._media_duration = int(self.session.duration / 1000) - else: - self._media_duration = None - # title (movie name, tv episode name, music song name) - self._media_summary = self.session.summary - self._media_title = self.session.title - # media type - self._set_media_type() - if self.session.librarySectionID == LIVE_TV_SECTION: - self._app_name = "Live TV" - else: - self._app_name = ( - self.session.section().title - if self.session.section() is not None - else "" - ) - self._set_media_image() - else: - self._session_type = None - - def _set_media_image(self): - thumb_url = self.session.thumbUrl - if ( - self.media_content_type is MEDIA_TYPE_TVSHOW - and not self.plex_server.option_use_episode_art - ): - if self.session.librarySectionID == LIVE_TV_SECTION: - thumb_url = self.session.grandparentThumb - else: - thumb_url = self.session.url(self.session.grandparentThumb) - - if thumb_url is None: - _LOGGER.debug( - "Using media art because media thumb was not found: %s", self.name - ) - thumb_url = self.session.url(self.session.art) - - self._media_image_url = thumb_url - - def _set_player_state(self): - if self._player_state == "playing": - self._is_player_active = True - self._state = STATE_PLAYING - elif self._player_state == "paused": - self._is_player_active = True - self._state = STATE_PAUSED - elif self.device: - self._is_player_active = False - self._state = STATE_IDLE - else: - self._is_player_active = False - self._state = STATE_OFF - - def _set_media_type(self): - if self._session_type == "episode": - self._media_content_type = MEDIA_TYPE_TVSHOW - - # season number (00) - self._media_season = self.session.seasonNumber - # show name - self._media_series_title = self.session.grandparentTitle - # episode number (00) - if self.session.index is not None: - self._media_episode = self.session.index - - elif self._session_type == "movie": - self._media_content_type = MEDIA_TYPE_MOVIE - if self.session.year is not None and self._media_title is not None: - self._media_title += f" ({self.session.year!s})" - - elif self._session_type == "track": - self._media_content_type = MEDIA_TYPE_MUSIC - self._media_album_name = self.session.parentTitle - self._media_album_artist = self.session.grandparentTitle - self._media_track = self.session.index - self._media_artist = self.session.originalTitle - # use album artist if track artist is missing - if self._media_artist is None: - _LOGGER.debug( - "Using album artist because track artist was not found: %s", - self.name, - ) - self._media_artist = self._media_album_artist - - elif self._session_type == "clip": - _LOGGER.debug( - "Clip content type detected, compatibility may vary: %s", self.name - ) - self._media_content_type = MEDIA_TYPE_VIDEO def force_idle(self): """Force client to idle.""" - self._player_state = STATE_IDLE self._state = STATE_IDLE - self.session = None - self._clear_media_details() + if self.player_source == "session": + self.device = None + self.session_device = None + self._available = False @property def should_poll(self): @@ -367,12 +223,21 @@ class PlexMediaPlayer(MediaPlayerEntity): @property def unique_id(self): """Return the id of this plex client.""" - return f"{self.plex_server.machine_identifier}:{self._machine_identifier}" + return f"{self.plex_server.machine_identifier}:{self.machine_identifier}" @property - def machine_identifier(self): - """Return the Plex-provided identifier of this plex client.""" - return self._machine_identifier + def session(self): + """Return the active session for this player.""" + return self._session + + @session.setter + def session(self, session): + self._session = session + if session: + self.session_device = self.session.player + self.update_state(self.session.state) + else: + self._state = STATE_IDLE @property def available(self): @@ -385,20 +250,33 @@ class PlexMediaPlayer(MediaPlayerEntity): return self._name @property + @needs_session def username(self): """Return the username of the client owner.""" - return self._session_username - - @property - def app_name(self): - """Return the library name of playing media.""" - return self._app_name + return self.session.username @property def state(self): """Return the state of the device.""" return self._state + def update_state(self, state): + """Set the state of the device, handle session termination.""" + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + elif state == "stopped": + self.session = None + self.force_idle() + else: + self._state = STATE_IDLE + + @property + def _is_player_active(self): + """Report if the client is playing media.""" + return self.state in [STATE_PLAYING, STATE_PAUSED] + @property def _active_media_plexapi_type(self): """Get the active media type required by PlexAPI commands.""" @@ -408,84 +286,112 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" @property + @needs_session + def session_key(self): + """Return current session key.""" + return self.session.sessionKey + + @property + @needs_session + def media_library_title(self): + """Return the library name of playing media.""" + return self.session.media_library_title + + @property + @needs_session def media_content_id(self): """Return the content ID of current playing media.""" - return self._media_content_id + return self.session.media_content_id @property + @needs_session def media_content_type(self): """Return the content type of current playing media.""" - return self._media_content_type + return self.session.media_content_type @property + @needs_session + def media_content_rating(self): + """Return the content rating of current playing media.""" + return self.session.media_content_rating + + @property + @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" - return self._media_artist + return self.session.media_artist @property + @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" - return self._media_album_name + return self.session.media_album_name @property + @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" - return self._media_album_artist + return self.session.media_album_artist @property + @needs_session def media_track(self): """Return the track number of current playing media, music only.""" - return self._media_track + return self.session.media_track @property + @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" - return self._media_duration + return self.session.media_duration @property + @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" - return self._media_position + return self.session.media_position @property + @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" - return self._media_position_updated_at + return self.session.media_position_updated_at @property + @needs_session def media_image_url(self): """Return the image URL of current playing media.""" - return self._media_image_url + return self.session.media_image_url @property + @needs_session def media_summary(self): """Return the summary of current playing media.""" - return self._media_summary + return self.session.media_summary @property + @needs_session def media_title(self): """Return the title of current playing media.""" - return self._media_title + return self.session.media_title @property + @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" - return self._media_season + return self.session.media_season @property + @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" - return self._media_series_title + return self.session.media_series_title @property + @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" - return self._media_episode - - @property - def make(self): - """Return the make of the device (ex. SHIELD Android TV).""" - return self._make + return self.session.media_episode @property def supported_features(self): @@ -521,12 +427,14 @@ class PlexMediaPlayer(MediaPlayerEntity): and "playback" in self._device_protocol_capabilities ): return self._volume_level + return None @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" if self._is_player_active and self.device: return self._volume_muted + return None def mute_volume(self, mute): """Mute the volume. @@ -605,13 +513,19 @@ class PlexMediaPlayer(MediaPlayerEntity): @property def device_state_attributes(self): """Return the scene state attributes.""" - return { - "media_content_rating": self._media_content_rating, - "session_username": self.username, - "media_library_name": self._app_name, - "summary": self.media_summary, - "player_source": self.player_source, - } + attributes = {} + for attr in [ + "media_content_rating", + "media_library_title", + "player_source", + "summary", + "username", + ]: + value = getattr(self, attr, None) + if value: + attributes[attr] = value + + return attributes @property def device_info(self): @@ -621,10 +535,10 @@ class PlexMediaPlayer(MediaPlayerEntity): return { "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, - "manufacturer": self._device_platform or "Plex", - "model": self._device_product or self.make, + "manufacturer": self.device_platform or "Plex", + "model": self.device_product or self.device_make, "name": self.name, - "sw_version": self._device_version, + "sw_version": self.device_version, "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), } diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py new file mode 100644 index 00000000000..7633c5deaa8 --- /dev/null +++ b/homeassistant/components/plex/models.py @@ -0,0 +1,126 @@ +"""Models to represent various Plex objects used in the integration.""" +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, +) +from homeassistant.util import dt as dt_util + +LIVE_TV_SECTION = "-4" + + +class PlexSession: + """Represents a Plex playback session.""" + + def __init__(self, plex_server, session): + """Initialize the object.""" + self.plex_server = plex_server + + # Available on both media and session objects + self.media_content_id = None + self.media_content_type = None + self.media_content_rating = None + self.media_duration = None + self.media_image_url = None + self.media_library_title = None + self.media_summary = None + self.media_title = None + # TV Shows + self.media_episode = None + self.media_season = None + self.media_series_title = None + # Music + self.media_album_name = None + self.media_album_artist = None + self.media_artist = None + self.media_track = None + + # Only available on sessions + self.player = next(iter(session.players), None) + self.device_product = self.player.product + self.media_position = session.viewOffset + self.session_key = session.sessionKey + self.state = self.player.state + self.username = next(iter(session.usernames), None) + + # Used by sensor entity + sensor_user_list = [self.username, self.device_product] + self.sensor_title = None + self.sensor_user = " - ".join(filter(None, sensor_user_list)) + + self.update_media(session) + + def __repr__(self): + """Return representation of the session.""" + return f"<{self.session_key}:{self.sensor_title}>" + + def update_media(self, media): + """Update attributes from a media object.""" + self.media_content_id = media.ratingKey + self.media_content_rating = getattr(media, "contentRating", None) + self.media_image_url = self.get_media_image_url(media) + self.media_summary = media.summary + self.media_title = media.title + + if media.duration: + self.media_duration = int(media.duration / 1000) + + if media.librarySectionID == LIVE_TV_SECTION: + self.media_library_title = "Live TV" + else: + self.media_library_title = ( + media.section().title if media.section() is not None else "" + ) + + if media.type == "episode": + self.media_content_type = MEDIA_TYPE_TVSHOW + self.media_season = media.seasonNumber + self.media_series_title = media.grandparentTitle + if media.index is not None: + self.media_episode = media.index + self.sensor_title = f"{self.media_series_title} - {media.seasonEpisode} - {self.media_title}" + elif media.type == "movie": + self.media_content_type = MEDIA_TYPE_MOVIE + if media.year is not None and media.title is not None: + self.media_title += f" ({media.year!s})" + self.sensor_title = self.media_title + elif media.type == "track": + self.media_content_type = MEDIA_TYPE_MUSIC + self.media_album_name = media.parentTitle + self.media_album_artist = media.grandparentTitle + self.media_track = media.index + self.media_artist = media.originalTitle or self.media_album_artist + self.sensor_title = ( + f"{self.media_artist} - {self.media_album_name} - {self.media_title}" + ) + elif media.type == "clip": + self.media_content_type = MEDIA_TYPE_VIDEO + self.sensor_title = media.title + else: + self.sensor_title = "Unknown" + + @property + def media_position(self): + """Return the current playback position.""" + return self._media_position + + @media_position.setter + def media_position(self, offset): + """Set the current playback position.""" + self._media_position = int(offset / 1000) + self.media_position_updated_at = dt_util.utcnow() + + def get_media_image_url(self, media): + """Get the image URL from a media object.""" + thumb_url = media.thumbUrl + if media.type == "episode" and not self.plex_server.option_use_episode_art: + if media.librarySectionID == LIVE_TV_SECTION: + thumb_url = media.grandparentThumb + else: + thumb_url = media.url(media.grandparentThumb) + + if thumb_url is None: + thumb_url = media.url(media.art) + + return thumb_url diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index a3b465dfdb0..8c3733a7450 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,20 +1,15 @@ """Support for Plex media server monitoring.""" import logging -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_call_later from .const import ( CONF_SERVER_IDENTIFIER, DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_UPDATE_PLATFORMS_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) @@ -26,21 +21,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - sensor = PlexSensor(plexserver) + sensor = PlexSensor(hass, plexserver) async_add_entities([sensor]) class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__(self, plex_server): + def __init__(self, hass, plex_server): """Initialize the sensor.""" - self.sessions = [] self._state = None - self._now_playing = [] self._server = plex_server self._name = NAME_FORMAT.format(plex_server.friendly_name) self._unique_id = f"sensor-{plex_server.machine_identifier}" + self.async_refresh_sensor = Debouncer( + hass, + _LOGGER, + cooldown=3, + immediate=False, + function=self._async_refresh_sensor, + ).async_call async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -52,85 +52,12 @@ class PlexSensor(Entity): ) self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - async def async_refresh_sensor(self, sessions): + async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - - self.sessions = sessions - update_failed = False - - @callback - def update_plex(_): - async_dispatcher_send( - self.hass, - PLEX_UPDATE_PLATFORMS_SIGNAL.format(self._server.machine_identifier), - ) - - now_playing = [] - for sess in self.sessions: - if sess.TYPE == "photo": - _LOGGER.debug("Photo session detected, skipping: %s", sess) - continue - if not sess.usernames: - _LOGGER.debug( - "Session temporarily incomplete, will try again: %s", sess - ) - update_failed = True - continue - user = sess.usernames[0] - device = sess.players[0].title - now_playing_user = f"{user} - {device}" - now_playing_title = "" - - if sess.TYPE == "episode": - # example: - # "Supernatural (2005) - s01e13 - Route 666" - - def sync_io_attributes(session): - year = None - try: - year = session.show().year - except TypeError: - pass - return (year, session.seasonEpisode) - - year, season_episode = await self.hass.async_add_executor_job( - sync_io_attributes, sess - ) - season_title = sess.grandparentTitle - if year is not None: - season_title += f" ({year!s})" - episode_title = sess.title - now_playing_title = ( - f"{season_title} - {season_episode} - {episode_title}" - ) - elif sess.TYPE == "track": - # example: - # "Billy Talent - Afraid of Heights - Afraid of Heights" - track_artist = sess.grandparentTitle - track_album = sess.parentTitle - track_title = sess.title - now_playing_title = f"{track_artist} - {track_album} - {track_title}" - elif sess.TYPE == "movie": - # example: - # "picture_of_last_summer_camp (2015)" - # "The Incredible Hulk (2008)" - now_playing_title = sess.title - year = await self.hass.async_add_executor_job(getattr, sess, "year") - if year is not None: - now_playing_title += f" ({year})" - else: - now_playing_title = sess.title - - now_playing.append((now_playing_user, now_playing_title)) - self._state = len(self.sessions) - self._now_playing = now_playing - + self._state = len(self._server.sensor_attributes) self.async_write_ha_state() - if update_failed: - async_call_later(self.hass, 5, update_plex) - @property def name(self): """Return the name of the sensor.""" @@ -164,7 +91,7 @@ class PlexSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return {content[0]: content[1] for content in self._now_playing} + return self._server.sensor_attributes @property def device_info(self): diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index ed84b0c5d8f..3834833b740 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -37,6 +37,7 @@ from .const import ( GDM_SCANNER, PLAYER_SOURCE, PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, PLEXTV_THROTTLE, @@ -52,6 +53,7 @@ from .errors import ( ShouldUpdateConfigEntry, ) from .media_search import lookup_movie, lookup_music, lookup_tv +from .models import PlexSession _LOGGER = logging.getLogger(__name__) @@ -71,6 +73,7 @@ class PlexServer: """Initialize a Plex server instance.""" self.hass = hass self.entry_id = entry_id + self.active_sessions = {} self._plex_account = None self._plex_server = None self._created_clients = set() @@ -233,7 +236,7 @@ class PlexServer: raise ShouldUpdateConfigEntry @callback - def async_refresh_entity(self, machine_identifier, device, session): + def async_refresh_entity(self, machine_identifier, device, session, source): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" _LOGGER.debug("Refreshing %s", unique_id) @@ -242,6 +245,64 @@ class PlexServer: PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, + source, + ) + + async def async_update_session(self, payload): + """Process a session payload received from a websocket callback.""" + try: + session_payload = payload["PlaySessionStateNotification"][0] + except KeyError: + await self.async_update_platforms() + return + + state = session_payload["state"] + if state == "buffering": + return + + session_key = int(session_payload["sessionKey"]) + offset = int(session_payload["viewOffset"]) + rating_key = int(session_payload["ratingKey"]) + + unique_id, active_session = next( + ( + (unique_id, session) + for unique_id, session in self.active_sessions.items() + if session.session_key == session_key + ), + (None, None), + ) + + if not active_session: + await self.async_update_platforms() + return + + if state == "stopped": + self.active_sessions.pop(unique_id, None) + else: + active_session.state = state + active_session.media_position = offset + + def update_with_new_media(): + """Update an existing session with new media details.""" + media = self.fetch_item(rating_key) + active_session.update_media(media) + + if active_session.media_content_id != rating_key and state in [ + "playing", + "paused", + ]: + await self.hass.async_add_executor_job(update_with_new_media) + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(unique_id), + state, + ) + + async_dispatcher_send( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), ) def _fetch_platform_data(self): @@ -322,9 +383,6 @@ class PlexServer: device.machineIdentifier, ) - for device in devices: - process_device("PMS", device) - def connect_to_client(source, baseurl, machine_identifier, name="Unknown"): """Connect to a Plex client and return a PlexClient instance.""" try: @@ -385,25 +443,46 @@ class PlexServer: elif plextv_client.clientIdentifier not in available_clients: connect_to_resource(plextv_client) - await self.hass.async_add_executor_job(connect_new_clients) + def process_sessions(): + live_session_keys = {x.sessionKey for x in sessions} + for unique_id, session in list(self.active_sessions.items()): + if session.session_key not in live_session_keys: + _LOGGER.debug("Purging unknown session: %s", session.session_key) + self.active_sessions.pop(unique_id) - for session in sessions: - if session.TYPE == "photo": - _LOGGER.debug("Photo session detected, skipping: %s", session) - continue - - session_username = session.usernames[0] - for player in session.players: - if session_username and session_username not in monitored_users: - ignored_clients.add(player.machineIdentifier) - _LOGGER.debug( - "Ignoring %s client owned by '%s'", - player.product, - session_username, - ) + for session in sessions: + if session.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", session) continue - process_device("session", player) - available_clients[player.machineIdentifier]["session"] = session + + session_username = session.usernames[0] + for player in session.players: + unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" + if unique_id not in self.active_sessions: + _LOGGER.debug("Creating new Plex session: %s", session) + self.active_sessions[unique_id] = PlexSession(self, session) + if session_username and session_username not in monitored_users: + ignored_clients.add(player.machineIdentifier) + _LOGGER.debug( + "Ignoring %s client owned by '%s'", + player.product, + session_username, + ) + continue + + process_device("session", player) + available_clients[player.machineIdentifier][ + "session" + ] = self.active_sessions[unique_id] + + for device in devices: + process_device("PMS", device) + + def sync_tasks(): + connect_new_clients() + process_sessions() + + await self.hass.async_add_executor_job(sync_tasks) new_entity_configs = [] for client_id, client_data in available_clients.items(): @@ -414,7 +493,10 @@ class PlexServer: self._created_clients.add(client_id) else: self.async_refresh_entity( - client_id, client_data["device"], client_data.get("session") + client_id, + client_data["device"], + client_data.get("session"), + client_data.get(PLAYER_SOURCE), ) self._known_clients.update(new_clients | ignored_clients) @@ -423,7 +505,7 @@ class PlexServer: self._known_clients - self._known_idle - ignored_clients ).difference(available_clients) for client_id in idle_clients: - self.async_refresh_entity(client_id, None, None) + self.async_refresh_entity(client_id, None, None, None) self._known_idle.add(client_id) self._client_device_cache.pop(client_id, None) @@ -437,7 +519,6 @@ class PlexServer: async_dispatcher_send( self.hass, PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), - sessions, ) @property @@ -572,3 +653,8 @@ class PlexServer: except MediaNotFound as failed_item: _LOGGER.error("%s not found in %s", failed_item, library_name) return None + + @property + def sensor_attributes(self): + """Return active session information for use in activity sensor.""" + return {x.sensor_user: x.sensor_title for x in self.active_sessions.values()} diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index b3fc235bfc8..1838df32a05 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.plex.const import DOMAIN from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .helpers import websocket_connected from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer from tests.async_mock import patch @@ -52,6 +53,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + websocket_connected(mock_websocket) + await hass.async_block_till_done() return plex_server return _wrapper diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index a20d70fbb7e..2fca88fae27 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,8 +1,39 @@ """Helper methods for Plex tests.""" -from plexwebsocket import SIGNAL_DATA +from datetime import timedelta + +from plexwebsocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA, STATE_CONNECTED + +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed + +UPDATE_PAYLOAD = { + "PlaySessionStateNotification": [ + { + "sessionKey": "999", + "ratingKey": "12345", + "viewOffset": 5050, + "playQueueItemID": 54321, + "state": "playing", + } + ] +} -def trigger_plex_update(mock_websocket): - """Call the websocket callback method.""" +def websocket_connected(mock_websocket): + """Call the websocket callback method to signal successful connection.""" callback = mock_websocket.call_args[0][1] - callback(SIGNAL_DATA, None, None) + callback(SIGNAL_CONNECTION_STATE, STATE_CONNECTED, None) + + +def trigger_plex_update(mock_websocket, payload=UPDATE_PAYLOAD): + """Call the websocket callback method with a Plex update.""" + callback = mock_websocket.call_args[0][1] + callback(SIGNAL_DATA, payload, None) + + +async def wait_for_debouncer(hass): + """Move time forward to wait for sensor debouncer.""" + next_update = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 13fe4c4113b..8ac894438be 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -334,6 +334,7 @@ class MockPlexSession: self.usernames = [list(MOCK_USERS)[index]] self.players = [player] self._section = MockPlexLibrarySection("Movies") + self.sessionKey = index + 1 @property def duration(self): diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index d96fdd4a00b..66cbc51ef82 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -8,16 +8,12 @@ from homeassistant.components.plex.media_browser import SPECIAL_METHODS from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT from .const import DEFAULT_DATA -from .helpers import trigger_plex_update async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket): """Test getting Plex clients from plex.tv.""" websocket_client = await hass_ws_client(hass) - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - media_players = hass.states.async_entity_ids("media_player") msg_id = 1 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index a7a2896d307..d8c010ceb92 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ) from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import ( MockGDM, MockPlexAccount, @@ -440,10 +440,10 @@ async def test_option_flow_new_users_available( OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} entry.options = OPTIONS_OWNER_ONLY - mock_plex_server = await setup_plex_server(config_entry=entry, disable_gdm=False) - with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient): - trigger_plex_update(mock_websocket) + mock_plex_server = await setup_plex_server( + config_entry=entry, disable_gdm=False + ) await hass.async_block_till_done() server_id = mock_plex_server.machineIdentifier @@ -453,6 +453,8 @@ async def test_option_flow_new_users_available( assert len(monitored_users) == 1 assert len(new_users) == 2 + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -754,7 +756,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state != ENTRY_STATE_LOADED diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 13e33791459..a1a159010ef 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,11 +13,11 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer from tests.async_mock import patch @@ -91,19 +91,19 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server): """Test setup component with config.""" - mock_plex_server = await setup_plex_server(config_entry=entry, session_type="photo") + await setup_plex_server(session_type="photo") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - - trigger_plex_update(mock_websocket) await hass.async_block_till_done() media_player = hass.states.get("media_player.plex_product_title") - assert media_player.state == "idle" + assert media_player.state == STATE_IDLE + + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == "0" async def test_setup_when_certificate_changed(hass, entry): diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 94703d5dfb3..a4bda5467e2 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -8,45 +8,30 @@ from tests.async_mock import patch async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server): """Test getting Plex clients from plex.tv.""" - mock_plex_server = await setup_plex_server() - server_id = mock_plex_server.machineIdentifier - plex_server = hass.data[DOMAIN][SERVERS][server_id] - resource = next( x for x in mock_plex_account.resources() if x.name.startswith("plex.tv Resource Player") ) with patch.object(resource, "connect", side_effect=NotFound): - await plex_server._async_update_platforms() + mock_plex_server = await setup_plex_server() await hass.async_block_till_done() + server_id = mock_plex_server.machineIdentifier + plex_server = hass.data[DOMAIN][SERVERS][server_id] media_players_before = len(hass.states.async_entity_ids("media_player")) # Ensure one more client is discovered await hass.config_entries.async_unload(entry.entry_id) - mock_plex_server = await setup_plex_server() plex_server = hass.data[DOMAIN][SERVERS][server_id] - - await plex_server._async_update_platforms() - await hass.async_block_till_done() - media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 # Ensure only plex.tv resource client is found await hass.config_entries.async_unload(entry.entry_id) - - mock_plex_server = await setup_plex_server() - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() - + mock_plex_server = await setup_plex_server(num_users=0) plex_server = hass.data[DOMAIN][SERVERS][server_id] - - await plex_server._async_update_platforms() - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("media_player")) == 1 # Ensure cache gets called diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 8ac707c393f..99a324786f6 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -26,9 +26,8 @@ from homeassistant.components.plex.const import ( from homeassistant.const import ATTR_ENTITY_ID from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .helpers import trigger_plex_update +from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import ( - MockGDM, MockPlexAccount, MockPlexAlbum, MockPlexArtist, @@ -54,15 +53,14 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve server_id = mock_plex_server.machineIdentifier - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users ignored_users = [x for x in monitored_users if not monitored_users[x]["enabled"]] assert len(monitored_users) == 1 assert len(ignored_users) == 0 + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -81,9 +79,6 @@ async def test_new_ignored_users_available( server_id = mock_plex_server.machineIdentifier - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users ignored_users = [x for x in mock_plex_server.accounts if x not in monitored_users] @@ -100,6 +95,8 @@ async def test_new_ignored_users_available( in caplog.text ) + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -111,8 +108,7 @@ async def test_network_error_during_refresh( server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -128,14 +124,14 @@ async def test_network_error_during_refresh( async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): """Test connection failure to a GDM discovered client.""" - mock_plex_server = await setup_plex_server(disable_gdm=False) - with patch( "homeassistant.components.plex.server.PlexClient", side_effect=ConnectionError ): - trigger_plex_update(mock_websocket) + mock_plex_server = await setup_plex_server(disable_gdm=False) await hass.async_block_till_done() + await wait_for_debouncer(hass) + sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -146,11 +142,7 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): """Test marking media_players as idle when sessions end.""" - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[DOMAIN][SERVERS][server_id] - - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -158,30 +150,23 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): mock_plex_server.clear_clients() mock_plex_server.clear_sessions() - await loaded_server._async_update_platforms() + trigger_plex_update(mock_websocket) await hass.async_block_till_done() + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == "0" -async def test_ignore_plex_web_client(hass, entry, mock_websocket): +async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server): """Test option to ignore Plex Web clients.""" OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True entry.options = OPTIONS - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) - ), patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)): + mock_plex_server = await setup_plex_server(config_entry=entry) + await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -197,9 +182,6 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - trigger_plex_update(mock_websocket) - await hass.async_block_till_done() - media_player_id = hass.states.async_entity_ids("media_player")[0] with patch("homeassistant.components.plex.PlexServer.create_playqueue"): assert await hass.services.async_call(