299 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
"""Support for Openhome Devices."""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
from collections.abc import Awaitable, Callable, Coroutine
 | 
						|
import functools
 | 
						|
import logging
 | 
						|
from typing import Any, Concatenate, ParamSpec, TypeVar
 | 
						|
 | 
						|
import aiohttp
 | 
						|
from async_upnp_client.client import UpnpError
 | 
						|
import voluptuous as vol
 | 
						|
 | 
						|
from homeassistant.components import media_source
 | 
						|
from homeassistant.components.media_player import (
 | 
						|
    BrowseMedia,
 | 
						|
    MediaPlayerEntity,
 | 
						|
    MediaPlayerEntityFeature,
 | 
						|
    MediaPlayerState,
 | 
						|
    MediaType,
 | 
						|
    async_process_play_media_url,
 | 
						|
)
 | 
						|
from homeassistant.config_entries import ConfigEntry
 | 
						|
from homeassistant.core import HomeAssistant
 | 
						|
from homeassistant.helpers import config_validation as cv, entity_platform
 | 
						|
from homeassistant.helpers.device_registry import DeviceInfo
 | 
						|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
 | 
						|
 | 
						|
from .const import ATTR_PIN_INDEX, DOMAIN, SERVICE_INVOKE_PIN
 | 
						|
 | 
						|
_OpenhomeDeviceT = TypeVar("_OpenhomeDeviceT", bound="OpenhomeDevice")
 | 
						|
_R = TypeVar("_R")
 | 
						|
_P = ParamSpec("_P")
 | 
						|
 | 
						|
SUPPORT_OPENHOME = (
 | 
						|
    MediaPlayerEntityFeature.SELECT_SOURCE
 | 
						|
    | MediaPlayerEntityFeature.TURN_OFF
 | 
						|
    | MediaPlayerEntityFeature.TURN_ON
 | 
						|
)
 | 
						|
 | 
						|
_LOGGER = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
async def async_setup_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    config_entry: ConfigEntry,
 | 
						|
    async_add_entities: AddEntitiesCallback,
 | 
						|
) -> None:
 | 
						|
    """Set up the Openhome config entry."""
 | 
						|
 | 
						|
    _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
 | 
						|
 | 
						|
    device = hass.data[DOMAIN][config_entry.entry_id]
 | 
						|
 | 
						|
    entity = OpenhomeDevice(hass, device)
 | 
						|
 | 
						|
    async_add_entities([entity])
 | 
						|
 | 
						|
    platform = entity_platform.async_get_current_platform()
 | 
						|
 | 
						|
    platform.async_register_entity_service(
 | 
						|
        SERVICE_INVOKE_PIN,
 | 
						|
        {vol.Required(ATTR_PIN_INDEX): cv.positive_int},
 | 
						|
        "async_invoke_pin",
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
_FuncType = Callable[Concatenate[_OpenhomeDeviceT, _P], Awaitable[_R]]
 | 
						|
_ReturnFuncType = Callable[
 | 
						|
    Concatenate[_OpenhomeDeviceT, _P], Coroutine[Any, Any, _R | None]
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
def catch_request_errors() -> (
 | 
						|
    Callable[
 | 
						|
        [_FuncType[_OpenhomeDeviceT, _P, _R]], _ReturnFuncType[_OpenhomeDeviceT, _P, _R]
 | 
						|
    ]
 | 
						|
):
 | 
						|
    """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
 | 
						|
 | 
						|
    def call_wrapper(
 | 
						|
        func: _FuncType[_OpenhomeDeviceT, _P, _R],
 | 
						|
    ) -> _ReturnFuncType[_OpenhomeDeviceT, _P, _R]:
 | 
						|
        """Call wrapper for decorator."""
 | 
						|
 | 
						|
        @functools.wraps(func)
 | 
						|
        async def wrapper(
 | 
						|
            self: _OpenhomeDeviceT, *args: _P.args, **kwargs: _P.kwargs
 | 
						|
        ) -> _R | None:
 | 
						|
            """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
 | 
						|
            try:
 | 
						|
                return await func(self, *args, **kwargs)
 | 
						|
            except (TimeoutError, aiohttp.ClientError, UpnpError):
 | 
						|
                _LOGGER.error("Error during call %s", func.__name__)
 | 
						|
            return None
 | 
						|
 | 
						|
        return wrapper
 | 
						|
 | 
						|
    return call_wrapper
 | 
						|
 | 
						|
 | 
						|
class OpenhomeDevice(MediaPlayerEntity):
 | 
						|
    """Representation of an Openhome device."""
 | 
						|
 | 
						|
    _attr_supported_features = SUPPORT_OPENHOME
 | 
						|
    _attr_state = MediaPlayerState.PLAYING
 | 
						|
    _attr_available = True
 | 
						|
 | 
						|
    def __init__(self, hass, device):
 | 
						|
        """Initialise the Openhome device."""
 | 
						|
        self.hass = hass
 | 
						|
        self._device = device
 | 
						|
        self._attr_unique_id = device.uuid()
 | 
						|
        self._source_index = {}
 | 
						|
        self._attr_device_info = DeviceInfo(
 | 
						|
            identifiers={
 | 
						|
                (DOMAIN, device.uuid()),
 | 
						|
            },
 | 
						|
            manufacturer=device.manufacturer(),
 | 
						|
            model=device.model_name(),
 | 
						|
            name=device.friendly_name(),
 | 
						|
        )
 | 
						|
 | 
						|
    async def async_update(self) -> None:
 | 
						|
        """Update state of device."""
 | 
						|
        try:
 | 
						|
            self._attr_name = await self._device.room()
 | 
						|
            self._attr_supported_features = SUPPORT_OPENHOME
 | 
						|
            source_index = {}
 | 
						|
            source_names = []
 | 
						|
 | 
						|
            track_information = await self._device.track_info()
 | 
						|
            self._attr_media_image_url = track_information.get("albumArtwork")
 | 
						|
            self._attr_media_album_name = track_information.get("albumTitle")
 | 
						|
            self._attr_media_title = track_information.get("title")
 | 
						|
            if artists := track_information.get("artist"):
 | 
						|
                self._attr_media_artist = artists[0]
 | 
						|
 | 
						|
            if self._device.volume_enabled:
 | 
						|
                self._attr_supported_features |= (
 | 
						|
                    MediaPlayerEntityFeature.VOLUME_STEP
 | 
						|
                    | MediaPlayerEntityFeature.VOLUME_MUTE
 | 
						|
                    | MediaPlayerEntityFeature.VOLUME_SET
 | 
						|
                )
 | 
						|
                self._attr_volume_level = await self._device.volume() / 100.0
 | 
						|
                self._attr_is_volume_muted = await self._device.is_muted()
 | 
						|
 | 
						|
            for source in await self._device.sources():
 | 
						|
                source_names.append(source["name"])
 | 
						|
                source_index[source["name"]] = source["index"]
 | 
						|
 | 
						|
            source = await self._device.source()
 | 
						|
            self._attr_source = source.get("name")
 | 
						|
            self._source_index = source_index
 | 
						|
            self._attr_source_list = source_names
 | 
						|
 | 
						|
            if source["type"] in ("Radio", "Receiver"):
 | 
						|
                self._attr_supported_features |= (
 | 
						|
                    MediaPlayerEntityFeature.STOP
 | 
						|
                    | MediaPlayerEntityFeature.PLAY
 | 
						|
                    | MediaPlayerEntityFeature.PLAY_MEDIA
 | 
						|
                    | MediaPlayerEntityFeature.BROWSE_MEDIA
 | 
						|
                )
 | 
						|
            if source["type"] in ("Playlist", "Spotify"):
 | 
						|
                self._attr_supported_features |= (
 | 
						|
                    MediaPlayerEntityFeature.PREVIOUS_TRACK
 | 
						|
                    | MediaPlayerEntityFeature.NEXT_TRACK
 | 
						|
                    | MediaPlayerEntityFeature.PAUSE
 | 
						|
                    | MediaPlayerEntityFeature.PLAY
 | 
						|
                    | MediaPlayerEntityFeature.PLAY_MEDIA
 | 
						|
                    | MediaPlayerEntityFeature.BROWSE_MEDIA
 | 
						|
                )
 | 
						|
 | 
						|
            in_standby = await self._device.is_in_standby()
 | 
						|
            transport_state = await self._device.transport_state()
 | 
						|
            if in_standby:
 | 
						|
                self._attr_state = MediaPlayerState.OFF
 | 
						|
            elif transport_state == "Paused":
 | 
						|
                self._attr_state = MediaPlayerState.PAUSED
 | 
						|
            elif transport_state in ("Playing", "Buffering"):
 | 
						|
                self._attr_state = MediaPlayerState.PLAYING
 | 
						|
            elif transport_state == "Stopped":
 | 
						|
                self._attr_state = MediaPlayerState.IDLE
 | 
						|
            else:
 | 
						|
                # Device is playing an external source with no transport controls
 | 
						|
                self._attr_state = MediaPlayerState.PLAYING
 | 
						|
 | 
						|
            self._attr_available = True
 | 
						|
        except (TimeoutError, aiohttp.ClientError, UpnpError):
 | 
						|
            self._attr_available = False
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_turn_on(self) -> None:
 | 
						|
        """Bring device out of standby."""
 | 
						|
        await self._device.set_standby(False)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_turn_off(self) -> None:
 | 
						|
        """Put device in standby."""
 | 
						|
        await self._device.set_standby(True)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_play_media(
 | 
						|
        self, media_type: MediaType | str, media_id: str, **kwargs: Any
 | 
						|
    ) -> None:
 | 
						|
        """Send the play_media command to the media player."""
 | 
						|
        if media_source.is_media_source_id(media_id):
 | 
						|
            media_type = MediaType.MUSIC
 | 
						|
            play_item = await media_source.async_resolve_media(
 | 
						|
                self.hass, media_id, self.entity_id
 | 
						|
            )
 | 
						|
            media_id = play_item.url
 | 
						|
 | 
						|
        if media_type != MediaType.MUSIC:
 | 
						|
            _LOGGER.error(
 | 
						|
                "Invalid media type %s. Only %s is supported",
 | 
						|
                media_type,
 | 
						|
                MediaType.MUSIC,
 | 
						|
            )
 | 
						|
            return
 | 
						|
 | 
						|
        media_id = async_process_play_media_url(self.hass, media_id)
 | 
						|
 | 
						|
        track_details = {"title": "Home Assistant", "uri": media_id}
 | 
						|
        await self._device.play_media(track_details)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_media_pause(self) -> None:
 | 
						|
        """Send pause command."""
 | 
						|
        await self._device.pause()
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_media_stop(self) -> None:
 | 
						|
        """Send stop command."""
 | 
						|
        await self._device.stop()
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_media_play(self) -> None:
 | 
						|
        """Send play command."""
 | 
						|
        await self._device.play()
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_media_next_track(self) -> None:
 | 
						|
        """Send next track command."""
 | 
						|
        await self._device.skip(1)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_media_previous_track(self) -> None:
 | 
						|
        """Send previous track command."""
 | 
						|
        await self._device.skip(-1)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_select_source(self, source: str) -> None:
 | 
						|
        """Select input source."""
 | 
						|
        await self._device.set_source(self._source_index[source])
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_invoke_pin(self, pin):
 | 
						|
        """Invoke pin."""
 | 
						|
        try:
 | 
						|
            if self._device.pins_enabled:
 | 
						|
                await self._device.invoke_pin(pin)
 | 
						|
            else:
 | 
						|
                _LOGGER.error("Pins service not supported")
 | 
						|
        except UpnpError:
 | 
						|
            _LOGGER.error("Error invoking pin %s", pin)
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_volume_up(self) -> None:
 | 
						|
        """Volume up media player."""
 | 
						|
        await self._device.increase_volume()
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_volume_down(self) -> None:
 | 
						|
        """Volume down media player."""
 | 
						|
        await self._device.decrease_volume()
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_set_volume_level(self, volume: float) -> None:
 | 
						|
        """Set volume level, range 0..1."""
 | 
						|
        await self._device.set_volume(int(volume * 100))
 | 
						|
 | 
						|
    @catch_request_errors()
 | 
						|
    async def async_mute_volume(self, mute: bool) -> None:
 | 
						|
        """Mute (true) or unmute (false) media player."""
 | 
						|
        await self._device.set_mute(mute)
 | 
						|
 | 
						|
    async def async_browse_media(
 | 
						|
        self,
 | 
						|
        media_content_type: MediaType | str | None = None,
 | 
						|
        media_content_id: str | None = None,
 | 
						|
    ) -> BrowseMedia:
 | 
						|
        """Implement the websocket media browsing helper."""
 | 
						|
        return await media_source.async_browse_media(
 | 
						|
            self.hass,
 | 
						|
            media_content_id,
 | 
						|
            content_filter=lambda item: item.media_content_type.startswith("audio/"),
 | 
						|
        )
 |