Central update for Plex platforms (#27764)

* Update Plex platforms together

* Remove unnecessary methods

* Overhaul of Plex update logic

* Apply suggestions from code review

Use set instead of list

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Review suggestions and cleanup

* Fixes, remove sensor throttle

* Guarantee entity name, use common scheme

* Keep name stable once set
This commit is contained in:
jjlawren 2019-10-19 16:31:15 -05:00 committed by Martin Hjelmare
parent 5c50fa3405
commit eeb1bfc6f5
6 changed files with 252 additions and 266 deletions

View File

@ -1,5 +1,6 @@
"""Support to embed Plex."""
import asyncio
from datetime import timedelta
import logging
import plexapi.exceptions
@ -17,6 +18,7 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from .const import (
CONF_USE_EPISODE_ART,
@ -26,6 +28,7 @@ from .const import (
DEFAULT_PORT,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
PLATFORMS,
PLEX_MEDIA_PLAYER_OPTIONS,
@ -64,7 +67,9 @@ _LOGGER = logging.getLogger(__package__)
def setup(hass, config):
"""Set up the Plex component."""
hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}})
hass.data.setdefault(
PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}}
)
plex_config = config.get(PLEX_DOMAIN, {})
if plex_config:
@ -104,7 +109,7 @@ async def async_setup_entry(hass, entry):
)
hass.config_entries.async_update_entry(entry, options=options)
plex_server = PlexServer(server_config, entry.options)
plex_server = PlexServer(hass, server_config, entry.options)
try:
await hass.async_add_executor_job(plex_server.connect)
except requests.exceptions.ConnectionError as error:
@ -129,7 +134,9 @@ async def async_setup_entry(hass, entry):
_LOGGER.debug(
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
)
hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server
server_id = plex_server.machine_identifier
hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = []
for platform in PLATFORMS:
hass.async_create_task(
@ -138,6 +145,10 @@ async def async_setup_entry(hass, entry):
entry.add_update_listener(async_options_updated)
hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval(
hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10)
)
return True
@ -146,7 +157,11 @@ async def async_unload_entry(hass, entry):
server_id = entry.data[CONF_SERVER_IDENTIFIER]
cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id)
await hass.async_add_executor_job(cancel)
cancel()
dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id)
for unsub in dispatchers:
unsub()
tasks = [
hass.config_entries.async_forward_entry_unload(entry, platform)

View File

@ -79,7 +79,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
self.current_login = server_config
plex_server = PlexServer(server_config)
plex_server = PlexServer(self.hass, server_config)
try:
await self.hass.async_add_executor_job(plex_server.connect)

View File

@ -2,12 +2,13 @@
from homeassistant.const import __version__
DOMAIN = "plex"
NAME_FORMAT = "Plex {}"
NAME_FORMAT = "Plex ({})"
DEFAULT_PORT = 32400
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
DISPATCHERS = "dispatchers"
PLATFORMS = ["media_player", "sensor"]
REFRESH_LISTENERS = "refresh_listeners"
SERVERS = "servers"
@ -16,6 +17,10 @@ PLEX_CONFIG_FILE = "plex.conf"
PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options"
PLEX_SERVER_CONFIG = "server_config"
PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal"
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}"
PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal"
CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"

View File

