From 9d2f52dbc5dc353067e0e277dab3db2327853639 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Jul 2023 11:42:28 -0500 Subject: [PATCH] Allow deleting config entry devices in jellyfin (#97377) --- homeassistant/components/jellyfin/__init__.py | 16 +++++ .../components/jellyfin/coordinator.py | 3 + tests/components/jellyfin/test_init.py | 60 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 4ee97020724..f25c3410edb 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, LOGGER, PLATFORMS @@ -60,3 +61,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return True + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove device from a config entry.""" + data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = data.coordinators["sessions"] + + return not device_entry.identifiers.intersection( + ( + (DOMAIN, coordinator.server_id), + *((DOMAIN, id) for id in coordinator.device_ids), + ) + ) diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index 3d5b150f39f..f4ab98ca268 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -47,6 +47,7 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT], ABC): self.user_id: str = user_id self.session_ids: set[str] = set() + self.device_ids: set[str] = set() async def _async_update_data(self) -> JellyfinDataT: """Get the latest data from Jellyfin.""" @@ -75,4 +76,6 @@ class SessionsDataUpdateCoordinator( and session["Client"] != USER_APP_NAME } + self.device_ids = {session["DeviceId"] for session in sessions_by_id.values()} + return sessions_by_id diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index 56e352bd71f..9af73391d18 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -4,10 +4,29 @@ from unittest.mock import MagicMock from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import async_load_json_fixture from tests.common import MockConfigEntry +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + + +async def remove_device( + ws_client: MockHAClientWebSocket, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] async def test_config_entry_not_ready( @@ -66,3 +85,44 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert mock_config_entry.entry_id not in hass.data[DOMAIN] assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_remove_devices( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + identifiers={ + ( + DOMAIN, + "DEVICE-UUID", + ) + }, + ) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, mock_config_entry.entry_id + ) + is False + ) + old_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, "OLD-DEVICE-UUID")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), old_device_entry.id, mock_config_entry.entry_id + ) + is True + )