1 """Support for Openhome Devices."""
3 from __future__
import annotations
5 from collections.abc
import Awaitable, Callable, Coroutine
8 from typing
import Any, Concatenate
11 from async_upnp_client.client
import UpnpError
12 import voluptuous
as vol
18 MediaPlayerEntityFeature,
21 async_process_play_media_url,
29 from .const
import ATTR_PIN_INDEX, DOMAIN, SERVICE_INVOKE_PIN
32 MediaPlayerEntityFeature.SELECT_SOURCE
33 | MediaPlayerEntityFeature.TURN_OFF
34 | MediaPlayerEntityFeature.TURN_ON
37 _LOGGER = logging.getLogger(__name__)
42 config_entry: ConfigEntry,
43 async_add_entities: AddEntitiesCallback,
45 """Set up the Openhome config entry."""
47 _LOGGER.debug(
"Setting up config entry: %s", config_entry.unique_id)
49 device = hass.data[DOMAIN][config_entry.entry_id]
55 platform = entity_platform.async_get_current_platform()
57 platform.async_register_entity_service(
59 {vol.Required(ATTR_PIN_INDEX): cv.positive_int},
64 type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
65 type _ReturnFuncType[_T, **_P, _R] = Callable[
66 Concatenate[_T, _P], Coroutine[Any, Any, _R |
None]
70 def catch_request_errors[_OpenhomeDeviceT: OpenhomeDevice, **_P, _R]() -> (
72 [_FuncType[_OpenhomeDeviceT, _P, _R]], _ReturnFuncType[_OpenhomeDeviceT, _P, _R]
75 """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
78 func: _FuncType[_OpenhomeDeviceT, _P, _R],
79 ) -> _ReturnFuncType[_OpenhomeDeviceT, _P, _R]:
80 """Call wrapper for decorator."""
82 @functools.wraps(func)
84 self: _OpenhomeDeviceT, *args: _P.args, **kwargs: _P.kwargs
86 """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
88 return await func(self, *args, **kwargs)
89 except (TimeoutError, aiohttp.ClientError, UpnpError):
90 _LOGGER.error(
"Error during call %s", func.__name__)
99 """Representation of an Openhome device."""
101 _attr_supported_features = SUPPORT_OPENHOME
102 _attr_state = MediaPlayerState.PLAYING
103 _attr_available =
True
106 """Initialise the Openhome device."""
113 (DOMAIN, device.uuid()),
115 manufacturer=device.manufacturer(),
116 model=device.model_name(),
117 name=device.friendly_name(),
121 """Update state of device."""
128 track_information = await self.
_device_device.track_info()
132 if artists := track_information.get(
"artist"):
135 if self.
_device_device.volume_enabled:
137 MediaPlayerEntityFeature.VOLUME_STEP
138 | MediaPlayerEntityFeature.VOLUME_MUTE
139 | MediaPlayerEntityFeature.VOLUME_SET
144 for source
in await self.
_device_device.sources():
145 source_names.append(source[
"name"])
146 source_index[source[
"name"]] = source[
"index"]
153 if source[
"type"]
in (
"Radio",
"Receiver"):
155 MediaPlayerEntityFeature.STOP
156 | MediaPlayerEntityFeature.PLAY
157 | MediaPlayerEntityFeature.PLAY_MEDIA
158 | MediaPlayerEntityFeature.BROWSE_MEDIA
160 if source[
"type"]
in (
"Playlist",
"Spotify"):
162 MediaPlayerEntityFeature.PREVIOUS_TRACK
163 | MediaPlayerEntityFeature.NEXT_TRACK
164 | MediaPlayerEntityFeature.PAUSE
165 | MediaPlayerEntityFeature.PLAY
166 | MediaPlayerEntityFeature.PLAY_MEDIA
167 | MediaPlayerEntityFeature.BROWSE_MEDIA
170 in_standby = await self.
_device_device.is_in_standby()
171 transport_state = await self.
_device_device.transport_state()
174 elif transport_state ==
"Paused":
175 self.
_attr_state_attr_state = MediaPlayerState.PAUSED
176 elif transport_state
in (
"Playing",
"Buffering"):
177 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
178 elif transport_state ==
"Stopped":
179 self.
_attr_state_attr_state = MediaPlayerState.IDLE
182 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
185 except (TimeoutError, aiohttp.ClientError, UpnpError):
188 @catch_request_errors()
190 """Bring device out of standby."""
191 await self.
_device_device.set_standby(
False)
193 @catch_request_errors()
195 """Put device in standby."""
196 await self.
_device_device.set_standby(
True)
198 @catch_request_errors()
200 self, media_type: MediaType | str, media_id: str, **kwargs: Any
202 """Send the play_media command to the media player."""
203 if media_source.is_media_source_id(media_id):
204 media_type = MediaType.MUSIC
205 play_item = await media_source.async_resolve_media(
208 media_id = play_item.url
210 if media_type != MediaType.MUSIC:
212 "Invalid media type %s. Only %s is supported",
220 track_details = {
"title":
"Home Assistant",
"uri": media_id}
223 @catch_request_errors()
225 """Send pause command."""
226 await self.
_device_device.pause()
228 @catch_request_errors()
230 """Send stop command."""
231 await self.
_device_device.stop()
233 @catch_request_errors()
235 """Send play command."""
236 await self.
_device_device.play()
238 @catch_request_errors()
240 """Send next track command."""
241 await self.
_device_device.skip(1)
243 @catch_request_errors()
245 """Send previous track command."""
246 await self.
_device_device.skip(-1)
248 @catch_request_errors()
250 """Select input source."""
253 @catch_request_errors()
257 if self.
_device_device.pins_enabled:
258 await self.
_device_device.invoke_pin(pin)
260 _LOGGER.error(
"Pins service not supported")
262 _LOGGER.error(
"Error invoking pin %s", pin)
264 @catch_request_errors()
266 """Volume up media player."""
267 await self.
_device_device.increase_volume()
269 @catch_request_errors()
271 """Volume down media player."""
272 await self.
_device_device.decrease_volume()
274 @catch_request_errors()
276 """Set volume level, range 0..1."""
277 await self.
_device_device.set_volume(
int(volume * 100))
279 @catch_request_errors()
281 """Mute (true) or unmute (false) media player."""
282 await self.
_device_device.set_mute(mute)
286 media_content_type: MediaType | str |
None =
None,
287 media_content_id: str |
None =
None,
289 """Implement the websocket media browsing helper."""
290 return await media_source.async_browse_media(
293 content_filter=
lambda item: item.media_content_type.startswith(
"audio/"),