diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 89a3570dd10..c83dfe13347 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -161,12 +161,20 @@ async def async_setup_entry(hass, entry): } ) - hass.services.async_register( - PLEX_DOMAIN, - SERVICE_PLAY_ON_SONOS, - async_play_on_sonos_service, - schema=play_on_sonos_schema, - ) + def get_plex_account(plex_server): + try: + return plex_server.account + except plexapi.exceptions.Unauthorized: + return None + + plex_account = await hass.async_add_executor_job(get_plex_account, plex_server) + if plex_account: + hass.services.async_register( + PLEX_DOMAIN, + SERVICE_PLAY_ON_SONOS, + async_play_on_sonos_service, + schema=play_on_sonos_schema, + ) return True diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 05dae668512..a49a73cb51b 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -76,6 +76,7 @@ class PlexServer: self._plextv_clients = None self._plextv_client_timestamp = 0 self._plextv_device_cache = {} + self._use_plex_tv = self._token is not None self._version = None self.async_update_platforms = Debouncer( hass, @@ -94,18 +95,35 @@ class PlexServer: @property def account(self): """Return a MyPlexAccount instance.""" - if not self._plex_account: - self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) + if not self._plex_account and self._use_plex_tv: + try: + self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) + except Unauthorized: + self._use_plex_tv = False + _LOGGER.error("Not authorized to access plex.tv with provided token") + raise return self._plex_account + @property + def plextv_resources(self): + """Return all resources linked to Plex account.""" + if self.account is None: + return [] + + return self.account.resources() + def plextv_clients(self): """Return available clients linked to Plex account.""" + if self.account is None: + return [] + 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 + x + for x in self.plextv_resources + if "player" in x.provides and x.presence ] _LOGGER.debug( "Current available clients from plex.tv: %s", self._plextv_clients @@ -119,7 +137,7 @@ class PlexServer: def _connect_with_token(): available_servers = [ (x.name, x.clientIdentifier) - for x in self.account.resources() + for x in self.plextv_resources if "server" in x.provides ] @@ -145,14 +163,18 @@ class PlexServer: ) def _update_plexdirect_hostname(): - matching_server = [ + matching_servers = [ x.name - for x in self.account.resources() + for x in self.plextv_resources if x.clientIdentifier == self._server_id - ][0] - self._plex_server = self.account.resource(matching_server).connect( - timeout=10 - ) + ] + if matching_servers: + self._plex_server = self.account.resource(matching_servers[0]).connect( + timeout=10 + ) + return True + _LOGGER.error("Attempt to update plex.direct hostname failed") + return False if self._url: try: @@ -168,8 +190,12 @@ class PlexServer: _LOGGER.warning( "Plex SSL certificate's hostname changed, updating." ) - _update_plexdirect_hostname() - config_entry_update_needed = True + if _update_plexdirect_hostname(): + config_entry_update_needed = True + else: + raise Unauthorized( + "New certificate cannot be validated with provided token" + ) else: raise else: diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index f218b4c4d79..125367a32f6 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -367,8 +367,8 @@ async def test_option_flow(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -417,8 +417,8 @@ async def test_missing_option_flow(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -471,8 +471,8 @@ async def test_option_flow_new_users_available(hass, caplog): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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() @@ -741,6 +741,8 @@ async def test_setup_with_limited_credentials(hass): ), patch.object( mock_plex_server, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized ) as mock_accounts, patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch( "homeassistant.components.plex.PlexWebsocket.listen" ) as mock_listen: entry.add_to_hass(hass) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 5f626bf6a23..76b1138fc06 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util @@ -115,8 +115,8 @@ async def test_set_config_entry_unique_id(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -181,8 +181,8 @@ async def test_setup_with_insecure_config_entry(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -210,8 +210,8 @@ async def test_unload_config_entry(hass): assert entry is config_entries[0] with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket.listen") as mock_listen: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert mock_listen.called @@ -243,8 +243,8 @@ async def test_setup_with_photo_session(hass): ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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() @@ -254,11 +254,8 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - 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() + 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" @@ -293,10 +290,33 @@ async def test_setup_when_certificate_changed(hass): new_entry = MockConfigEntry(domain=const.DOMAIN, data=DEFAULT_DATA) + # Test with account failure + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertHostnameException + ), patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ): + old_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() + + assert old_entry.state == ENTRY_STATE_SETUP_ERROR + await hass.config_entries.async_unload(old_entry.entry_id) + + # Test with no servers found + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertHostnameException + ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)): + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() + + assert old_entry.state == ENTRY_STATE_SETUP_ERROR + await hass.config_entries.async_unload(old_entry.entry_id) + + # Test with success with patch( "plexapi.server.PlexServer", side_effect=WrongCertHostnameException ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - old_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(old_entry.entry_id) await hass.async_block_till_done() @@ -307,3 +327,32 @@ async def test_setup_when_certificate_changed(hass): old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] ) + + +async def test_tokenless_server(hass): + """Test setup with a server with token auth disabled.""" + mock_plex_server = MockPlexServer() + + TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) + TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) + + entry = MockConfigEntry( + domain=const.DOMAIN, + data=TOKENLESS_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ), 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() + + assert entry.state == ENTRY_STATE_LOADED + + server_id = mock_plex_server.machineIdentifier + + async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 0cd76c15ab6..d3e2de91cf9 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -23,8 +23,8 @@ async def test_plex_tv_clients(hass): mock_plex_account = MockPlexAccount() with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), 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() @@ -37,9 +37,7 @@ async def test_plex_tv_clients(hass): 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): + with patch.object(resource, "connect", side_effect=NotFound): await plex_server._async_update_platforms() await hass.async_block_till_done() @@ -49,16 +47,15 @@ async def test_plex_tv_clients(hass): 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" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), 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() + 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 @@ -70,22 +67,20 @@ async def test_plex_tv_clients(hass): mock_plex_server.clear_sessions() with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), 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() + 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() + await plex_server._async_update_platforms() + await hass.async_block_till_done() assert len(hass.states.async_entity_ids("media_player")) == 1 diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 7a90d8dfad8..dafc8720ab1 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -28,8 +28,8 @@ async def test_sonos_playback(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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() @@ -37,10 +37,6 @@ async def test_sonos_playback(hass): server_id = mock_plex_server.machineIdentifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - # Access and cache PlexAccount - assert loaded_server.account - # Test Sonos integration lookup failure with patch.object( hass.components.sonos, "get_coordinator_id", side_effect=HomeAssistantError diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index a42e1aff710..5cd0d13e90c 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -55,17 +55,16 @@ async def test_new_users_available(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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 - 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() + 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 @@ -95,17 +94,16 @@ async def test_new_ignored_users_available(hass, caplog): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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 - 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() + 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 @@ -248,17 +246,16 @@ async def test_ignore_plex_web_client(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0) + ), 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 - 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() + 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)) @@ -281,8 +278,8 @@ async def test_media_lookups(hass): mock_plex_server = MockPlexServer(config_entry=entry) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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() @@ -291,9 +288,9 @@ async def test_media_lookups(hass): loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches - 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() + 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(