Add Jellyfin remote entity (#126461)

* jellyfin: Add remote entity

This allows sending general commands via the
"Sessions/{sessionId}/Command" endpoint

* jellyfin: Add remote entity tests
This commit is contained in:
Ian Hattendorf 2024-10-10 00:30:05 -07:00 committed by GitHub
parent 347440019e
commit 4efb747389
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 175 additions and 1 deletions

View File

@ -83,5 +83,5 @@ MEDIA_CLASS_MAP = {
"Season": MediaClass.SEASON,
}
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR]
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
LOGGER = logging.getLogger(__package__)

View File

@ -41,6 +41,7 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, An
self.user_id: str = user_id
self.session_ids: set[str] = set()
self.remote_session_ids: set[str] = set()
self.device_ids: set[str] = set()
async def _async_update_data(self) -> dict[str, dict[str, Any]]:

View File

@ -0,0 +1,80 @@
"""Support for Jellyfin remote commands."""
from __future__ import annotations
from collections.abc import Iterable
import time
from typing import Any
from homeassistant.components.remote import (
ATTR_DELAY_SECS,
ATTR_NUM_REPEATS,
DEFAULT_DELAY_SECS,
DEFAULT_NUM_REPEATS,
RemoteEntity,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import JellyfinConfigEntry
from .const import LOGGER
from .coordinator import JellyfinDataUpdateCoordinator
from .entity import JellyfinClientEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: JellyfinConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Jellyfin remote from a config entry."""
coordinator = entry.runtime_data
@callback
def handle_coordinator_update() -> None:
"""Add remote per session."""
entities: list[RemoteEntity] = []
for session_id, session_data in coordinator.data.items():
if (
session_id not in coordinator.remote_session_ids
and session_data["SupportsRemoteControl"]
):
entity = JellyfinRemote(coordinator, session_id)
LOGGER.debug("Creating remote for session: %s", session_id)
coordinator.remote_session_ids.add(session_id)
entities.append(entity)
async_add_entities(entities)
handle_coordinator_update()
entry.async_on_unload(coordinator.async_add_listener(handle_coordinator_update))
class JellyfinRemote(JellyfinClientEntity, RemoteEntity):
"""Defines a Jellyfin remote entity."""
def __init__(
self,
coordinator: JellyfinDataUpdateCoordinator,
session_id: str,
) -> None:
"""Initialize the Jellyfin Remote entity."""
super().__init__(coordinator, session_id)
self._attr_unique_id = f"{coordinator.server_id}-{session_id}"
@property
def is_on(self) -> bool:
"""Return if the client is on."""
return self.session_data["IsActive"] if self.session_data else False
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to the client."""
num_repeats = kwargs.get(ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS)
delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
for _ in range(num_repeats):
for single_command in command:
self.coordinator.api_client.jellyfin.command(
self.session_id, single_command
)
time.sleep(delay)

View File

@ -0,0 +1,93 @@
"""Tests for the Jellyfin remote platform."""
from unittest.mock import MagicMock
from homeassistant.components.remote import (
ATTR_COMMAND,
ATTR_DELAY_SECS,
ATTR_HOLD_SECS,
ATTR_NUM_REPEATS,
DOMAIN as R_DOMAIN,
SERVICE_SEND_COMMAND,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
async def test_remote(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
mock_jellyfin: MagicMock,
mock_api: MagicMock,
) -> None:
"""Test the Jellyfin remote."""
state = hass.states.get("remote.jellyfin_device")
state2 = hass.states.get("remote.jellyfin_device_two")
state3 = hass.states.get("remote.jellyfin_device_three")
state4 = hass.states.get("remote.jellyfin_device_four")
assert state
assert state2
# Doesn't support remote control; remote not created
assert state3 is None
assert state4
assert state.state == STATE_ON
async def test_services(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_jellyfin: MagicMock,
mock_api: MagicMock,
) -> None:
"""Test Jellyfin remote services."""
state = hass.states.get("remote.jellyfin_device")
assert state
command = "Select"
await hass.services.async_call(
R_DOMAIN,
SERVICE_SEND_COMMAND,
{
ATTR_ENTITY_ID: state.entity_id,
ATTR_COMMAND: command,
ATTR_NUM_REPEATS: 1,
ATTR_DELAY_SECS: 0,
ATTR_HOLD_SECS: 0,
},
blocking=True,
)
assert len(mock_api.command.mock_calls) == 1
assert mock_api.command.mock_calls[0].args == (
"SESSION-UUID",
command,
)
command = "MoveLeft"
await hass.services.async_call(
R_DOMAIN,
SERVICE_SEND_COMMAND,
{
ATTR_ENTITY_ID: state.entity_id,
ATTR_COMMAND: command,
ATTR_NUM_REPEATS: 2,
ATTR_DELAY_SECS: 0,
ATTR_HOLD_SECS: 0,
},
blocking=True,
)
assert len(mock_api.command.mock_calls) == 3
assert mock_api.command.mock_calls[1].args == (
"SESSION-UUID",
command,
)
assert mock_api.command.mock_calls[2].args == (
"SESSION-UUID",
command,
)