mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
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:
parent
5c50fa3405
commit
eeb1bfc6f5
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user