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

View file

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

View file

@ -2,12 +2,13 @@
from homeassistant.const import __version__ from homeassistant.const import __version__
DOMAIN = "plex" DOMAIN = "plex"
NAME_FORMAT = "Plex {}" NAME_FORMAT = "Plex ({})"
DEFAULT_PORT = 32400 DEFAULT_PORT = 32400
DEFAULT_SSL = False DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
DISPATCHERS = "dispatchers"
PLATFORMS = ["media_player", "sensor"] PLATFORMS = ["media_player", "sensor"]
REFRESH_LISTENERS = "refresh_listeners" REFRESH_LISTENERS = "refresh_listeners"
SERVERS = "servers" SERVERS = "servers"
@ -16,6 +17,10 @@ PLEX_CONFIG_FILE = "plex.conf"
PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options"
PLEX_SERVER_CONFIG = "server_config" 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 = "server"
CONF_SERVER_IDENTIFIER = "server_id" CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art" CONF_USE_EPISODE_ART = "use_episode_art"

View file

@ -1,5 +1,4 @@
"""Support to interface with the Plex API.""" """Support to interface with the Plex API."""
from datetime import timedelta
import json import json
import logging import logging
from xml.etree.ElementTree import ParseError from xml.etree.ElementTree import ParseError
@ -29,14 +28,17 @@ from homeassistant.const import (
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, 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 homeassistant.util import dt as dt_util
from .const import ( from .const import (
CONF_SERVER_IDENTIFIER, CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN, DOMAIN as PLEX_DOMAIN,
NAME_FORMAT, NAME_FORMAT,
REFRESH_LISTENERS, PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
SERVERS, 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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex media_player from a config entry.""" """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] 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] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
plex_clients = {} for entity_params in new_entities:
plex_sessions = {} plex_mp = PlexMediaPlayer(plexserver, **entity_params)
hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( entities.append(plex_mp)
hass, lambda now: update_devices(), timedelta(seconds=10)
)
def update_devices(): async_add_entities(entities, True)
"""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)
class PlexClient(MediaPlayerDevice): class PlexMediaPlayer(MediaPlayerDevice):
"""Representation of a Plex device.""" """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.""" """Initialize the Plex device."""
self.plex_server = plex_server
self.device = device
self.session = session
self._app_name = "" self._app_name = ""
self._device = None
self._available = False self._available = False
self._marked_unavailable = None
self._device_protocol_capabilities = None self._device_protocol_capabilities = None
self._is_player_active = False self._is_player_active = False
self._is_player_available = False self._machine_identifier = device.machineIdentifier
self._player = None
self._machine_identifier = None
self._make = "" self._make = ""
self._name = None self._name = None
self._player_state = "idle" self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting self._previous_volume_level = 1 # Used in fake muting
self._session = None
self._session_type = None self._session_type = None
self._session_username = None self._session_username = None
self._state = STATE_IDLE self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # 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 # General
self._media_content_id = None self._media_content_id = None
self._media_content_rating = None self._media_content_rating = None
@ -208,7 +121,22 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None self._media_season = None
self._media_series_title = 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): def _clear_media_details(self):
"""Set all Media Items to None.""" """Set all Media Items to None."""
@ -232,52 +160,46 @@ class PlexClient(MediaPlayerDevice):
# Clear library Name # Clear library Name
self._app_name = "" self._app_name = ""
def refresh(self, device, session): def update(self):
"""Refresh key device data.""" """Refresh key device data."""
self._clear_media_details() self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App self._available = self.device or self.session
self._session = session name_base = None
if device:
self._device = device if self.device:
try: try:
device_url = self._device.url("/") device_url = self.device.url("/")
except plexapi.exceptions.BadRequest: except plexapi.exceptions.BadRequest:
device_url = "127.0.0.1" device_url = "127.0.0.1"
if "127.0.0.1" in device_url: if "127.0.0.1" in device_url:
self._device.proxyThroughServer() self.device.proxyThroughServer()
self._session = None name_base = self.device.title or self.device.product
self._machine_identifier = self._device.machineIdentifier self._device_protocol_capabilities = self.device.protocolCapabilities
self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME) self._player_state = self.device.state
self._device_protocol_capabilities = self._device.protocolCapabilities
# set valid session, preferring device session if not self.session:
if self._device.machineIdentifier in self.plex_sessions: self.force_idle()
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 = [
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
else: else:
self._is_player_available = False 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
name_base = name_base or session_device.title or session_device.product
else:
_LOGGER.warning("No player associated with active session")
self._session_username = self.session.usernames[0]
# Calculate throttled position for proper progress display. # Calculate throttled position for proper progress display.
position = int(self._session.viewOffset / 1000) position = int(self.session.viewOffset / 1000)
now = dt_util.utcnow() now = dt_util.utcnow()
if self._media_position is not None: if self._media_position is not None:
pos_diff = position - self._media_position pos_diff = position - self._media_position
@ -289,21 +211,22 @@ class PlexClient(MediaPlayerDevice):
self._media_position_updated_at = now self._media_position_updated_at = now
self._media_position = position self._media_position = position
self._media_content_id = self._session.ratingKey self._media_content_id = self.session.ratingKey
self._media_content_rating = getattr(self._session, "contentRating", None) 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() self._set_player_state()
if self._is_player_active and self._session is not None: if self._is_player_active and self.session is not None:
self._session_type = self._session.type self._session_type = self.session.type
self._media_duration = int(self._session.duration / 1000) self._media_duration = int(self.session.duration / 1000)
# title (movie name, tv episode name, music song name) # title (movie name, tv episode name, music song name)
self._media_title = self._session.title self._media_title = self.session.title
# media type # media type
self._set_media_type() self._set_media_type()
self._app_name = ( self._app_name = (
self._session.section().title self.session.section().title
if self._session.section() is not None if self.session.section() is not None
else "" else ""
) )
self._set_media_image() self._set_media_image()
@ -311,33 +234,21 @@ class PlexClient(MediaPlayerDevice):
self._session_type = None self._session_type = None
def _set_media_image(self): def _set_media_image(self):
thumb_url = self._session.thumbUrl thumb_url = self.session.thumbUrl
if ( if (
self.media_content_type is MEDIA_TYPE_TVSHOW self.media_content_type is MEDIA_TYPE_TVSHOW
and not self.plex_server.use_episode_art 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: if thumb_url is None:
_LOGGER.debug( _LOGGER.debug(
"Using media art because media thumb " "was not found: %s", "Using media art because media thumb was not found: %s", self.name
self.entity_id,
) )
thumb_url = self.session.url(self._session.art) thumb_url = self.session.url(self.session.art)
self._media_image_url = thumb_url 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): def _set_player_state(self):
if self._player_state == "playing": if self._player_state == "playing":
self._is_player_active = True self._is_player_active = True
@ -357,41 +268,41 @@ class PlexClient(MediaPlayerDevice):
self._media_content_type = MEDIA_TYPE_TVSHOW self._media_content_type = MEDIA_TYPE_TVSHOW
# season number (00) # season number (00)
if callable(self._session.season): if callable(self.session.season):
self._media_season = str((self._session.season()).index).zfill(2) self._media_season = str((self.session.season()).index).zfill(2)
elif self._session.parentIndex is not None: elif self.session.parentIndex is not None:
self._media_season = self._session.parentIndex.zfill(2) self._media_season = self.session.parentIndex.zfill(2)
else: else:
self._media_season = None self._media_season = None
# show name # show name
self._media_series_title = self._session.grandparentTitle self._media_series_title = self.session.grandparentTitle
# episode number (00) # episode number (00)
if self._session.index is not None: if self.session.index is not None:
self._media_episode = str(self._session.index).zfill(2) self._media_episode = str(self.session.index).zfill(2)
elif self._session_type == "movie": elif self._session_type == "movie":
self._media_content_type = MEDIA_TYPE_MOVIE self._media_content_type = MEDIA_TYPE_MOVIE
if self._session.year is not None and self._media_title is not None: if self.session.year is not None and self._media_title is not None:
self._media_title += " (" + str(self._session.year) + ")" self._media_title += " (" + str(self.session.year) + ")"
elif self._session_type == "track": elif self._session_type == "track":
self._media_content_type = MEDIA_TYPE_MUSIC self._media_content_type = MEDIA_TYPE_MUSIC
self._media_album_name = self._session.parentTitle self._media_album_name = self.session.parentTitle
self._media_album_artist = self._session.grandparentTitle self._media_album_artist = self.session.grandparentTitle
self._media_track = self._session.index self._media_track = self.session.index
self._media_artist = self._session.originalTitle self._media_artist = self.session.originalTitle
# use album artist if track artist is missing # use album artist if track artist is missing
if self._media_artist is None: if self._media_artist is None:
_LOGGER.debug( _LOGGER.debug(
"Using album artist because track artist " "was not found: %s", "Using album artist because track artist was not found: %s",
self.entity_id, self.name,
) )
self._media_artist = self._media_album_artist self._media_artist = self._media_album_artist
def force_idle(self): def force_idle(self):
"""Force client to idle.""" """Force client to idle."""
self._state = STATE_IDLE self._state = STATE_IDLE
self._session = None self.session = None
self._clear_media_details() self._clear_media_details()
@property @property
@ -402,7 +313,7 @@ class PlexClient(MediaPlayerDevice):
@property @property
def unique_id(self): def unique_id(self):
"""Return the id of this plex client.""" """Return the id of this plex client."""
return self.machine_identifier return self._machine_identifier
@property @property
def available(self): def available(self):
@ -414,31 +325,11 @@ class PlexClient(MediaPlayerDevice):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property
def machine_identifier(self):
"""Return the machine identifier of the device."""
return self._machine_identifier
@property @property
def app_name(self): def app_name(self):
"""Return the library name of playing media.""" """Return the library name of playing media."""
return self._app_name 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 @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
@ -462,8 +353,7 @@ class PlexClient(MediaPlayerDevice):
"""Return the content type of current playing media.""" """Return the content type of current playing media."""
if self._session_type == "clip": if self._session_type == "clip":
_LOGGER.debug( _LOGGER.debug(
"Clip content type detected, " "compatibility may vary: %s", "Clip content type detected, compatibility may vary: %s", self.name
self.entity_id,
) )
return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_TVSHOW
if self._session_type == "episode": if self._session_type == "episode":
@ -560,8 +450,8 @@ class PlexClient(MediaPlayerDevice):
# no mute support # no mute support
if self.make.lower() == "shield android tv": if self.make.lower() == "shield android tv":
_LOGGER.debug( _LOGGER.debug(
"Shield Android TV client detected, disabling mute " "controls: %s", "Shield Android TV client detected, disabling mute controls: %s",
self.entity_id, self.name,
) )
return ( return (
SUPPORT_PAUSE SUPPORT_PAUSE
@ -579,7 +469,7 @@ class PlexClient(MediaPlayerDevice):
_LOGGER.debug( _LOGGER.debug(
"Tivo client detected, only enabling pause, play, " "Tivo client detected, only enabling pause, play, "
"stop, and off controls: %s", "stop, and off controls: %s",
self.entity_id, self.name,
) )
return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF 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: if self.device and "playback" in self._device_protocol_capabilities:
self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) self.device.setVolume(int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve self._volume_level = volume # store since we can't retrieve
self.update_devices() self.plex_server.update_platforms()
@property @property
def volume_level(self): def volume_level(self):
@ -642,19 +532,19 @@ class PlexClient(MediaPlayerDevice):
"""Send play command.""" """Send play command."""
if self.device and "playback" in self._device_protocol_capabilities: if self.device and "playback" in self._device_protocol_capabilities:
self.device.play(self._active_media_plexapi_type) self.device.play(self._active_media_plexapi_type)
self.update_devices() self.plex_server.update_platforms()
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
if self.device and "playback" in self._device_protocol_capabilities: if self.device and "playback" in self._device_protocol_capabilities:
self.device.pause(self._active_media_plexapi_type) self.device.pause(self._active_media_plexapi_type)
self.update_devices() self.plex_server.update_platforms()
def media_stop(self): def media_stop(self):
"""Send stop command.""" """Send stop command."""
if self.device and "playback" in self._device_protocol_capabilities: if self.device and "playback" in self._device_protocol_capabilities:
self.device.stop(self._active_media_plexapi_type) self.device.stop(self._active_media_plexapi_type)
self.update_devices() self.plex_server.update_platforms()
def turn_off(self): def turn_off(self):
"""Turn the client off.""" """Turn the client off."""
@ -665,13 +555,13 @@ class PlexClient(MediaPlayerDevice):
"""Send next track command.""" """Send next track command."""
if self.device and "playback" in self._device_protocol_capabilities: if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipNext(self._active_media_plexapi_type) self.device.skipNext(self._active_media_plexapi_type)
self.update_devices() self.plex_server.update_platforms()
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
if self.device and "playback" in self._device_protocol_capabilities: if self.device and "playback" in self._device_protocol_capabilities:
self.device.skipPrevious(self._active_media_plexapi_type) 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): def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media.""" """Play a piece of media."""
@ -706,7 +596,7 @@ class PlexClient(MediaPlayerDevice):
except requests.exceptions.ConnectTimeout: except requests.exceptions.ConnectTimeout:
_LOGGER.error("Timed out playing on %s", self.name) _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): def _get_music_media(self, library_name, src):
"""Find music media and return a Plex media object.""" """Find music media and return a Plex media object."""

