# -*- coding: utf-8 -*- """ Support for DLNA DMR (Device Media Renderer). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.dlna_dmr/ """ import asyncio import functools import logging from datetime import datetime import aiohttp import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_URL, CONF_NAME, STATE_OFF, STATE_ON, STATE_IDLE, STATE_PLAYING, STATE_PAUSED) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import get_local_ip DLNA_DMR_DATA = 'dlna_dmr' REQUIREMENTS = [ 'async-upnp-client==0.12.3', ] DEFAULT_NAME = 'DLNA Digital Media Renderer' DEFAULT_LISTEN_PORT = 8301 CONF_LISTEN_IP = 'listen_ip' CONF_LISTEN_PORT = 'listen_port' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LISTEN_IP): cv.string, vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) HOME_ASSISTANT_UPNP_CLASS_MAPPING = { 'music': 'object.item.audioItem', 'tvshow': 'object.item.videoItem', 'video': 'object.item.videoItem', 'episode': 'object.item.videoItem', 'channel': 'object.item.videoItem', 'playlist': 'object.item.playlist', } HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { 'music': 'audio/*', 'tvshow': 'video/*', 'video': 'video/*', 'episode': 'video/*', 'channel': 'video/*', 'playlist': 'playlist/*', } _LOGGER = logging.getLogger(__name__) def catch_request_errors(): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" def call_wrapper(func): """Call wrapper for decorator.""" @functools.wraps(func) def wrapper(self, *args, **kwargs): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" try: return func(self, *args, **kwargs) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error during call %s", func.__name__) return wrapper return call_wrapper async def async_start_event_handler(hass, server_host, server_port, requester): """Register notify view.""" hass_data = hass.data[DLNA_DMR_DATA] if 'event_handler' in hass_data: return hass_data['event_handler'] # start event handler from async_upnp_client.aiohttp import AiohttpNotifyServer server = AiohttpNotifyServer(requester, server_port, server_host, hass.loop) await server.start_server() _LOGGER.info('UPNP/DLNA event handler listening on: %s', server.callback_url) hass_data['notify_server'] = server hass_data['event_handler'] = server.event_handler # register for graceful shutdown async def async_stop_server(event): """Stop server.""" _LOGGER.debug('Stopping UPNP/DLNA event handler') await server.stop_server() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_server) return hass_data['event_handler'] async def async_setup_platform(hass: HomeAssistant, config, async_add_devices, discovery_info=None): """Set up DLNA DMR platform.""" if config.get(CONF_URL) is not None: url = config[CONF_URL] name = config.get(CONF_NAME) elif discovery_info is not None: url = discovery_info['ssdp_description'] name = discovery_info.get('name') if DLNA_DMR_DATA not in hass.data: hass.data[DLNA_DMR_DATA] = {} if 'lock' not in hass.data[DLNA_DMR_DATA]: hass.data[DLNA_DMR_DATA]['lock'] = asyncio.Lock() # build upnp/aiohttp requester from async_upnp_client.aiohttp import AiohttpSessionRequester session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) # ensure event handler has been started with await hass.data[DLNA_DMR_DATA]['lock']: server_host = config.get(CONF_LISTEN_IP) if server_host is None: server_host = get_local_ip() server_port = config.get(CONF_LISTEN_PORT, DEFAULT_LISTEN_PORT) event_handler = await async_start_event_handler(hass, server_host, server_port, requester) # create upnp device from async_upnp_client import UpnpFactory factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) except (asyncio.TimeoutError, aiohttp.ClientError): raise PlatformNotReady() # wrap with DmrDevice from async_upnp_client.dlna import DmrDevice dlna_device = DmrDevice(upnp_device, event_handler) # create our own device device = DlnaDmrDevice(dlna_device, name) _LOGGER.debug("Adding device: %s", device) async_add_devices([device], True) class DlnaDmrDevice(MediaPlayerDevice): """Representation of a DLNA DMR device.""" def __init__(self, dmr_device, name=None): """Initializer.""" self._device = dmr_device self._name = name self._available = False self._subscription_renew_time = None async def async_added_to_hass(self): """Callback when added.""" self._device.on_event = self._on_event # register unsubscribe on stop bus = self.hass.bus bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_on_hass_stop) @property def available(self): """Device is available.""" return self._available async def _async_on_hass_stop(self, event): """Event handler on HASS stop.""" with await self.hass.data[DLNA_DMR_DATA]['lock']: await self._device.async_unsubscribe_services() async def async_update(self): """Retrieve the latest data.""" was_available = self._available try: await self._device.async_update() self._available = True except (asyncio.TimeoutError, aiohttp.ClientError): self._available = False _LOGGER.debug("Device unavailable") return # do we need to (re-)subscribe? now = datetime.now() should_renew = self._subscription_renew_time and \ now >= self._subscription_renew_time if should_renew or \ not was_available and self._available: try: timeout = await self._device.async_subscribe_services() self._subscription_renew_time = datetime.now() + timeout / 2 except (asyncio.TimeoutError, aiohttp.ClientError): self._available = False _LOGGER.debug("Could not (re)subscribe") def _on_event(self, service, state_variables): """State variable(s) changed, let home-assistant know.""" self.schedule_update_ha_state() @property def supported_features(self): """Flag media player features that are supported.""" supported_features = 0 if self._device.has_volume_level: supported_features |= SUPPORT_VOLUME_SET if self._device.has_volume_mute: supported_features |= SUPPORT_VOLUME_MUTE if self._device.has_play: supported_features |= SUPPORT_PLAY if self._device.has_pause: supported_features |= SUPPORT_PAUSE if self._device.has_stop: supported_features |= SUPPORT_STOP if self._device.has_previous: supported_features |= SUPPORT_PREVIOUS_TRACK if self._device.has_next: supported_features |= SUPPORT_NEXT_TRACK if self._device.has_play_media: supported_features |= SUPPORT_PLAY_MEDIA return supported_features @property def volume_level(self): """Volume level of the media player (0..1).""" return self._device.volume_level @catch_request_errors() async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" await self._device.async_set_volume_level(volume) @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._device.is_volume_muted @catch_request_errors() async def async_mute_volume(self, mute): """Mute the volume.""" desired_mute = bool(mute) await self._device.async_mute_volume(desired_mute) @catch_request_errors() async def async_media_pause(self): """Send pause command.""" if not self._device.can_pause: _LOGGER.debug('Cannot do Pause') return await self._device.async_pause() @catch_request_errors() async def async_media_play(self): """Send play command.""" if not self._device.can_play: _LOGGER.debug('Cannot do Play') return await self._device.async_play() @catch_request_errors() async def async_media_stop(self): """Send stop command.""" if not self._device.can_stop: _LOGGER.debug('Cannot do Stop') return await self._device.async_stop() @catch_request_errors() async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" title = "Home Assistant" mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING[media_type] upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING[media_type] # stop current playing media if self._device.can_stop: await self.async_media_stop() # queue media await self._device.async_set_transport_uri(media_id, title, mime_type, upnp_class) await self._device.async_wait_for_can_play() # if already playing, no need to call Play from async_upnp_client import dlna if self._device.state == dlna.STATE_PLAYING: return # play it await self.async_media_play() @catch_request_errors() async def async_media_previous_track(self): """Send previous track command.""" if not self._device.can_previous: _LOGGER.debug('Cannot do Previous') return await self._device.async_previous() @catch_request_errors() async def async_media_next_track(self): """Send next track command.""" if not self._device.can_next: _LOGGER.debug('Cannot do Next') return await self._device.async_next() @property def media_title(self): """Title of current playing media.""" return self._device.media_title @property def media_image_url(self): """Image url of current playing media.""" return self._device.media_image_url @property def state(self): """State of the player.""" if not self._available: return STATE_OFF from async_upnp_client import dlna if self._device.state is None: return STATE_ON if self._device.state == dlna.STATE_PLAYING: return STATE_PLAYING if self._device.state == dlna.STATE_PAUSED: return STATE_PAUSED return STATE_IDLE @property def media_duration(self): """Duration of current playing media in seconds.""" return self._device.media_duration @property def media_position(self): """Position of current playing media in seconds.""" return self._device.media_position @property def media_position_updated_at(self): """When was the position of the current playing media valid. Returns value from homeassistant.util.dt.utcnow(). """ return self._device.media_position_updated_at @property def name(self) -> str: """Return the name of the device.""" if self._name: return self._name return self._device.name @property def unique_id(self) -> str: """Return an unique ID.""" return self._device.udn