From 2ff1fc83bc183d6ce00c45da13e19b9b4c0bf283 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 29 Sep 2021 19:11:53 -0500 Subject: [PATCH] Add latest added media as Plex library sensor attribute (#56235) --- homeassistant/components/plex/sensor.py | 17 +++ tests/components/plex/conftest.py | 80 ++++++++++ tests/components/plex/test_sensor.py | 137 ++++++++++++++++- .../plex/library_movies_collections.xml | 47 ++++++ .../fixtures/plex/library_movies_metadata.xml | 111 ++++++++++++++ tests/fixtures/plex/library_movies_size.xml | 3 + .../plex/library_music_collections.xml | 45 ++++++ .../fixtures/plex/library_music_metadata.xml | 143 ++++++++++++++++++ tests/fixtures/plex/library_music_size.xml | 3 + .../plex/library_tvshows_collections.xml | 45 ++++++ .../plex/library_tvshows_metadata.xml | 140 +++++++++++++++++ .../plex/library_tvshows_most_recent.xml | 12 ++ 12 files changed, 779 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/plex/library_movies_collections.xml create mode 100644 tests/fixtures/plex/library_movies_metadata.xml create mode 100644 tests/fixtures/plex/library_movies_size.xml create mode 100644 tests/fixtures/plex/library_music_collections.xml create mode 100644 tests/fixtures/plex/library_music_metadata.xml create mode 100644 tests/fixtures/plex/library_music_size.xml create mode 100644 tests/fixtures/plex/library_tvshows_collections.xml create mode 100644 tests/fixtures/plex/library_tvshows_metadata.xml create mode 100644 tests/fixtures/plex/library_tvshows_most_recent.xml diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 0969967e673..db2ce15d395 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -16,6 +16,7 @@ from .const import ( PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) +from .helpers import pretty_title LIBRARY_ATTRIBUTE_TYPES = { "artist": ["artist", "album"], @@ -28,6 +29,11 @@ LIBRARY_PRIMARY_LIBTYPE = { "artist": "track", } +LIBRARY_RECENT_LIBTYPE = { + "show": "episode", + "artist": "album", +} + LIBRARY_ICON_LOOKUP = { "artist": "mdi:music", "movie": "mdi:movie", @@ -174,6 +180,17 @@ class PlexLibrarySectionSensor(SensorEntity): libtype=libtype, includeCollections=False ) + recent_libtype = LIBRARY_RECENT_LIBTYPE.get( + self.library_type, self.library_type + ) + recently_added = self.library_section.recentlyAdded( + maxresults=1, libtype=recent_libtype + ) + if recently_added: + media = recently_added[0] + self._attr_extra_state_attributes["last_added_item"] = pretty_title(media) + self._attr_extra_state_attributes["last_added_timestamp"] = media.addedAt + @property def device_info(self): """Return a device description for device registry.""" diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 6ed8eaaa94a..cdd0d4dff3e 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -78,18 +78,54 @@ def library_movies_all_fixture(): return load_fixture("plex/library_movies_all.xml") +@pytest.fixture(name="library_movies_metadata", scope="session") +def library_movies_metadata_fixture(): + """Load payload for metadata in the movies library and return it.""" + return load_fixture("plex/library_movies_metadata.xml") + + +@pytest.fixture(name="library_movies_collections", scope="session") +def library_movies_collections_fixture(): + """Load payload for collections in the movies library and return it.""" + return load_fixture("plex/library_movies_collections.xml") + + @pytest.fixture(name="library_tvshows_all", scope="session") def library_tvshows_all_fixture(): """Load payload for all items in the tvshows library and return it.""" return load_fixture("plex/library_tvshows_all.xml") +@pytest.fixture(name="library_tvshows_metadata", scope="session") +def library_tvshows_metadata_fixture(): + """Load payload for metadata in the TV shows library and return it.""" + return load_fixture("plex/library_tvshows_metadata.xml") + + +@pytest.fixture(name="library_tvshows_collections", scope="session") +def library_tvshows_collections_fixture(): + """Load payload for collections in the TV shows library and return it.""" + return load_fixture("plex/library_tvshows_collections.xml") + + @pytest.fixture(name="library_music_all", scope="session") def library_music_all_fixture(): """Load payload for all items in the music library and return it.""" return load_fixture("plex/library_music_all.xml") +@pytest.fixture(name="library_music_metadata", scope="session") +def library_music_metadata_fixture(): + """Load payload for metadata in the music library and return it.""" + return load_fixture("plex/library_music_metadata.xml") + + +@pytest.fixture(name="library_music_collections", scope="session") +def library_music_collections_fixture(): + """Load payload for collections in the music library and return it.""" + return load_fixture("plex/library_music_collections.xml") + + @pytest.fixture(name="library_movies_sort", scope="session") def library_movies_sort_fixture(): """Load sorting payload for movie library and return it.""" @@ -120,6 +156,18 @@ def library_fixture(): return load_fixture("plex/library.xml") +@pytest.fixture(name="library_movies_size", scope="session") +def library_movies_size_fixture(): + """Load movie library size payload and return it.""" + return load_fixture("plex/library_movies_size.xml") + + +@pytest.fixture(name="library_music_size", scope="session") +def library_music_size_fixture(): + """Load music library size payload and return it.""" + return load_fixture("plex/library_music_size.xml") + + @pytest.fixture(name="library_tvshows_size", scope="session") def library_tvshows_size_fixture(): """Load tvshow library size payload and return it.""" @@ -352,10 +400,16 @@ def mock_plex_calls( library, library_sections, library_movies_all, + library_movies_collections, + library_movies_metadata, library_movies_sort, library_music_all, + library_music_collections, + library_music_metadata, library_music_sort, library_tvshows_all, + library_tvshows_collections, + library_tvshows_metadata, library_tvshows_sort, media_1, media_30, @@ -396,6 +450,32 @@ def mock_plex_calls( requests_mock.get(f"{url}/library/sections/2/all", text=library_tvshows_all) requests_mock.get(f"{url}/library/sections/3/all", text=library_music_all) + requests_mock.get( + f"{url}/library/sections/1/all?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_movies_metadata, + ) + requests_mock.get( + f"{url}/library/sections/2/all?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_tvshows_metadata, + ) + requests_mock.get( + f"{url}/library/sections/3/all?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_music_metadata, + ) + + requests_mock.get( + f"{url}/library/sections/1/collections?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_movies_collections, + ) + requests_mock.get( + f"{url}/library/sections/2/collections?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_tvshows_collections, + ) + requests_mock.get( + f"{url}/library/sections/3/collections?includeMeta=1&includeAdvanced=1&X-Plex-Container-Start=0&X-Plex-Container-Size=0", + text=library_music_collections, + ) + requests_mock.get(f"{url}/library/metadata/200/children", text=children_200) requests_mock.get(f"{url}/library/metadata/300/children", text=children_300) requests_mock.get(f"{url}/library/metadata/300/allLeaves", text=grandchildren_300) diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 39a2901e72d..0e87f25850f 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -1,11 +1,14 @@ """Tests for Plex sensors.""" -from datetime import timedelta +from datetime import datetime, timedelta +from unittest.mock import patch import requests.exceptions +from homeassistant.components.plex.const import PLEX_UPDATE_LIBRARY_SIGNAL from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import dt from .helpers import trigger_plex_update, wait_for_debouncer @@ -14,6 +17,51 @@ from tests.common import async_fire_time_changed LIBRARY_UPDATE_PAYLOAD = {"StatusNotification": [{"title": "Library scan complete"}]} +TIMESTAMP = datetime(2021, 9, 1) + + +class MockPlexMedia: + """Minimal mock of base plexapi media object.""" + + key = "key" + addedAt = str(TIMESTAMP) + listType = "video" + year = 2021 + + +class MockPlexClip(MockPlexMedia): + """Minimal mock of plexapi clip object.""" + + type = "clip" + title = "Clip 1" + + +class MockPlexMovie(MockPlexMedia): + """Minimal mock of plexapi movie object.""" + + type = "movie" + title = "Movie 1" + + +class MockPlexMusic(MockPlexMedia): + """Minimal mock of plexapi album object.""" + + listType = "audio" + type = "album" + title = "Album" + parentTitle = "Artist" + + +class MockPlexTVEpisode(MockPlexMedia): + """Minimal mock of plexapi episode object.""" + + type = "episode" + title = "Episode 5" + grandparentTitle = "TV Show" + seasonEpisode = "s01e05" + year = None + parentYear = 2021 + async def test_library_sensor_values( hass, @@ -21,11 +69,18 @@ async def test_library_sensor_values( setup_plex_server, mock_websocket, requests_mock, + library_movies_size, + library_music_size, library_tvshows_size, library_tvshows_size_episodes, library_tvshows_size_seasons, ): """Test the library sensors.""" + requests_mock.get( + "/library/sections/1/all?includeCollections=0", + text=library_movies_size, + ) + requests_mock.get( "/library/sections/2/all?includeCollections=0&type=2", text=library_tvshows_size, @@ -39,7 +94,12 @@ async def test_library_sensor_values( text=library_tvshows_size_episodes, ) - await setup_plex_server() + requests_mock.get( + "/library/sections/3/all?includeCollections=0", + text=library_music_size, + ) + + mock_plex_server = await setup_plex_server() await wait_for_debouncer(hass) activity_sensor = hass.states.get("sensor.plex_plex_server_1") @@ -59,12 +119,20 @@ async def test_library_sensor_values( hass, dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + + media = [MockPlexTVEpisode()] + with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + await hass.async_block_till_done() library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") assert library_tv_sensor.state == "10" assert library_tv_sensor.attributes["seasons"] == 1 assert library_tv_sensor.attributes["shows"] == 1 + assert ( + library_tv_sensor.attributes["last_added_item"] + == "TV Show - S01E05 - Episode 5" + ) + assert library_tv_sensor.attributes["last_added_timestamp"] == str(TIMESTAMP) # Handle `requests` exception requests_mock.get( @@ -89,7 +157,8 @@ async def test_library_sensor_values( trigger_plex_update( mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD ) - await hass.async_block_till_done() + with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + await hass.async_block_till_done() library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") assert library_tv_sensor.state == "10" @@ -105,3 +174,63 @@ async def test_library_sensor_values( library_tv_sensor = hass.states.get("sensor.plex_server_1_library_tv_shows") assert library_tv_sensor.state == STATE_UNAVAILABLE + + # Test movie library sensor + entity_registry.async_update_entity( + entity_id="sensor.plex_server_1_library_tv_shows", disabled_by="user" + ) + entity_registry.async_update_entity( + entity_id="sensor.plex_server_1_library_movies", disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + + media = [MockPlexMovie()] + with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + await hass.async_block_till_done() + + library_movies_sensor = hass.states.get("sensor.plex_server_1_library_movies") + assert library_movies_sensor.state == "1" + assert library_movies_sensor.attributes["last_added_item"] == "Movie 1 (2021)" + assert library_movies_sensor.attributes["last_added_timestamp"] == str(TIMESTAMP) + + # Test with clip + media = [MockPlexClip()] + with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + async_dispatcher_send( + hass, PLEX_UPDATE_LIBRARY_SIGNAL.format(mock_plex_server.machine_identifier) + ) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + library_movies_sensor = hass.states.get("sensor.plex_server_1_library_movies") + assert library_movies_sensor.attributes["last_added_item"] == "Clip 1" + + # Test music library sensor + entity_registry.async_update_entity( + entity_id="sensor.plex_server_1_library_movies", disabled_by="user" + ) + entity_registry.async_update_entity( + entity_id="sensor.plex_server_1_library_music", disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + + media = [MockPlexMusic()] + with patch("plexapi.library.LibrarySection.recentlyAdded", return_value=media): + await hass.async_block_till_done() + + library_music_sensor = hass.states.get("sensor.plex_server_1_library_music") + assert library_music_sensor.state == "1" + assert library_music_sensor.attributes["artists"] == 1 + assert library_music_sensor.attributes["albums"] == 1 + assert library_music_sensor.attributes["last_added_item"] == "Artist - Album (2021)" + assert library_music_sensor.attributes["last_added_timestamp"] == str(TIMESTAMP) diff --git a/tests/fixtures/plex/library_movies_collections.xml b/tests/fixtures/plex/library_movies_collections.xml new file mode 100644 index 00000000000..a5ca772f36f --- /dev/null +++ b/tests/fixtures/plex/library_movies_collections.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_movies_metadata.xml b/tests/fixtures/plex/library_movies_metadata.xml new file mode 100644 index 00000000000..e05dfabaaae --- /dev/null +++ b/tests/fixtures/plex/library_movies_metadata.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_movies_size.xml b/tests/fixtures/plex/library_movies_size.xml new file mode 100644 index 00000000000..3ad67aed531 --- /dev/null +++ b/tests/fixtures/plex/library_movies_size.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/plex/library_music_collections.xml b/tests/fixtures/plex/library_music_collections.xml new file mode 100644 index 00000000000..59f36c153b2 --- /dev/null +++ b/tests/fixtures/plex/library_music_collections.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_music_metadata.xml b/tests/fixtures/plex/library_music_metadata.xml new file mode 100644 index 00000000000..d9c6a511f82 --- /dev/null +++ b/tests/fixtures/plex/library_music_metadata.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_music_size.xml b/tests/fixtures/plex/library_music_size.xml new file mode 100644 index 00000000000..a7418df8488 --- /dev/null +++ b/tests/fixtures/plex/library_music_size.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/plex/library_tvshows_collections.xml b/tests/fixtures/plex/library_tvshows_collections.xml new file mode 100644 index 00000000000..914d99bfa91 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_collections.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_tvshows_metadata.xml b/tests/fixtures/plex/library_tvshows_metadata.xml new file mode 100644 index 00000000000..6aace99f521 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_metadata.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/library_tvshows_most_recent.xml b/tests/fixtures/plex/library_tvshows_most_recent.xml new file mode 100644 index 00000000000..3e9bd49f66e --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_most_recent.xml @@ -0,0 +1,12 @@ + + + + + + + +