From 8922547e8f98a1a180e1499aad197af424d54aae Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 14:26:26 +0100 Subject: [PATCH 1/6] Fix blocking HA process using asyncio Also stop/play media on volume change so it is applied --- custom_components/sox/media_player.py | 75 +++++++++++++++------------ 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index c27595d..b37de79 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -1,6 +1,7 @@ """Support for interacting with the SoX music player.""" + import logging -import socket +import asyncio import homeassistant.helpers.config_validation as cv import voluptuous as vol @@ -114,28 +115,31 @@ def supported_features(self): return supported - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Mute. Emulated with set_volume_level.""" if self.volume_level is not None and mute != self._muted: if mute: self._muted_volume = self.volume_level - self.set_volume_level(0) + await self.async_set_volume_level(0) elif self._muted_volume is not None: - self.set_volume_level(self._muted_volume) + await self.async_set_volume_level(self._muted_volume) self._muted = mute - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume of media player.""" self._volume = round(volume, 2) + if self._is_playing: + await self.async_media_stop() + await self.async_media_play() - def media_play(self): + async def async_media_play(self): """Send play command.""" _LOGGER.debug("SoX play: %s", self.hass.data[DOMAIN][self._name]["media_id"]) - self._send(self.hass.data[DOMAIN][self._name]["media_id"]) + await self._async_send(self.hass.data[DOMAIN][self._name]["media_id"]) - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" - self._send("stop") + await self._async_send("stop") async def async_browse_media(self, media_content_type, media_content_id): """Implement the websocket media browsing helper.""" @@ -157,7 +161,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): media_id = async_process_play_media_url(self.hass, play_item.url) if media_type in [MediaType.MUSIC, MediaType.PLAYLIST]: - self._send(media_id) + await self._async_send(media_id) self.hass.data[DOMAIN][self._name]["media_id"] = media_id else: _LOGGER.error( @@ -167,42 +171,47 @@ async def async_play_media(self, media_type, media_id, **kwargs): MediaType.PLAYLIST, ) - def _send(self, media_id): + async def _async_send(self, media_id): try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(5) - sock.connect((self._host, self._port)) - sock.sendall(f"{media_id};{self._volume};".encode()) - output = sock.recv(256).decode("utf-8").rstrip() - self._is_connected = True - if "=" in output and ";" in output: - output_parsed = dict(x.split("=") for x in output.split(";")) # type: ignore - if "volume" in output_parsed.keys(): - self._volume = float(output_parsed["volume"]) - self._is_playing = output_parsed.get("playing") == "True" or False - - except (socket.error, socket.timeout) as err: - _LOGGER.debug("SoX connection error: %s", err) - if self._volume is not None: # For compatibility with old sound server - self._is_connected = False - - def volume_up(self): + reader, writer = await asyncio.open_connection(self._host, self._port) + # Set a timeout for operations + writer.write(f"{media_id};{self._volume};".encode()) + await writer.drain() + # Try to receive data with a timeout + data = await asyncio.wait_for(reader.read(256), timeout=5) + output = data.decode("utf-8").rstrip() + self._is_connected = True + + if "=" in output and ";" in output: + output_parsed = dict(x.split("=") for x in output.split(";")) + if "volume" in output_parsed.keys(): + self._volume = float(output_parsed["volume"]) + self._is_playing = output_parsed.get("playing") == "True" or False + + except (asyncio.TimeoutError, OSError) as err: + _LOGGER.debug("Async SoX connection error: %s", err) + self._is_connected = False + finally: + writer.close() + await writer.wait_closed() + + async def async_volume_up(self): """Service to send the MPD the command for volume up.""" if self.volume_level is not None: current_volume = self.volume_level if current_volume < 1: - self.set_volume_level(min(current_volume + 0.05, 1)) + await self.async_set_volume_level(min(current_volume + 0.05, 1)) - def volume_down(self): + async def async_volume_down(self): """Service to send the MPD the command for volume down.""" if self.volume_level is not None: current_volume = self.volume_level if current_volume > 0: - self.set_volume_level(max(current_volume - 0.05, 0)) + await self.async_set_volume_level(max(current_volume - 0.05, 0)) async def async_update(self): """Get the latest data and update the state.""" if self._is_connected is None or self._volume is not None: - self._send("") # For compatibility with old sound server + await self._async_send("") # For compatibility with old sound server From a28d09b535207628ae27830ee194159d4f31cf28 Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 14:48:17 +0100 Subject: [PATCH 2/6] Update media_player.py Timeout on connection --- custom_components/sox/media_player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index b37de79..b2fff83 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -173,7 +173,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): async def _async_send(self, media_id): try: - reader, writer = await asyncio.open_connection(self._host, self._port) + reader, writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._port), timeout=5) # Set a timeout for operations writer.write(f"{media_id};{self._volume};".encode()) await writer.drain() @@ -192,8 +192,9 @@ async def _async_send(self, media_id): _LOGGER.debug("Async SoX connection error: %s", err) self._is_connected = False finally: - writer.close() - await writer.wait_closed() + if writer: + writer.close() + await writer.wait_closed() async def async_volume_up(self): """Service to send the MPD the command for volume up.""" From 8d2f370cfc2c0ab29739ed9f6124b9d2055fa8d3 Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 15:00:21 +0100 Subject: [PATCH 3/6] Update media_player.py lint --- custom_components/sox/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index b2fff83..677e7f4 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -173,7 +173,9 @@ async def async_play_media(self, media_type, media_id, **kwargs): async def _async_send(self, media_id): try: - reader, writer = await asyncio.wait_for(asyncio.open_connection(self._host, self._port), timeout=5) + reader, writer = await asyncio.wait_for( + asyncio.open_connection(self._host, self._port), timeout=5 + ) # Set a timeout for operations writer.write(f"{media_id};{self._volume};".encode()) await writer.drain() From e5a85795b59620c71297fd17b46af5ab520fa919 Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 15:23:17 +0100 Subject: [PATCH 4/6] Fix undefined variable when not available --- custom_components/sox/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index 677e7f4..4cb4d54 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -172,6 +172,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): ) async def _async_send(self, media_id): + reader, writer = (None, None) try: reader, writer = await asyncio.wait_for( asyncio.open_connection(self._host, self._port), timeout=5 From c1624af11f7ae8a7b8ee9347eb627bb3f72bc403 Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 17:28:21 +0100 Subject: [PATCH 5/6] Update media_player.py fix reconnection --- custom_components/sox/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index 4cb4d54..9c65c2c 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -194,6 +194,7 @@ async def _async_send(self, media_id): except (asyncio.TimeoutError, OSError) as err: _LOGGER.debug("Async SoX connection error: %s", err) self._is_connected = False + raise err finally: if writer: writer.close() @@ -217,5 +218,5 @@ async def async_volume_down(self): async def async_update(self): """Get the latest data and update the state.""" - if self._is_connected is None or self._volume is not None: + if not self._is_connected or self._volume is not None: await self._async_send("") # For compatibility with old sound server From 7e7be4e0c43feca2dba68718de9adc0c8bb984fa Mon Sep 17 00:00:00 2001 From: David Buezas Date: Sat, 16 Mar 2024 17:38:27 +0100 Subject: [PATCH 6/6] Avoid exceptions in updater, but let send rise --- custom_components/sox/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/sox/media_player.py b/custom_components/sox/media_player.py index 9c65c2c..a5faace 100644 --- a/custom_components/sox/media_player.py +++ b/custom_components/sox/media_player.py @@ -219,4 +219,7 @@ async def async_volume_down(self): async def async_update(self): """Get the latest data and update the state.""" if not self._is_connected or self._volume is not None: - await self._async_send("") # For compatibility with old sound server + try: + await self._async_send("") # For compatibility with old sound server + except: + pass