Discover Plex clients using GDM (#39053)

This commit is contained in:
jjlawren 2020-10-14 08:46:52 -05:00 committed by GitHub
parent 72759d7501
commit c63c253b7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 207 additions and 39 deletions

View File

@ -1,10 +1,12 @@
"""Support to embed Plex.""" """Support to embed Plex."""
import asyncio import asyncio
import functools import functools
from functools import partial
import json import json
import logging import logging
import plexapi.exceptions import plexapi.exceptions
from plexapi.gdm import GDM
from plexwebsocket import ( from plexwebsocket import (
SIGNAL_CONNECTION_STATE, SIGNAL_CONNECTION_STATE,
SIGNAL_DATA, SIGNAL_DATA,
@ -33,6 +35,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
@ -43,6 +46,8 @@ from .const import (
CONF_SERVER_IDENTIFIER, CONF_SERVER_IDENTIFIER,
DISPATCHERS, DISPATCHERS,
DOMAIN as PLEX_DOMAIN, DOMAIN as PLEX_DOMAIN,
GDM_DEBOUNCER,
GDM_SCANNER,
PLATFORMS, PLATFORMS,
PLATFORMS_COMPLETED, PLATFORMS_COMPLETED,
PLEX_SERVER_CONFIG, PLEX_SERVER_CONFIG,
@ -67,6 +72,16 @@ async def async_setup(hass, config):
await async_setup_services(hass) await async_setup_services(hass)
gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM()
hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer(
hass,
_LOGGER,
cooldown=10,
immediate=True,
function=partial(gdm.scan, scan_for_clients=True),
).async_call
return True return True
@ -143,10 +158,14 @@ async def async_setup_entry(hass, entry):
entry.add_update_listener(async_options_updated) entry.add_update_listener(async_options_updated)
async def async_update_plex():
await hass.data[PLEX_DOMAIN][GDM_DEBOUNCER]()
await plex_server.async_update_platforms()
unsub = async_dispatcher_connect( unsub = async_dispatcher_connect(
hass, hass,
PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id),
plex_server.async_update_platforms, async_update_plex,
) )
hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, [])
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)

View File

@ -13,6 +13,8 @@ PLEXTV_THROTTLE = 60
DEBOUNCE_TIMEOUT = 1 DEBOUNCE_TIMEOUT = 1
DISPATCHERS = "dispatchers" DISPATCHERS = "dispatchers"
GDM_DEBOUNCER = "gdm_debouncer"
GDM_SCANNER = "gdm_scanner"
PLATFORMS = frozenset(["media_player", "sensor"]) PLATFORMS = frozenset(["media_player", "sensor"])
PLATFORMS_COMPLETED = "platforms_completed" PLATFORMS_COMPLETED = "platforms_completed"
PLAYER_SOURCE = "player_source" PLAYER_SOURCE = "player_source"

View File

@ -4,6 +4,7 @@ import ssl
import time import time
from urllib.parse import urlparse from urllib.parse import urlparse
from plexapi.client import PlexClient
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
import plexapi.myplex import plexapi.myplex
import plexapi.playqueue import plexapi.playqueue
@ -32,6 +33,8 @@ from .const import (
CONF_USE_EPISODE_ART, CONF_USE_EPISODE_ART,
DEBOUNCE_TIMEOUT, DEBOUNCE_TIMEOUT,
DEFAULT_VERIFY_SSL, DEFAULT_VERIFY_SSL,
DOMAIN,
GDM_SCANNER,
PLAYER_SOURCE, PLAYER_SOURCE,
PLEX_NEW_MP_SIGNAL, PLEX_NEW_MP_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
@ -84,7 +87,7 @@ class PlexServer:
self._owner_username = None self._owner_username = None
self._plextv_clients = None self._plextv_clients = None
self._plextv_client_timestamp = 0 self._plextv_client_timestamp = 0
self._plextv_device_cache = {} self._client_device_cache = {}
self._use_plex_tv = self._token is not None self._use_plex_tv = self._token is not None
self._version = None self._version = None
self.async_update_platforms = Debouncer( self.async_update_platforms = Debouncer(
@ -289,6 +292,9 @@ class PlexServer:
return return
def process_device(source, device): def process_device(source, device):
if device is None:
return
self._known_idle.discard(device.machineIdentifier) self._known_idle.discard(device.machineIdentifier)
available_clients.setdefault(device.machineIdentifier, {"device": device}) available_clients.setdefault(device.machineIdentifier, {"device": device})
available_clients[device.machineIdentifier].setdefault( available_clients[device.machineIdentifier].setdefault(
@ -321,13 +327,33 @@ class PlexServer:
for device in devices: for device in devices:
process_device("PMS", device) process_device("PMS", device)
def connect_to_client(source, baseurl, machine_identifier, name="Unknown"):
"""Connect to a Plex client and return a PlexClient instance."""
try:
client = PlexClient(
server=self._plex_server,
baseurl=baseurl,
token=self._plex_server.createToken(),
)
except requests.exceptions.ConnectionError:
_LOGGER.error(
"Direct client connection failed, will try again: %s (%s)",
name,
baseurl,
)
except Unauthorized:
_LOGGER.error(
"Direct client connection unauthorized, ignoring: %s (%s)",
name,
baseurl,
)
self._client_device_cache[machine_identifier] = None
else:
self._client_device_cache[client.machineIdentifier] = client
process_device(source, client)
def connect_to_resource(resource): def connect_to_resource(resource):
"""Connect to a plex.tv resource and return a Plex client.""" """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: try:
client = resource.connect(timeout=3) client = resource.connect(timeout=3)
_LOGGER.debug("plex.tv resource connection successful: %s", client) _LOGGER.debug("plex.tv resource connection successful: %s", client)
@ -335,17 +361,33 @@ class PlexServer:
_LOGGER.error("plex.tv resource connection failed: %s", resource.name) _LOGGER.error("plex.tv resource connection failed: %s", resource.name)
else: else:
client.proxyThroughServer(value=False, server=self._plex_server) client.proxyThroughServer(value=False, server=self._plex_server)
self._client_device_cache[client.machineIdentifier] = client
process_device("plex.tv", client)
self._plextv_device_cache[client_id] = client def connect_new_clients():
return client """Create connections to newly discovered clients."""
for gdm_entry in self.hass.data[DOMAIN][GDM_SCANNER].entries:
machine_identifier = gdm_entry["data"]["Resource-Identifier"]
if machine_identifier in self._client_device_cache:
client = self._client_device_cache[machine_identifier]
if client is not None:
process_device("GDM", client)
elif machine_identifier not in available_clients:
baseurl = (
f"http://{gdm_entry['from'][0]}:{gdm_entry['data']['Port']}"
)
name = gdm_entry["data"]["Name"]
connect_to_client("GDM", baseurl, machine_identifier, name)
for plextv_client in plextv_clients: for plextv_client in plextv_clients:
if plextv_client.clientIdentifier not in available_clients: if plextv_client.clientIdentifier in self._client_device_cache:
device = await self.hass.async_add_executor_job( client = self._client_device_cache[plextv_client.clientIdentifier]
connect_to_resource, plextv_client if client is not None:
) process_device("plex.tv", client)
if device: elif plextv_client.clientIdentifier not in available_clients:
process_device("plex.tv", device) connect_to_resource(plextv_client)
await self.hass.async_add_executor_job(connect_new_clients)
for session in sessions: for session in sessions:
if session.TYPE == "photo": if session.TYPE == "photo":
@ -385,7 +427,7 @@ class PlexServer:
for client_id in idle_clients: for client_id in idle_clients:
self.async_refresh_entity(client_id, None, None) self.async_refresh_entity(client_id, None, None)
self._known_idle.add(client_id) self._known_idle.add(client_id)
self._plextv_device_cache.pop(client_id, None) self._client_device_cache.pop(client_id, None)
if new_entity_configs: if new_entity_configs:
async_dispatcher_send( async_dispatcher_send(

View File

@ -4,7 +4,7 @@ import pytest
from homeassistant.components.plex.const import DOMAIN from homeassistant.components.plex.const import DOMAIN
from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .mock_classes import MockPlexAccount, MockPlexServer from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -43,8 +43,12 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket):
async def _wrapper(**kwargs): async def _wrapper(**kwargs):
"""Wrap the fixture to allow passing arguments to the MockPlexServer instance.""" """Wrap the fixture to allow passing arguments to the MockPlexServer instance."""
config_entry = kwargs.get("config_entry", entry) config_entry = kwargs.get("config_entry", entry)
disable_gdm = kwargs.pop("disable_gdm", True)
plex_server = MockPlexServer(**kwargs) plex_server = MockPlexServer(**kwargs)
with patch("plexapi.server.PlexServer", return_value=plex_server): with patch("plexapi.server.PlexServer", return_value=plex_server), patch(
"homeassistant.components.plex.GDM",
return_value=MockGDM(disabled=disable_gdm),
):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_URL
from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS
GDM_PAYLOAD = [ GDM_SERVER_PAYLOAD = [
{ {
"data": { "data": {
"Content-Type": "plex/media-server", "Content-Type": "plex/media-server",
@ -27,17 +27,73 @@ GDM_PAYLOAD = [
} }
] ]
GDM_CLIENT_PAYLOAD = [
{
"data": {
"Content-Type": "plex/media-player",
"Device-Class": "stb",
"Name": "plexamp",
"Port": "36000",
"Product": "Plexamp",
"Protocol": "plex",
"Protocol-Capabilities": "timeline,playback,playqueues,playqueues-creation",
"Protocol-Version": "1",
"Resource-Identifier": "client-2",
"Version": "1.1.0",
},
"from": ("1.2.3.10", 32412),
},
{
"data": {
"Content-Type": "plex/media-player",
"Device-Class": "pc",
"Name": "Chrome",
"Port": "32400",
"Product": "Plex Web",
"Protocol": "plex",
"Protocol-Capabilities": "timeline,playback,navigation,mirror,playqueues",
"Protocol-Version": "3",
"Resource-Identifier": "client-1",
"Version": "4.40.1",
},
"from": ("1.2.3.4", 32412),
},
{
"data": {
"Content-Type": "plex/media-player",
"Device-Class": "mobile",
"Name": "SHIELD Android TV",
"Port": "32500",
"Product": "Plex for Android (TV)",
"Protocol": "plex",
"Protocol-Capabilities": "timeline,playback,navigation,mirror,playqueues,provider-playback",
"Protocol-Version": "1",
"Resource-Identifier": "client-999",
"Updated-At": "1597686153",
"Version": "8.5.0.19697",
},
"from": ("1.2.3.11", 32412),
},
]
class MockGDM: class MockGDM:
"""Mock a GDM instance.""" """Mock a GDM instance."""
def __init__(self): def __init__(self, disabled=False):
"""Initialize the object.""" """Initialize the object."""
self.entries = GDM_PAYLOAD self.entries = []
self.disabled = disabled
def scan(self): def scan(self, scan_for_clients=False):
"""Mock the scan call.""" """Mock the scan call."""
pass if self.disabled:
return
if scan_for_clients:
self.entries = GDM_CLIENT_PAYLOAD
else:
self.entries = GDM_SERVER_PAYLOAD
class MockResource: class MockResource:
@ -56,7 +112,9 @@ class MockResource:
self.name = f"plex.tv Resource Player {index+10}" self.name = f"plex.tv Resource Player {index+10}"
self.clientIdentifier = f"client-{index+10}" self.clientIdentifier = f"client-{index+10}"
self.provides = ["player"] self.provides = ["player"]
self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10) self.device = MockPlexClient(
baseurl=f"http://192.168.0.1{index}:32500", index=index + 10
)
self.presence = index == 0 self.presence = index == 0
self.publicAddressMatches = True self.publicAddressMatches = True
@ -122,6 +180,7 @@ class MockPlexServer:
self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users))) self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users)))
self._clients = [] self._clients = []
self._session = None
self._sessions = [] self._sessions = []
self.set_clients(num_users) self.set_clients(num_users)
self.set_sessions(num_users, session_type) self.set_sessions(num_users, session_type)
@ -130,7 +189,9 @@ class MockPlexServer:
def set_clients(self, num_clients): def set_clients(self, num_clients):
"""Set up mock PlexClients for this PlexServer.""" """Set up mock PlexClients for this PlexServer."""
self._clients = [MockPlexClient(self._baseurl, x) for x in range(num_clients)] self._clients = [
MockPlexClient(baseurl=self._baseurl, index=x) for x in range(num_clients)
]
def set_sessions(self, num_sessions, session_type): def set_sessions(self, num_sessions, session_type):
"""Set up mock PlexSessions for this PlexServer.""" """Set up mock PlexSessions for this PlexServer."""
@ -151,6 +212,10 @@ class MockPlexServer:
"""Mock the clients method.""" """Mock the clients method."""
return self._clients return self._clients
def createToken(self):
"""Mock the createToken method."""
return "temporary_token"
def sessions(self): def sessions(self):
"""Mock the sessions method.""" """Mock the sessions method."""
return self._sessions return self._sessions
@ -204,10 +269,10 @@ class MockPlexServer:
class MockPlexClient: class MockPlexClient:
"""Mock a PlexClient instance.""" """Mock a PlexClient instance."""
def __init__(self, url, index=0): def __init__(self, server=None, baseurl=None, token=None, index=0):
"""Initialize the object.""" """Initialize the object."""
self.machineIdentifier = f"client-{index+1}" self.machineIdentifier = f"client-{index+1}"
self._baseurl = url self._baseurl = baseurl
self._index = index self._index = index
def url(self, key): def url(self, key):

View File

@ -37,7 +37,13 @@ from homeassistant.const import (
from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
from .helpers import trigger_plex_update from .helpers import trigger_plex_update
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer, MockResource from .mock_classes import (
MockGDM,
MockPlexAccount,
MockPlexClient,
MockPlexServer,
MockResource,
)
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -434,10 +440,11 @@ async def test_option_flow_new_users_available(
OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}}
entry.options = OPTIONS_OWNER_ONLY entry.options = OPTIONS_OWNER_ONLY
mock_plex_server = await setup_plex_server(config_entry=entry) mock_plex_server = await setup_plex_server(config_entry=entry, disable_gdm=False)
trigger_plex_update(mock_websocket) with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient):
await hass.async_block_till_done() trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
server_id = mock_plex_server.machineIdentifier server_id = mock_plex_server.machineIdentifier
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
@ -640,7 +647,11 @@ async def test_manual_config(hass):
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True "homeassistant.components.plex.PlexWebsocket", autospec=True
), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): ), patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()
):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MANUAL_SERVER result["flow_id"], user_input=MANUAL_SERVER
) )
@ -674,7 +685,11 @@ async def test_manual_config_with_token(hass):
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
"plexapi.server.PlexServer", return_value=mock_plex_server "plexapi.server.PlexServer", return_value=mock_plex_server
), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): ), patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True
):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN}
) )

View File

@ -18,7 +18,7 @@ import homeassistant.util.dt as dt_util
from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .helpers import trigger_plex_update from .helpers import trigger_plex_update
from .mock_classes import MockPlexAccount, MockPlexServer from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -183,6 +183,8 @@ async def test_bad_token_with_tokenless_server(hass, entry):
"""Test setup with a bad token and a server with token auth disabled.""" """Test setup with a bad token and a server with token auth disabled."""
with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch( with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch(
"plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
), patch(
"homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)
), patch( ), patch(
"homeassistant.components.plex.PlexWebsocket", autospec=True "homeassistant.components.plex.PlexWebsocket", autospec=True
) as mock_websocket: ) as mock_websocket:

View File

@ -26,7 +26,7 @@ async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server
# Ensure one more client is discovered # Ensure one more client is discovered
await hass.config_entries.async_unload(entry.entry_id) await hass.config_entries.async_unload(entry.entry_id)
mock_plex_server = await setup_plex_server(config_entry=entry) mock_plex_server = await setup_plex_server()
plex_server = hass.data[DOMAIN][SERVERS][server_id] plex_server = hass.data[DOMAIN][SERVERS][server_id]
await plex_server._async_update_platforms() await plex_server._async_update_platforms()
@ -38,7 +38,7 @@ async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server
# Ensure only plex.tv resource client is found # Ensure only plex.tv resource client is found
await hass.config_entries.async_unload(entry.entry_id) await hass.config_entries.async_unload(entry.entry_id)
mock_plex_server = await setup_plex_server(config_entry=entry) mock_plex_server = await setup_plex_server()
mock_plex_server.clear_clients() mock_plex_server.clear_clients()
mock_plex_server.clear_sessions() mock_plex_server.clear_sessions()

View File

@ -2,7 +2,7 @@
import copy import copy
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from requests.exceptions import RequestException from requests.exceptions import ConnectionError, RequestException
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
@ -28,6 +28,7 @@ from homeassistant.const import ATTR_ENTITY_ID
from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .const import DEFAULT_DATA, DEFAULT_OPTIONS
from .helpers import trigger_plex_update from .helpers import trigger_plex_update
from .mock_classes import ( from .mock_classes import (
MockGDM,
MockPlexAccount, MockPlexAccount,
MockPlexAlbum, MockPlexAlbum,
MockPlexArtist, MockPlexArtist,
@ -125,6 +126,24 @@ async def test_network_error_during_refresh(
) )
async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server):
"""Test connection failure to a GDM discovered client."""
mock_plex_server = await setup_plex_server(disable_gdm=False)
with patch(
"homeassistant.components.plex.server.PlexClient", side_effect=ConnectionError
):
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
with patch.object(mock_plex_server, "clients", side_effect=RequestException):
trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
"""Test marking media_players as idle when sessions end.""" """Test marking media_players as idle when sessions end."""
server_id = mock_plex_server.machineIdentifier server_id = mock_plex_server.machineIdentifier
@ -156,7 +175,7 @@ async def test_ignore_plex_web_client(hass, entry, mock_websocket):
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
"plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)
): ), patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)):
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id) assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()