mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Discover controllable Plex clients using plex.tv (#36857)
This commit is contained in:
parent
94c8d74a66
commit
d5cc3208af
@ -9,6 +9,8 @@ DEFAULT_PORT = 32400
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
PLEXTV_THROTTLE = 60
|
||||
|
||||
DEBOUNCE_TIMEOUT = 1
|
||||
DISPATCHERS = "dispatchers"
|
||||
PLATFORMS = frozenset(["media_player", "sensor"])
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Shared class to maintain Plex server instances."""
|
||||
import logging
|
||||
import ssl
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from plexapi.exceptions import NotFound, Unauthorized
|
||||
@ -34,6 +35,7 @@ from .const import (
|
||||
PLEX_NEW_MP_SIGNAL,
|
||||
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
|
||||
PLEX_UPDATE_SENSOR_SIGNAL,
|
||||
PLEXTV_THROTTLE,
|
||||
X_PLEX_DEVICE_NAME,
|
||||
X_PLEX_PLATFORM,
|
||||
X_PLEX_PRODUCT,
|
||||
@ -70,6 +72,9 @@ class PlexServer:
|
||||
self.server_choice = None
|
||||
self._accounts = []
|
||||
self._owner_username = None
|
||||
self._plextv_clients = None
|
||||
self._plextv_client_timestamp = 0
|
||||
self._plextv_device_cache = {}
|
||||
self._version = None
|
||||
self.async_update_platforms = Debouncer(
|
||||
hass,
|
||||
@ -92,15 +97,28 @@ class PlexServer:
|
||||
self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token)
|
||||
return self._plex_account
|
||||
|
||||
def plextv_clients(self):
|
||||
"""Return available clients linked to Plex account."""
|
||||
now = time.time()
|
||||
if now - self._plextv_client_timestamp > PLEXTV_THROTTLE:
|
||||
self._plextv_client_timestamp = now
|
||||
resources = self.account.resources()
|
||||
self._plextv_clients = [
|
||||
x for x in resources if "player" in x.provides and x.presence
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Current available clients from plex.tv: %s", self._plextv_clients
|
||||
)
|
||||
return self._plextv_clients
|
||||
|
||||
def connect(self):
|
||||
"""Connect to a Plex server directly, obtaining direct URL if necessary."""
|
||||
config_entry_update_needed = False
|
||||
|
||||
def _connect_with_token():
|
||||
account = plexapi.myplex.MyPlexAccount(token=self._token)
|
||||
available_servers = [
|
||||
(x.name, x.clientIdentifier)
|
||||
for x in account.resources()
|
||||
for x in self.account.resources()
|
||||
if "server" in x.provides
|
||||
]
|
||||
|
||||
@ -112,7 +130,9 @@ class PlexServer:
|
||||
self.server_choice = (
|
||||
self._server_name if self._server_name else available_servers[0][0]
|
||||
)
|
||||
self._plex_server = account.resource(self.server_choice).connect(timeout=10)
|
||||
self._plex_server = self.account.resource(self.server_choice).connect(
|
||||
timeout=10
|
||||
)
|
||||
|
||||
def _connect_with_url():
|
||||
session = None
|
||||
@ -124,13 +144,14 @@ class PlexServer:
|
||||
)
|
||||
|
||||
def _update_plexdirect_hostname():
|
||||
account = plexapi.myplex.MyPlexAccount(token=self._token)
|
||||
matching_server = [
|
||||
x.name
|
||||
for x in account.resources()
|
||||
for x in self.account.resources()
|
||||
if x.clientIdentifier == self._server_id
|
||||
][0]
|
||||
self._plex_server = account.resource(matching_server).connect(timeout=10)
|
||||
self._plex_server = self.account.resource(matching_server).connect(
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if self._url:
|
||||
try:
|
||||
@ -193,7 +214,11 @@ class PlexServer:
|
||||
|
||||
def _fetch_platform_data(self):
|
||||
"""Fetch all data from the Plex server in a single method."""
|
||||
return (self._plex_server.clients(), self._plex_server.sessions())
|
||||
return (
|
||||
self._plex_server.clients(),
|
||||
self._plex_server.sessions(),
|
||||
self.plextv_clients(),
|
||||
)
|
||||
|
||||
async def _async_update_platforms(self):
|
||||
"""Update the platform entities."""
|
||||
@ -217,7 +242,7 @@ class PlexServer:
|
||||
monitored_users.add(new_user)
|
||||
|
||||
try:
|
||||
devices, sessions = await self.hass.async_add_executor_job(
|
||||
devices, sessions, plextv_clients = await self.hass.async_add_executor_job(
|
||||
self._fetch_platform_data
|
||||
)
|
||||
except (
|
||||
@ -245,10 +270,8 @@ class PlexServer:
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
device.machineIdentifier not in self._created_clients
|
||||
and device.machineIdentifier not in ignored_clients
|
||||
and device.machineIdentifier not in new_clients
|
||||
if device.machineIdentifier not in (
|
||||
self._created_clients | ignored_clients | new_clients
|
||||
):
|
||||
new_clients.add(device.machineIdentifier)
|
||||
_LOGGER.debug(
|
||||
@ -258,6 +281,30 @@ class PlexServer:
|
||||
for device in devices:
|
||||
process_device("device", device)
|
||||
|
||||
def connect_to_resource(resource):
|
||||
"""Connect to a plex.tv resource and return a Plex client."""
|
||||
client_id = resource.clientIdentifier
|
||||
if client_id in self._plextv_device_cache:
|
||||
return self._plextv_device_cache[client_id]
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = resource.connect(timeout=3)
|
||||
_LOGGER.debug("plex.tv resource connection successful: %s", client)
|
||||
except NotFound:
|
||||
_LOGGER.error("plex.tv resource connection failed: %s", resource.name)
|
||||
|
||||
self._plextv_device_cache[client_id] = client
|
||||
return client
|
||||
|
||||
for plextv_client in plextv_clients:
|
||||
if plextv_client.clientIdentifier not in available_clients:
|
||||
device = await self.hass.async_add_executor_job(
|
||||
connect_to_resource, plextv_client
|
||||
)
|
||||
if device:
|
||||
process_device("resource", device)
|
||||
|
||||
for session in sessions:
|
||||
if session.TYPE == "photo":
|
||||
_LOGGER.debug("Photo session detected, skipping: %s", session)
|
||||
@ -296,6 +343,7 @@ class PlexServer:
|
||||
for client_id in idle_clients:
|
||||
self.async_refresh_entity(client_id, None, None)
|
||||
self._known_idle.add(client_id)
|
||||
self._plextv_device_cache.pop(client_id, None)
|
||||
|
||||
if new_entity_configs:
|
||||
async_dispatcher_send(
|
||||
@ -390,7 +438,7 @@ class PlexServer:
|
||||
key = kwargs["plex_key"]
|
||||
try:
|
||||
return self.fetch_item(key)
|
||||
except plexapi.exceptions.NotFound:
|
||||
except NotFound:
|
||||
_LOGGER.error("Media for key %s not found", key)
|
||||
return None
|
||||
|
||||
|
@ -38,28 +38,37 @@ class MockGDM:
|
||||
class MockResource:
|
||||
"""Mock a PlexAccount resource."""
|
||||
|
||||
def __init__(self, index):
|
||||
def __init__(self, index, kind="server"):
|
||||
"""Initialize the object."""
|
||||
self.name = MOCK_SERVERS[index][CONF_SERVER]
|
||||
self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name
|
||||
CONF_SERVER_IDENTIFIER
|
||||
]
|
||||
self.provides = ["server"]
|
||||
self._mock_plex_server = MockPlexServer(index)
|
||||
if kind == "server":
|
||||
self.name = MOCK_SERVERS[index][CONF_SERVER]
|
||||
self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name
|
||||
CONF_SERVER_IDENTIFIER
|
||||
]
|
||||
self.provides = ["server"]
|
||||
self.device = MockPlexServer(index)
|
||||
else:
|
||||
self.name = f"plex.tv Resource Player {index+10}"
|
||||
self.clientIdentifier = f"client-{index+10}"
|
||||
self.provides = ["player"]
|
||||
self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10)
|
||||
self.presence = index == 0
|
||||
|
||||
def connect(self, timeout):
|
||||
"""Mock the resource connect method."""
|
||||
return self._mock_plex_server
|
||||
return self.device
|
||||
|
||||
|
||||
class MockPlexAccount:
|
||||
"""Mock a PlexAccount instance."""
|
||||
|
||||
def __init__(self, servers=1):
|
||||
def __init__(self, servers=1, players=3):
|
||||
"""Initialize the object."""
|
||||
self._resources = []
|
||||
for index in range(servers):
|
||||
self._resources.append(MockResource(index))
|
||||
for index in range(players):
|
||||
self._resources.append(MockResource(index, kind="player"))
|
||||
|
||||
def resource(self, name):
|
||||
"""Mock the PlexAccount resource lookup method."""
|
||||
|
@ -479,8 +479,9 @@ async def test_option_flow_new_users_available(hass, caplog):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
|
@ -254,8 +254,11 @@ async def test_setup_with_photo_session(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
|
||||
async_dispatcher_send(
|
||||
hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
media_player = hass.states.get("media_player.plex_product_title")
|
||||
assert media_player.state == "idle"
|
||||
|
91
tests/components/plex/test_media_players.py
Normal file
91
tests/components/plex/test_media_players.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Tests for Plex media_players."""
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
from homeassistant.components.plex.const import DOMAIN, SERVERS
|
||||
|
||||
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
|
||||
from .mock_classes import MockPlexAccount, MockPlexServer
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_plex_tv_clients(hass):
|
||||
"""Test getting Plex clients from plex.tv."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=DEFAULT_DATA,
|
||||
options=DEFAULT_OPTIONS,
|
||||
unique_id=DEFAULT_DATA["server_id"],
|
||||
)
|
||||
|
||||
mock_plex_server = MockPlexServer(config_entry=entry)
|
||||
mock_plex_account = MockPlexAccount()
|
||||
|
||||
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
|
||||
"homeassistant.components.plex.PlexWebsocket.listen"
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
plex_server = hass.data[DOMAIN][SERVERS][server_id]
|
||||
|
||||
resource = next(
|
||||
x
|
||||
for x in mock_plex_account.resources()
|
||||
if x.name.startswith("plex.tv Resource Player")
|
||||
)
|
||||
with patch(
|
||||
"plexapi.myplex.MyPlexAccount", return_value=mock_plex_account
|
||||
), patch.object(resource, "connect", side_effect=NotFound):
|
||||
await plex_server._async_update_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
media_players_before = len(hass.states.async_entity_ids("media_player"))
|
||||
|
||||
# Ensure one more client is discovered
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
|
||||
"homeassistant.components.plex.PlexWebsocket.listen"
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
plex_server = hass.data[DOMAIN][SERVERS][server_id]
|
||||
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account):
|
||||
await plex_server._async_update_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
media_players_after = len(hass.states.async_entity_ids("media_player"))
|
||||
assert media_players_after == media_players_before + 1
|
||||
|
||||
# Ensure only plex.tv resource client is found
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
mock_plex_server.clear_clients()
|
||||
mock_plex_server.clear_sessions()
|
||||
|
||||
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
|
||||
"homeassistant.components.plex.PlexWebsocket.listen"
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
plex_server = hass.data[DOMAIN][SERVERS][server_id]
|
||||
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account):
|
||||
await plex_server._async_update_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_entity_ids("media_player")) == 1
|
||||
|
||||
# Ensure cache gets called
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=mock_plex_account):
|
||||
await plex_server._async_update_platforms()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_entity_ids("media_player")) == 1
|
@ -26,6 +26,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
|
||||
from .mock_classes import (
|
||||
MockPlexAccount,
|
||||
MockPlexArtist,
|
||||
MockPlexLibrary,
|
||||
MockPlexLibrarySection,
|
||||
@ -62,8 +63,9 @@ async def test_new_users_available(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
@ -101,8 +103,9 @@ async def test_new_ignored_users_available(hass, caplog):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
@ -253,8 +256,9 @@ async def test_ignore_plex_web_client(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)):
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
sensor = hass.states.get("sensor.plex_plex_server_1")
|
||||
assert sensor.state == str(len(mock_plex_server.accounts))
|
||||
@ -287,8 +291,9 @@ async def test_media_lookups(hass):
|
||||
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
|
||||
|
||||
# Plex Key searches
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()):
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
media_player_id = hass.states.async_entity_ids("media_player")[0]
|
||||
with patch("homeassistant.components.plex.PlexServer.create_playqueue"):
|
||||
assert await hass.services.async_call(
|
||||
|
Loading…
x
Reference in New Issue
Block a user