8000 Discover controllable Plex clients using plex.tv by jjlawren · Pull Request #36857 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Discover controllable Plex clients using plex.tv #36857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/plex/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True

PLEXTV_THROTTLE = 60

DEBOUNCE_TIMEOUT = 1
DISPATCHERS = "dispatchers"
PLATFORMS = frozenset(["media_player", "sensor"])
Expand Down
74 changes: 61 additions & 13 deletions homeassistant/components/plex/server.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -34,6 +35,7 @@
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,
Expand Down Expand Up @@ -70,6 +72,9 @@ def __init__(self, hass, server_config, known_server_id=None, options=None):
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,
Expand All @@ -92,15 +97,28 @@ def account(self):
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
]

Expand All @@ -112,7 +130,9 @@ def _connect_with_token():
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
Expand All @@ -124,13 +144,14 @@ def _connect_with_url():
)

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:
Expand Down Expand Up @@ -193,7 +214,11 @@ def async_refresh_entity(self, machine_identifier, device, session):

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."""
Expand All @@ -217,7 +242,7 @@ async def _async_update_platforms(self):
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 (
Expand Down Expand Up @@ -245,10 +270,8 @@ def process_device(source, device):
)
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(
Expand All @@ -258,6 +281,30 @@ def process_device(source, device):
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)
Expand Down Expand Up @@ -296,6 +343,7 @@ def process_device(source, device):
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(
Expand Down Expand Up @@ -390,7 +438,7 @@ def lookup_media(self, media_type, **kwargs):
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

Expand Down
27 changes: 18 additions & 9 deletions tests/components/plex/mock_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,37 @@ def scan(self):
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."""
Expand Down
5 changes: 3 additions & 2 deletions tests/components/plex/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions tests/components/plex/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
91 changes: 91 additions & 0 deletions tests/components/plex/test_media_players.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 13 additions & 8 deletions tests/components/plex/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .mock_classes import (
MockPlexAccount,
MockPlexArtist,
MockPlexLibrary,
MockPlexLibrarySection,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(
Expand Down
0