Add update entity for Plex Media Server (#101682)
This commit is contained in:
parent
1a7601ebbe
commit
f116e83b62
7 changed files with 223 additions and 1 deletions
|
@ -20,7 +20,9 @@ DEBOUNCE_TIMEOUT = 1
|
||||||
DISPATCHERS: Final = "dispatchers"
|
DISPATCHERS: Final = "dispatchers"
|
||||||
GDM_DEBOUNCER: Final = "gdm_debouncer"
|
GDM_DEBOUNCER: Final = "gdm_debouncer"
|
||||||
GDM_SCANNER: Final = "gdm_scanner"
|
GDM_SCANNER: Final = "gdm_scanner"
|
||||||
PLATFORMS = frozenset([Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR])
|
PLATFORMS = frozenset(
|
||||||
|
[Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE]
|
||||||
|
)
|
||||||
PLATFORMS_COMPLETED: Final = "platforms_completed"
|
PLATFORMS_COMPLETED: Final = "platforms_completed"
|
||||||
PLAYER_SOURCE = "player_source"
|
PLAYER_SOURCE = "player_source"
|
||||||
SERVERS: Final = "servers"
|
SERVERS: Final = "servers"
|
||||||
|
|
76
homeassistant/components/plex/update.py
Normal file
76
homeassistant/components/plex/update.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""Representation of Plex updates."""
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from plexapi.exceptions import PlexApiException
|
||||||
|
import plexapi.server
|
||||||
|
import requests.exceptions
|
||||||
|
|
||||||
|
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import CONF_SERVER_IDENTIFIER
|
||||||
|
from .helpers import get_plex_server
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Plex media_player from a config entry."""
|
||||||
|
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
|
||||||
|
server = get_plex_server(hass, server_id)
|
||||||
|
plex_server = server.plex_server
|
||||||
|
can_update = await hass.async_add_executor_job(plex_server.canInstallUpdate)
|
||||||
|
async_add_entities([PlexUpdate(plex_server, can_update)], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PlexUpdate(UpdateEntity):
|
||||||
|
"""Representation of a Plex server update entity."""
|
||||||
|
|
||||||
|
_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
|
||||||
|
_release_notes: str | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, plex_server: plexapi.server.PlexServer, can_update: bool
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Update entity."""
|
||||||
|
self.plex_server = plex_server
|
||||||
|
self._attr_name = f"Plex Media Server ({plex_server.friendlyName})"
|
||||||
|
self._attr_unique_id = plex_server.machineIdentifier
|
||||||
|
if can_update:
|
||||||
|
self._attr_supported_features |= UpdateEntityFeature.INSTALL
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
"""Update sync attributes."""
|
||||||
|
self._attr_installed_version = self.plex_server.version
|
||||||
|
try:
|
||||||
|
if (release := self.plex_server.checkForUpdate()) is None:
|
||||||
|
return
|
||||||
|
except (requests.exceptions.RequestException, PlexApiException):
|
||||||
|
_LOGGER.debug("Polling update sensor failed, will try again")
|
||||||
|
return
|
||||||
|
self._attr_latest_version = release.version
|
||||||
|
if release.fixed:
|
||||||
|
self._release_notes = "\n".join(
|
||||||
|
f"* {line}" for line in release.fixed.split("\n")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._release_notes = None
|
||||||
|
|
||||||
|
def release_notes(self) -> str | None:
|
||||||
|
"""Return release notes for the available upgrade."""
|
||||||
|
return self._release_notes
|
||||||
|
|
||||||
|
def install(self, version: str | None, backup: bool, **kwargs: Any) -> None:
|
||||||
|
"""Install an update."""
|
||||||
|
try:
|
||||||
|
self.plex_server.installUpdate()
|
||||||
|
except (requests.exceptions.RequestException, PlexApiException) as exc:
|
||||||
|
raise HomeAssistantError(str(exc)) from exc
|
|
@ -396,6 +396,24 @@ def hubs_music_library_fixture():
|
||||||
return load_fixture("plex/hubs_library_section.xml")
|
return load_fixture("plex/hubs_library_section.xml")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="update_check_nochange", scope="session")
|
||||||
|
def update_check_fixture_nochange() -> str:
|
||||||
|
"""Load a no-change update resource payload and return it."""
|
||||||
|
return load_fixture("plex/release_nochange.xml")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="update_check_new", scope="session")
|
||||||
|
def update_check_fixture_new() -> str:
|
||||||
|
"""Load a changed update resource payload and return it."""
|
||||||
|
return load_fixture("plex/release_new.xml")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="update_check_new_not_updatable", scope="session")
|
||||||
|
def update_check_fixture_new_not_updatable() -> str:
|
||||||
|
"""Load a changed update resource payload (not updatable) and return it."""
|
||||||
|
return load_fixture("plex/release_new_not_updatable.xml")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="entry")
|
@pytest.fixture(name="entry")
|
||||||
async def mock_config_entry():
|
async def mock_config_entry():
|
||||||
"""Return the default mocked config entry."""
|
"""Return the default mocked config entry."""
|
||||||
|
@ -452,6 +470,7 @@ def mock_plex_calls(
|
||||||
plex_server_clients,
|
plex_server_clients,
|
||||||
plex_server_default,
|
plex_server_default,
|
||||||
security_token,
|
security_token,
|
||||||
|
update_check_nochange,
|
||||||
):
|
):
|
||||||
"""Mock Plex API calls."""
|
"""Mock Plex API calls."""
|
||||||
requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users)
|
requests_mock.get("https://plex.tv/api/users/", text=plextv_shared_users)
|
||||||
|
@ -519,6 +538,8 @@ def mock_plex_calls(
|
||||||
requests_mock.get(f"{url}/playlists", text=playlists)
|
requests_mock.get(f"{url}/playlists", text=playlists)
|
||||||
requests_mock.get(f"{url}/playlists/500/items", text=playlist_500)
|
requests_mock.get(f"{url}/playlists/500/items", text=playlist_500)
|
||||||
requests_mock.get(f"{url}/security/token", text=security_token)
|
requests_mock.get(f"{url}/security/token", text=security_token)
|
||||||
|
requests_mock.put(f"{url}/updater/check")
|
||||||
|
requests_mock.get(f"{url}/updater/status", text=update_check_nochange)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
4
tests/components/plex/fixtures/release_new.xml
Normal file
4
tests/components/plex/fixtures/release_new.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MediaContainer size="1" canInstall="1" checkedAt="1696794052" downloadURL="https://plex.tv/downloads/latest/5?channel=0&build=linux-x86_64&distro=debian&X-Plex-Token=xxxxxxxxxxxxxxxxxxxx" status="0">
|
||||||
|
<Release key="https://plex.tv/updater/releases/9999" version="1.50.0" added="" fixed="Summary of
release notes" downloadURL="https://plex.tv/downloads/latest/5?channel=0&build=linux-x86_64&distro=debian&X-Plex-Token=xxxxxxxxxxxxxxxxxxxx" state="notify" />
|
||||||
|
</MediaContainer>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MediaContainer size="1" canInstall="0" checkedAt="1696794052" downloadURL="https://plex.tv/downloads/latest/5?channel=0&build=linux-x86_64&distro=debian&X-Plex-Token=xxxxxxxxxxxxxxxxxxxx" status="0">
|
||||||
|
<Release key="https://plex.tv/updater/releases/9999" version="1.50.0" added="" fixed="Summary of
release notes" downloadURL="https://plex.tv/downloads/latest/5?channel=0&build=linux-x86_64&distro=debian&X-Plex-Token=xxxxxxxxxxxxxxxxxxxx" state="notify" />
|
||||||
|
</MediaContainer>
|
4
tests/components/plex/fixtures/release_nochange.xml
Normal file
4
tests/components/plex/fixtures/release_nochange.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MediaContainer size="1" canInstall="0" checkedAt="1696794052" downloadURL="https://plex.tv/downloads/latest/5?channel=0&build=linux-x86_64&distro=debian&X-Plex-Token=xxxxxxxxxxxxxxxxxxxx" status="0">
|
||||||
|
<Release key="https://plex.tv/updater/releases/1" version="1.20.4.3517-ab5e1197c" added="" fixed="" downloadURL="" state="notify" />
|
||||||
|
</MediaContainer>
|
111
tests/components/plex/test_update.py
Normal file
111
tests/components/plex/test_update.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
"""Tests for update entities."""
|
||||||
|
import pytest
|
||||||
|
import requests_mock
|
||||||
|
|
||||||
|
from homeassistant.components.update import (
|
||||||
|
DOMAIN as UPDATE_DOMAIN,
|
||||||
|
SCAN_INTERVAL as UPDATER_SCAN_INTERVAL,
|
||||||
|
SERVICE_INSTALL,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant, HomeAssistantError
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
UPDATE_ENTITY = "update.plex_media_server_plex_server_1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_plex_update(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: MockConfigEntry,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
mock_plex_server,
|
||||||
|
requests_mock: requests_mock.Mocker,
|
||||||
|
empty_payload: str,
|
||||||
|
update_check_new: str,
|
||||||
|
update_check_new_not_updatable: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test Plex update entity."""
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "update/release_notes",
|
||||||
|
"entity_id": UPDATE_ENTITY,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"] is None
|
||||||
|
|
||||||
|
apply_mock = requests_mock.put("/updater/apply")
|
||||||
|
|
||||||
|
# Failed updates
|
||||||
|
requests_mock.get("/updater/status", status_code=500)
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + UPDATER_SCAN_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
requests_mock.get("/updater/status", text=empty_payload)
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + UPDATER_SCAN_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# New release (not updatable)
|
||||||
|
requests_mock.get("/updater/status", text=update_check_new_not_updatable)
|
||||||
|
async_fire_time_changed(hass, dt_util.utcnow() + UPDATER_SCAN_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(UPDATE_ENTITY).state == STATE_ON
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
UPDATE_DOMAIN,
|
||||||
|
SERVICE_INSTALL,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert not apply_mock.called
|
||||||
|
|
||||||
|
# New release (updatable)
|
||||||
|
requests_mock.get("/updater/status", text=update_check_new)
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get(UPDATE_ENTITY).state == STATE_ON
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
await ws_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "update/release_notes",
|
||||||
|
"entity_id": UPDATE_ENTITY,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"] == "* Summary of\n* release notes"
|
||||||
|
|
||||||
|
# Successful upgrade request
|
||||||
|
await hass.services.async_call(
|
||||||
|
UPDATE_DOMAIN,
|
||||||
|
SERVICE_INSTALL,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert apply_mock.called_once
|
||||||
|
|
||||||
|
# Failed upgrade request
|
||||||
|
requests_mock.put("/updater/apply", status_code=500)
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
UPDATE_DOMAIN,
|
||||||
|
SERVICE_INSTALL,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: UPDATE_ENTITY,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue