From 5ac81ccb4cd69c21c8fa84bb0568255de88d1e34 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Wed, 26 May 2021 17:39:44 +0100 Subject: [PATCH] Openhome component now uses asyncio and handles unavailability (#49574) --- .../components/openhome/manifest.json | 2 +- .../components/openhome/media_player.py | 205 +++++++++++------- requirements_all.txt | 2 +- 3 files changed, 133 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index f45d6d31cef..c83b135cb8a 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,7 +2,7 @@ "domain": "openhome", "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": ["openhomedevice==0.7.2"], + "requirements": ["openhomedevice==2.0.1"], "codeowners": ["@bazwilliams"], "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 7e333f7432b..26393dad179 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,7 +1,11 @@ """Support for Openhome Devices.""" +import asyncio +import functools import logging -from openhomedevice.Device import Device +import aiohttp +from async_upnp_client.client import UpnpError +from openhomedevice.device import Device import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -43,25 +47,45 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.info("Openhome device found: %s", name) device = await hass.async_add_executor_job(Device, description) + await device.init() # if device has already been discovered - if device.Uuid() in openhome_data: + if device.uuid() in openhome_data: return True entity = OpenhomeDevice(hass, device) async_add_entities([entity]) - openhome_data.add(device.Uuid()) + openhome_data.add(device.uuid()) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_INVOKE_PIN, {vol.Required(ATTR_PIN_INDEX): cv.positive_int}, - "invoke_pin", + "async_invoke_pin", ) +def catch_request_errors(): + """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + + def call_wrapper(func): + """Call wrapper for decorator.""" + + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" + try: + return await func(self, *args, **kwargs) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error("Error during call %s", func.__name__) + + return wrapper + + return call_wrapper + + class OpenhomeDevice(MediaPlayerEntity): """Representation of an Openhome device.""" @@ -80,64 +104,80 @@ class OpenhomeDevice(MediaPlayerEntity): self._source = {} self._name = None self._state = STATE_PLAYING + self._available = True - def update(self): + @property + def available(self): + """Device is available.""" + return self._available + + async def async_update(self): """Update state of device.""" - self._in_standby = self._device.IsInStandby() - self._transport_state = self._device.TransportState() - self._track_information = self._device.TrackInfo() - self._source = self._device.Source() - self._name = self._device.Room().decode("utf-8") - self._supported_features = SUPPORT_OPENHOME - source_index = {} - source_names = [] + try: + self._in_standby = await self._device.is_in_standby() + self._transport_state = await self._device.transport_state() + self._track_information = await self._device.track_info() + self._source = await self._device.source() + self._name = await self._device.room() + self._supported_features = SUPPORT_OPENHOME + source_index = {} + source_names = [] - if self._device.VolumeEnabled(): - self._supported_features |= ( - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET - ) - self._volume_level = self._device.VolumeLevel() / 100.0 - self._volume_muted = self._device.IsMuted() + if self._device.volume_enabled: + self._supported_features |= ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) + self._volume_level = await self._device.volume() / 100.0 + self._volume_muted = await self._device.is_muted() - for source in self._device.Sources(): - source_names.append(source["name"]) - source_index[source["name"]] = source["index"] + for source in await self._device.sources(): + source_names.append(source["name"]) + source_index[source["name"]] = source["index"] - self._source_index = source_index - self._source_names = source_names + self._source_index = source_index + self._source_names = source_names - if self._source["type"] == "Radio": - self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA - if self._source["type"] in ("Playlist", "Spotify"): - self._supported_features |= ( - SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_PAUSE - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - ) + if self._source["type"] == "Radio": + self._supported_features |= ( + SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + ) + if self._source["type"] in ("Playlist", "Spotify"): + self._supported_features |= ( + SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + ) - if self._in_standby: - self._state = STATE_OFF - elif self._transport_state == "Paused": - self._state = STATE_PAUSED - elif self._transport_state in ("Playing", "Buffering"): - self._state = STATE_PLAYING - elif self._transport_state == "Stopped": - self._state = STATE_IDLE - else: - # Device is playing an external source with no transport controls - self._state = STATE_PLAYING + if self._in_standby: + self._state = STATE_OFF + elif self._transport_state == "Paused": + self._state = STATE_PAUSED + elif self._transport_state in ("Playing", "Buffering"): + self._state = STATE_PLAYING + elif self._transport_state == "Stopped": + self._state = STATE_IDLE + else: + # Device is playing an external source with no transport controls + self._state = STATE_PLAYING - def turn_on(self): + self._available = True + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + self._available = False + + @catch_request_errors() + async def async_turn_on(self): """Bring device out of standby.""" - self._device.SetStandby(False) + await self._device.set_standby(False) - def turn_off(self): + @catch_request_errors() + async def async_turn_off(self): """Put device in standby.""" - self._device.SetStandby(True) + await self._device.set_standby(True) - def play_media(self, media_type, media_id, **kwargs): + @catch_request_errors() + async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( @@ -147,35 +187,48 @@ class OpenhomeDevice(MediaPlayerEntity): ) return track_details = {"title": "Home Assistant", "uri": media_id} - self._device.PlayMedia(track_details) + await self._device.play_media(track_details) - def media_pause(self): + @catch_request_errors() + async def async_media_pause(self): """Send pause command.""" - self._device.Pause() + await self._device.pause() - def media_stop(self): + @catch_request_errors() + async def async_media_stop(self): """Send stop command.""" - self._device.Stop() + await self._device.stop() - def media_play(self): + @catch_request_errors() + async def async_media_play(self): """Send play command.""" - self._device.Play() + await self._device.play() - def media_next_track(self): + @catch_request_errors() + async def async_media_next_track(self): """Send next track command.""" - self._device.Skip(1) + await self._device.skip(1) - def media_previous_track(self): + @catch_request_errors() + async def async_media_previous_track(self): """Send previous track command.""" - self._device.Skip(-1) + await self._device.skip(-1) - def select_source(self, source): + @catch_request_errors() + async def async_select_source(self, source): """Select input source.""" - self._device.SetSource(self._source_index[source]) + await self._device.set_source(self._source_index[source]) - def invoke_pin(self, pin): + @catch_request_errors() + async def async_invoke_pin(self, pin): """Invoke pin.""" - self._device.InvokePin(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) @property def name(self): @@ -190,7 +243,7 @@ class OpenhomeDevice(MediaPlayerEntity): @property def unique_id(self): """Return a unique ID.""" - return self._device.Uuid() + return self._device.uuid() @property def state(self): @@ -239,18 +292,22 @@ class OpenhomeDevice(MediaPlayerEntity): """Return true if volume is muted.""" return self._volume_muted - def volume_up(self): + @catch_request_errors() + async def async_volume_up(self): """Volume up media player.""" - self._device.IncreaseVolume() + await self._device.increase_volume() - def volume_down(self): + @catch_request_errors() + async def async_volume_down(self): """Volume down media player.""" - self._device.DecreaseVolume() + await self._device.decrease_volume() - def set_volume_level(self, volume): + @catch_request_errors() + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._device.SetVolumeLevel(int(volume * 100)) + await self._device.set_volume(int(volume * 100)) - def mute_volume(self, mute): + @catch_request_errors() + async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - self._device.SetMute(mute) + await self._device.set_mute(mute) diff --git a/requirements_all.txt b/requirements_all.txt index fb30eb5bd68..98774e9a9ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1076,7 +1076,7 @@ openerz-api==0.1.0 openevsewifi==1.1.0 # homeassistant.components.openhome -openhomedevice==0.7.2 +openhomedevice==2.0.1 # homeassistant.components.opensensemap opensensemap-api==0.1.5