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 @@
+
+
+
+
+
+
+
+