View file

@ -1,19 +1,21 @@
"""Support for Plex media server monitoring.""" """Support for Plex media server monitoring."""
from datetime import timedelta
import logging import logging
import plexapi.exceptions from homeassistant.core import callback
import requests.exceptions from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity 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__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Plex sensor platform. """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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex sensor from a config entry.""" """Set up Plex sensor from a config entry."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER] server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
async_add_entities([sensor], True) sensor = PlexSensor(plexserver)
async_add_entities([sensor])
class PlexSensor(Entity): class PlexSensor(Entity):
@ -35,12 +38,27 @@ class PlexSensor(Entity):
def __init__(self, plex_server): def __init__(self, plex_server):
"""Initialize the sensor.""" """Initialize the sensor."""
self.sessions = []
self._state = None self._state = None
self._now_playing = [] self._now_playing = []
self._server = plex_server 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}" 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 @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
@ -51,6 +69,11 @@ class PlexSensor(Entity):
"""Return the id of this plex client.""" """Return the id of this plex client."""
return self._unique_id return self._unique_id
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
@ -66,24 +89,10 @@ class PlexSensor(Entity):
"""Return the state attributes.""" """Return the state attributes."""
return {content[0]: content[1] for content in self._now_playing} return {content[0]: content[1] for content in self._now_playing}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Update method for Plex sensor.""" """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 = [] now_playing = []
for sess in sessions: for sess in self.sessions:
user = sess.usernames[0] user = sess.usernames[0]
device = sess.players[0].title device = sess.players[0].title
now_playing_user = f"{user} - {device}" now_playing_user = f"{user} - {device}"
@ -120,5 +129,5 @@ class PlexSensor(Entity):
now_playing_title += f" ({sess.year})" now_playing_title += f" ({sess.year})"
now_playing.append((now_playing_user, now_playing_title)) now_playing.append((now_playing_user, now_playing_title))
self._state = len(sessions) self._state = len(self.sessions)
self._now_playing = now_playing self._now_playing = now_playing

View file

@ -1,17 +1,24 @@
"""Shared class to maintain Plex server instances.""" """Shared class to maintain Plex server instances."""
import logging
import plexapi.myplex import plexapi.myplex
import plexapi.playqueue import plexapi.playqueue
import plexapi.server import plexapi.server
from requests import Session from requests import Session
import requests.exceptions
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ( from .const import (
CONF_SERVER, CONF_SERVER,
CONF_SHOW_ALL_CONTROLS, CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART, CONF_USE_EPISODE_ART,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
X_PLEX_DEVICE_NAME, X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM, X_PLEX_PLATFORM,
X_PLEX_PRODUCT, X_PLEX_PRODUCT,
@ -19,6 +26,8 @@ from .const import (
) )
from .errors import NoServersFound, ServerNotSpecified from .errors import NoServersFound, ServerNotSpecified
_LOGGER = logging.getLogger(__name__)
# Set default headers sent by plexapi # Set default headers sent by plexapi
plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
@ -31,9 +40,11 @@ plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()
class PlexServer: class PlexServer:
"""Manages a single Plex server connection.""" """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.""" """Initialize a Plex server instance."""
self._hass = hass
self._plex_server = None self._plex_server = None
self._known_clients = set()
self._url = server_config.get(CONF_URL) self._url = server_config.get(CONF_URL)
self._token = server_config.get(CONF_TOKEN) self._token = server_config.get(CONF_TOKEN)
self._server_name = server_config.get(CONF_SERVER) self._server_name = server_config.get(CONF_SERVER)
@ -76,13 +87,69 @@ class PlexServer:
else: else:
_connect_with_token() _connect_with_token()
def clients(self): def refresh_entity(self, machine_identifier, device, session):
"""Pass through clients call to plexapi.""" """Forward refresh dispatch to media_player."""
return self._plex_server.clients() dispatcher_send(
self._hass,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier),
device,
session,
)
def sessions(self): def update_platforms(self):
"""Pass through sessions call to plexapi.""" """Update the platform entities."""
return self._plex_server.sessions() 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 @property
def friendly_name(self): def friendly_name(self):