mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add update entity for Plex Media Server (#101682)
This commit is contained in:
parent
1a7601ebbe
commit
f116e83b62
@ -20,7 +20,9 @@ DEBOUNCE_TIMEOUT = 1
|
||||
DISPATCHERS: Final = "dispatchers"
|
||||
GDM_DEBOUNCER: Final = "gdm_debouncer"
|
||||
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"
|
||||
PLAYER_SOURCE = "player_source"
|
||||
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")
|
||||
|
||||
|
||||
@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")
|
||||
async def mock_config_entry():
|
||||
"""Return the default mocked config entry."""
|
||||
@ -452,6 +470,7 @@ def mock_plex_calls(
|
||||
plex_server_clients,
|
||||
plex_server_default,
|
||||
security_token,
|
||||
update_check_nochange,
|
||||
):
|
||||
"""Mock Plex API calls."""
|
||||
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/500/items", text=playlist_500)
|
||||
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
|
||||
|
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…
x
Reference in New Issue
Block a user