@ -1,5 +1,4 @@
"""Support to interface with the Plex API."""
from datetime import timedelta
import json
import logging
from xml.etree.ElementTree import ParseError
@ -29,14 +28,17 @@ from homeassistant.const import (
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.helpers.event import track_time_interval
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from .const import (
CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
REFRESH_LISTENERS,
PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
SERVERS,
)
@ -53,142 +55,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex media_player from a config entry."""
def add_entities(entities, update_before_add=False):
"""Sync version of async add entities."""
hass.add_job(async_add_entities, entities, update_before_add)
hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities)
def _setup_platform(hass, config_entry, add_entities_callback):
"""Set up the Plex media_player platform."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
def async_new_media_players(new_entities):
_async_add_entities(
hass, config_entry, async_add_entities, server_id, new_entities
)
unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players)
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
def _async_add_entities(
hass, config_entry, async_add_entities, server_id, new_entities
):
"""Set up Plex media_player entities."""
entities = []
plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
plex_clients = {}
plex_sessions = {}
hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval(
hass, lambda now: update_devices(), timedelta(seconds=10)
)
for entity_params in new_entities:
plex_mp = PlexMediaPlayer(plexserver, **entity_params)
entities.append(plex_mp)
def update_devices():
"""Update the devices objects."""
try:
devices = plexserver.clients()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices")
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Could not connect to Plex server: %s (%s)",
plexserver.friendly_name,
ex,
)
return
new_plex_clients = []
available_client_ids = []
for device in devices:
# For now, let's allow all deviceClass types
if device.deviceClass in ["badClient"]:
continue
available_client_ids.append(device.machineIdentifier)
if device.machineIdentifier not in plex_clients:
new_client = PlexClient(
plexserver, device, None, plex_sessions, update_devices
)
plex_clients[device.machineIdentifier] = new_client
_LOGGER.debug("New device: %s", device.machineIdentifier)
new_plex_clients.append(new_client)
else:
_LOGGER.debug("Refreshing device: %s", device.machineIdentifier)
plex_clients[device.machineIdentifier].refresh(device, None)
# add devices with a session and no client (ex. PlexConnect Apple TV's)
try:
sessions = plexserver.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions")
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Could not connect to Plex server: %s (%s)",
plexserver.friendly_name,
ex,
)
return
plex_sessions.clear()
for session in sessions:
for player in session.players:
plex_sessions[player.machineIdentifier] = session, player
for machine_identifier, (session, player) in plex_sessions.items():
if machine_identifier in available_client_ids:
# Avoid using session if already added as a device.
_LOGGER.debug("Skipping session, device exists: %s", machine_identifier)
continue
if (
machine_identifier not in plex_clients
and machine_identifier is not None
):
new_client = PlexClient(
plexserver, player, session, plex_sessions, update_devices
)
plex_clients[machine_identifier] = new_client
_LOGGER.debug("New session: %s", machine_identifier)
new_plex_clients.append(new_client)
else:
_LOGGER.debug("Refreshing session: %s", machine_identifier)
plex_clients[machine_identifier].refresh(None, session)
for client in plex_clients.values():
# force devices to idle that do not have a valid session
if client.session is None:
client.force_idle()
client.set_availability(
client.machine_identifier in available_client_ids
or client.machine_identifier in plex_sessions
)
if client not in new_plex_clients:
client.schedule_update_ha_state()
if new_plex_clients:
add_entities_callback(new_plex_clients)
async_add_entities(entities, True)
class PlexClient(MediaPlayerDevice):
class PlexMediaPlayer(MediaPlayerDevice):
"""Representation of a Plex device."""
def __init__(self, plex_server, device, session, plex_sessions, update_devices):
def __init__(self, plex_server, device, session=None):
"""Initialize the Plex device."""
self.plex_server = plex_server
self.device = device
self.session = session
self._app_name = ""
self._device = None
self._available = False
self._marked_unavailable = None
self._device_protocol_capabilities = None
self._is_player_active = False
self._is_player_available = False
self._player = None
self._machine_identifier = None
self._machine_identifier = device.machineIdentifier
self._make = ""
self._name = None
self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting
self._session = None
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
self.plex_server = plex_server
self.plex_sessions = plex_sessions
self.update_devices = update_devices
# General
self._media_content_id = None
self._media_content_rating = None
@ -208,7 +121,22 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None
self._media_series_title = None
self.refresh(device, session)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
server_id = self.plex_server.machine_identifier
unsub = async_dispatcher_connect(
self.hass,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id),
self.async_refresh_media_player,
)
self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
def async_refresh_media_player(self, device, session):
"""Set instance objects and trigger an entity state update."""
self.device = device
self.session = session
self.async_schedule_update_ha_state(True)
def _clear_media_details(self):
"""Set all Media Items to None."""
@ -232,52 +160,46 @@ class PlexClient(MediaPlayerDevice):
# Clear library Name
self._app_name = ""
def refresh(self, device, session):
def update(self):
"""Refresh key device data."""
self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App
self._session = session
if device:
self._device = device
self._available = self.device or self.session
name_base = None
if self.device:
try:
device_url = self._device.url("/")
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._session = None
self._machine_identifier = self._device.machineIdentifier
self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME)
self._device_protocol_capabilities = self._device.protocolCapabilities
self.device.proxyThroughServer()
name_base = self.device.title or self.device.product
self._device_protocol_capabilities = self.device.protocolCapabilities
self._player_state = self.device.state
# set valid session, preferring device session
if self._device.machineIdentifier in self.plex_sessions:
self._session = self.plex_sessions.get(
self._device.machineIdentifier, [None, None]
)[0]
if self._session:
if (
self._device is not None
and self._device.machineIdentifier is not None
and self._session.players
):
self._is_player_available = True
self._player = [
if not self.session:
self.force_idle()
else:
session_device = next(
(
p
for p in self._session.players
if p.machineIdentifier == self._device.machineIdentifier
][0]
self._name = NAME_FORMAT.format(self._player.title)
self._player_state = self._player.state
self._session_username = self._session.usernames[0]
self._make = self._player.device
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
name_base = name_base or session_device.title or session_device.product
else:
self._is_player_available = False
_LOGGER.warning("No player associated with active session")
self._session_username = self.session.usernames[0]
# Calculate throttled position for proper progress display.
position = int(self._session.viewOffset / 1000)
position = int(self.session.viewOffset / 1000)
now = dt_util.utcnow()
if self._media_position is not None:
pos_diff = position - self._media_position
@ -289,21 +211,22 @@ class PlexClient(MediaPlayerDevice):
self._media_position_updated_at = now
self._media_position = position
self._media_content_id = self._session.ratingKey
self._media_content_rating = getattr(self._session, "contentRating", None)
self._media_content_id = self.session.ratingKey
self._media_content_rating = getattr(self.session, "contentRating", None)
self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME)
self._set_player_state()
if self._is_player_active and self._session is not None:
self._session_type = self._session.type
self._media_duration = int(self._session.duration / 1000)
if self._is_player_active and self.session is not None:
self._session_type = self.session.type
self._media_duration = int(self.session.duration / 1000)
# title (movie name, tv episode name, music song name)
self._media_title = self._session.title
self._media_title = self.session.title
# media type
self._set_media_type()
self._app_name = (
self._session.section().title
if self._session.section() is not None
self.session.section().title
if self.session.section() is not None
else ""
)
self._set_media_image()
@ -311,33 +234,21 @@ class PlexClient(MediaPlayerDevice):
self._session_type = None
def _set_media_image(self):
thumb_url = self._session.thumbUrl
thumb_url = self.session.thumbUrl
if (
self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.plex_server.use_episode_art
):
thumb_url = self._session.url(self._session.grandparentThumb)
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.entity_id,
"Using media art because media thumb was not found: %s", self.name
)
thumb_url = self.session.url(self._session.art)
thumb_url = self.session.url(self.session.art)
self._media_image_url = thumb_url
def set_availability(self, available):
"""Set the device as available/unavailable noting time."""
if not available:
self._clear_media_details()
if self._marked_unavailable is None:
self._marked_unavailable = dt_util.utcnow()
else:
self._marked_unavailable = None
self._available = available
def _set_player_state(self):
if self._player_state == "playing":
self._is_player_active = True
@ -357,41 +268,41 @@ class PlexClient(MediaPlayerDevice):
self._media_content_type = MEDIA_TYPE_TVSHOW
# season number (00)
if callable(self._session.season):
self._media_season = str((self._session.season()).index).zfill(2)
elif self._session.parentIndex is not None:
self._media_season = self._session.parentIndex.zfill(2)
if callable(self.session.season):
self._media_season = str((self.session.season()).index).zfill(2)
elif self.session.parentIndex is not None:
self._media_season = self.session.parentIndex.zfill(2)
else:
self._media_season = None
# show name
self._media_series_title = self._session.grandparentTitle
self._media_series_title = self.session.grandparentTitle
# episode number (00)
if self._session.index is not None:
self._media_episode = str(self._session.index).zfill(2)
if self.session.index is not None:
self._media_episode = str(self.session.index).zfill(2)
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 += " (" + str(self._session.year) + ")"
if self.session.year is not None and self._media_title is not None:
self._media_title += " (" + str(self.session.year) + ")"
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
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.entity_id,
"Using album artist because track artist was not found: %s",
self.name,
)
self._media_artist = self._media_album_artist
def force_idle(self):
"""Force client to idle."""
self._state = STATE_IDLE
self._session = None
self.session = None
self._clear_media_details()
@property
@ -402,7 +313,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
return self.machine_identifier
return self._machine_identifier
@property
def available(self):
@ -414,31 +325,11 @@ class PlexClient(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
@property
def machine_identifier(self):
"""Return the machine identifier of the device."""
return self._machine_identifier
@property
def app_name(self):
"""Return the library name of playing media."""
return self._app_name
@property
def device(self):
"""Return the device, if any."""
return self._device
@property
def marked_unavailable(self):
"""Return time device was marked unavailable."""
return self._marked_unavailable
@property
def session(self):
"""Return the session, if any."""
return self._session
@property
def state(self):
"""Return the state of the device."""
@ -462,8 +353,7 @@ class PlexClient(MediaPlayerDevice):
"""Return the content type of current playing media."""
if self._session_type == "clip":
_LOGGER.debug(
"Clip content type detected, " "compatibility may vary: %s",
self.entity_id,
"Clip content type detected, compatibility may vary: %s", self.name
)
return MEDIA_TYPE_TVSHOW
if self._session_type == "episode":
@ -560,8 +450,8 @@ class PlexClient(MediaPlayerDevice):
# no mute support
if self.make.lower() == "shield android tv":
_LOGGER.debug(
"Shield Android TV client detected, disabling mute " "controls: %s",
self.entity_id,
"Shield Android TV client detected, disabling mute controls: %s",
self.name,
)
return (
SUPPORT_PAUSE
@ -579,7 +469,7 @@ class PlexClient(MediaPlayerDevice):
_LOGGER.debug(
"Tivo client detected, only enabling pause, play, "
"stop, and off controls: %s",
self.entity_id,
self.name,
)
return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF
@ -603,7 +493,7 @@ class PlexClient(MediaPlayerDevice):
if self.device and "playback" in self._device_protocol_capabilities:
self.device.setVolume(int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
self.update_devices()
self.plex_server.update_platforms()
@property
def volume_level(self):
@ -642,19 +532,19 @@ class PlexClient(MediaPlayerDevice):
"""Send play command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.play(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_pause(self):
"""Send pause command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.pause(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_stop(self):
"""Send stop command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.stop(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def turn_off(self):
"""Turn the client off."""
@ -665,13 +555,13 @@ class PlexClient(MediaPlayerDevice):
"""Send next track command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipNext(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def media_previous_track(self):
"""Send previous track command."""
if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipPrevious(self._active_media_plexapi_type)
self.update_devices()
self.plex_server.update_platforms()
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
@ -706,7 +596,7 @@ class PlexClient(MediaPlayerDevice):
except requests.exceptions.ConnectTimeout:
_LOGGER.error("Timed out playing on %s", self.name)
self.update_devices()
self.plex_server.update_platforms()
def _get_music_media(self, library_name, src):
"""Find music media and return a Plex media object."""

View File

@ -1,19 +1,21 @@
"""Support for Plex media server monitoring."""
from datetime import timedelta
import logging
import plexapi.exceptions
import requests.exceptions
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS
from .const import (
CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
PLEX_UPDATE_SENSOR_SIGNAL,
SERVERS,
)
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Plex sensor platform.
@ -26,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
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]
sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id])
async_add_entities([sensor], True)
plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
sensor = PlexSensor(plexserver)
async_add_entities([sensor])
class PlexSensor(Entity):
@ -35,12 +38,27 @@ class PlexSensor(Entity):
def __init__(self, plex_server):
"""Initialize the sensor."""
self.sessions = []
self._state = None
self._now_playing = []
self._server = plex_server
self._name = f"Plex ({plex_server.friendly_name})"
self._name = NAME_FORMAT.format(plex_server.friendly_name)
self._unique_id = f"sensor-{plex_server.machine_identifier}"
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
server_id = self._server.machine_identifier
unsub = async_dispatcher_connect(
self.hass, PLEX_UPDATE_SENSOR_SIGNAL, self.async_refresh_sensor
)
self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
def async_refresh_sensor(self, sessions):
"""Set instance object and trigger an entity state update."""
self.sessions = sessions
self.async_schedule_update_ha_state(True)
@property
def name(self):
"""Return the name of the sensor."""
@ -51,6 +69,11 @@ class PlexSensor(Entity):
"""Return the id of this plex client."""
return self._unique_id
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def state(self):
"""Return the state of the sensor."""
@ -66,24 +89,10 @@ class PlexSensor(Entity):
"""Return the state attributes."""
return {content[0]: content[1] for content in self._now_playing}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update method for Plex sensor."""
try:
sessions = self._server.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.error(
"Error listing current Plex sessions on %s", self._server.friendly_name
)
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Temporary error connecting to %s (%s)", self._server.friendly_name, ex
)
return
now_playing = []
for sess in sessions:
for sess in self.sessions:
user = sess.usernames[0]
device = sess.players[0].title
now_playing_user = f"{user} - {device}"
@ -120,5 +129,5 @@ class PlexSensor(Entity):
now_playing_title += f" ({sess.year})"
now_playing.append((now_playing_user, now_playing_title))
self._state = len(sessions)
self._state = len(self.sessions)
self._now_playing = now_playing

View File

@ -1,17 +1,24 @@
"""Shared class to maintain Plex server instances."""
import logging
import plexapi.myplex
import plexapi.playqueue
import plexapi.server
from requests import Session
import requests.exceptions
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
CONF_SERVER,
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
DEFAULT_VERIFY_SSL,
PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
@ -19,6 +26,8 @@ from .const import (
)
from .errors import NoServersFound, ServerNotSpecified
_LOGGER = logging.getLogger(__name__)
# Set default headers sent by plexapi
plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
@ -31,9 +40,11 @@ plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
class PlexServer:
"""Manages a single Plex server connection."""
def __init__(self, server_config, options=None):
def __init__(self, hass, server_config, options=None):
"""Initialize a Plex server instance."""
self._hass = hass
self._plex_server = None
self._known_clients = set()
self._url = server_config.get(CONF_URL)
self._token = server_config.get(CONF_TOKEN)
self._server_name = server_config.get(CONF_SERVER)
@ -76,13 +87,69 @@ class PlexServer:
else:
_connect_with_token()
def clients(self):
"""Pass through clients call to plexapi."""
return self._plex_server.clients()
def refresh_entity(self, machine_identifier, device, session):
"""Forward refresh dispatch to media_player."""
dispatcher_send(
self._hass,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier),
device,
session,
)
def sessions(self):
"""Pass through sessions call to plexapi."""
return self._plex_server.sessions()
def update_platforms(self):
"""Update the platform entities."""
available_clients = {}
new_clients = set()
try:
devices = self._plex_server.clients()
sessions = self._plex_server.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error requesting Plex client data from server")
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Could not connect to Plex server: %s (%s)", self.friendly_name, ex
)
return
for device in devices:
available_clients[device.machineIdentifier] = {"device": device}
if device.machineIdentifier not in self._known_clients:
new_clients.add(device.machineIdentifier)
_LOGGER.debug("New device: %s", device.machineIdentifier)
for session in sessions:
for player in session.players:
available_clients.setdefault(
player.machineIdentifier, {"device": player}
)
available_clients[player.machineIdentifier]["session"] = session
if player.machineIdentifier not in self._known_clients:
new_clients.add(player.machineIdentifier)
_LOGGER.debug("New session: %s", player.machineIdentifier)
new_entity_configs = []
for client_id, client_data in available_clients.items():
if client_id in new_clients:
new_entity_configs.append(client_data)
else:
self.refresh_entity(
client_id, client_data["device"], client_data.get("session")
)
self._known_clients.update(new_clients)
idle_clients = self._known_clients.difference(available_clients)
for client_id in idle_clients:
self.refresh_entity(client_id, None, None)
if new_entity_configs:
dispatcher_send(self._hass, PLEX_NEW_MP_SIGNAL, new_entity_configs)
dispatcher_send(self._hass, PLEX_UPDATE_SENSOR_SIGNAL, sessions)
@property
def friendly_name(self):