Feat: Allow for advanced networking in the Sonos provider. (#1885)
authorIcelk <main@icelk.dev>
Sun, 19 Jan 2025 12:52:02 +0000 (13:52 +0100)
committerGitHub <noreply@github.com>
Sun, 19 Jan 2025 12:52:02 +0000 (13:52 +0100)
music_assistant/providers/sonos/__init__.py
music_assistant/providers/sonos/provider.py

index beb9361f4de45e137e97b48fe256764649c2021d..e28256d3ddce507ea974ce33792ee8d0db4d3433 100644 (file)
@@ -10,12 +10,14 @@ from __future__ import annotations
 import logging
 from typing import TYPE_CHECKING
 
+from music_assistant_models.config_entries import ConfigEntry, ConfigEntryType
+
 from music_assistant.constants import VERBOSE_LOG_LEVEL
 
-from .provider import SonosPlayerProvider
+from .provider import CONF_IPS, SonosPlayerProvider
 
 if TYPE_CHECKING:
-    from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
+    from music_assistant_models.config_entries import ConfigValueType, ProviderConfig
     from music_assistant_models.provider import ProviderManifest
 
     from music_assistant import MusicAssistant
@@ -49,4 +51,20 @@ async def get_config_entries(
     values: the (intermediate) raw values for config entries sent with the action.
     """
     # ruff: noqa: ARG001
-    return ()
+    return (
+        ConfigEntry(
+            key=CONF_IPS,
+            type=ConfigEntryType.STRING,
+            label="IP addresses (ADVANCED, NOT SUPPORTED)",
+            description="Additional fixed IP addresses for speakers. "
+            "Should be formatted as a comma separated list of IP addresses "
+            "(e.g. '10.0.0.42, 10.0.0.45').\n"
+            "Invalid addresses may result in the Sonos provider "
+            "becoming unresponsive and server crashes.\n"
+            "Bidirectional unicast communication to and between all IPs is required.\n"
+            "NOT SUPPORTED, USE ON YOU'RE OWN RISK",
+            category="advanced",
+            default_value=None,
+            required=False,
+        ),
+    )
index f9892fe58582d9118ede1949d024f3bcc3b40db7..986a4e578e1342adac6d2b53673e9d29cde0d810 100644 (file)
@@ -40,6 +40,8 @@ from .player import SonosPlayer
 if TYPE_CHECKING:
     from zeroconf.asyncio import AsyncServiceInfo
 
+CONF_IPS = "ips"
+
 
 class SonosPlayerProvider(PlayerProvider):
     """Sonos Player provider."""
@@ -67,6 +69,34 @@ class SonosPlayerProvider(PlayerProvider):
             "/sonos_queue/v2.3/timePlayed", self._handle_sonos_queue_time_played
         )
 
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        await super().loaded_in_mass()
+
+        manual_ip_config: str | None
+        # Handle config option for manual IP's (comma separated list)
+        if (manual_ip_config := self.config.get_value(CONF_IPS)) is not None:
+            ips = manual_ip_config.split(",")
+            for raw_ip in ips:
+                # strip to ignore whitespace
+                # (e.g. '10.0.0.42, 10.0.0.43' -> ('10.0.0.42', ' 10.0.0.43'))
+                ip = raw_ip.strip()
+                if ip == "":
+                    continue
+                try:
+                    # get discovery info from SONOS speaker so we can provide an ID & other info
+                    discovery_info = await get_discovery_info(self.mass.http_session, ip)
+                except ClientError as err:
+                    self.logger.debug(
+                        "Ignoring %s (manual IP) as it is not reachable: %s", ip, str(err)
+                    )
+                    continue
+                player_id = discovery_info["device"]["id"]
+                self.sonos_players[player_id] = sonos_player = SonosPlayer(
+                    self, player_id, discovery_info=discovery_info, ip_address=ip
+                )
+                await sonos_player.setup()
+
     async def unload(self, is_removed: bool = False) -> None:
         """Handle close/cleanup of the provider."""
         # disconnect all players