From 132b4bae95edcf7e16e8dbac3f1dd6003bd0842b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 6 Mar 2025 19:15:51 +0100 Subject: [PATCH] Enhancement: Migrate manual discovery IP's to dedicated config entry --- music_assistant/constants.py | 22 ++++++++++ music_assistant/controllers/config.py | 9 ++++ .../providers/chromecast/__init__.py | 7 +++- music_assistant/providers/sonos/__init__.py | 26 ++---------- music_assistant/providers/sonos/provider.py | 41 ++++++++----------- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index a553e103..53f5b571 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -86,6 +86,7 @@ CONF_MUTE_CONTROL: Final[str] = "mute_control" CONF_OUTPUT_CODEC: Final[str] = "output_codec" CONF_ALLOW_MEMORY_CACHE: Final[str] = "allow_memory_cache" + # config default values DEFAULT_HOST: Final[str] = "0.0.0.0" DEFAULT_PORT: Final[int] = 8095 @@ -535,6 +536,27 @@ CONF_ENTRY_WARN_PREVIEW = ConfigEntry( required=False, ) +CONF_ENTRY_MANUAL_DISCOVERY_IPS = ConfigEntry( + key="manual_discovery_ip_addresses", + type=ConfigEntryType.STRING, + label="Manual IP addresses for discovery", + description="In normal circumstances, " + "Music Assistant will automatically discover all players on the network. " + "using multicast discovery on the (L2) local network, such as mDNS or UPNP.\n\n" + "In case of special network setups or when you run into issues where " + "one or more players are not discovered, you can manually add the IP " + "addresses of the players here. \n\n" + "Note that this setting is not recommended for normal use and should only be used " + "if you know what you are doing. Also, if players are not on the same subnet as" + "the Music Assistant server, you may run into issues with streaming. " + "In that case always ensure that the players can reach the server on the network " + "and double check the base URL configuration of the Stream server in the settings.", + category="advanced", + default_value=[], + required=False, + multi_value=True, +) + def create_sample_rates_config_entry( supported_sample_rates: list[int] | None = None, diff --git a/music_assistant/controllers/config.py b/music_assistant/controllers/config.py index 6cbee71e..18a1a07c 100644 --- a/music_assistant/controllers/config.py +++ b/music_assistant/controllers/config.py @@ -808,6 +808,15 @@ class ConfigController: self._data[CONF_PROVIDERS].pop(instance_id, None) LOGGER.warning("Removed corrupt provider configuration: %s", instance_id) changed = True + # migrate manual_ips to new format + for instance_id, provider_config in list(self._data.get(CONF_PROVIDERS, {}).items()): + if not (values := provider_config.get("values")): + continue + if not (ips := values.get("ips")): + continue + values["manual_discovery_ip_addresses"] = ips.split(",") + del values["ips"] + changed = True # migrate sample_rates config entry for player_id, player_config in list(self._data.get(CONF_PLAYERS, {}).items()): if not (values := player_config.get("values")): diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index c472e7a4..36237c50 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -32,6 +32,7 @@ from music_assistant.constants import ( BASE_PLAYER_CONFIG_ENTRIES, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, + CONF_ENTRY_MANUAL_DISCOVERY_IPS, CONF_ENTRY_OUTPUT_CODEC, CONF_MUTE_CONTROL, CONF_PLAYERS, @@ -116,7 +117,7 @@ async def get_config_entries( values: the (intermediate) raw values for config entries sent with the action. """ # ruff: noqa: ARG001 - return () # we do not have any config entries (yet) + return (CONF_ENTRY_MANUAL_DISCOVERY_IPS,) @dataclass @@ -150,6 +151,8 @@ class ChromecastProvider(PlayerProvider): self._discover_lock = threading.Lock() self.castplayers = {} self.mz_mgr = MultizoneManager() + # Handle config option for manual IP's + manual_ip_config: list[str] = config.get_value(CONF_ENTRY_MANUAL_DISCOVERY_IPS.key) self.browser = CastBrowser( SimpleCastListener( add_callback=self._on_chromecast_discovered, @@ -157,6 +160,7 @@ class ChromecastProvider(PlayerProvider): update_callback=self._on_chromecast_discovered, ), self.mass.aiozc.zeroconf, + known_hosts=manual_ip_config, ) # set-up pychromecast logging if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL): @@ -166,7 +170,6 @@ class ChromecastProvider(PlayerProvider): async def discover_players(self) -> None: """Discover Cast players on the network.""" - # start discovery in executor await self.mass.loop.run_in_executor(None, self.browser.start_discovery) async def unload(self, is_removed: bool = False) -> None: diff --git a/music_assistant/providers/sonos/__init__.py b/music_assistant/providers/sonos/__init__.py index 0ccfa50c..31eed7c6 100644 --- a/music_assistant/providers/sonos/__init__.py +++ b/music_assistant/providers/sonos/__init__.py @@ -10,14 +10,12 @@ 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 CONF_ENTRY_MANUAL_DISCOVERY_IPS, VERBOSE_LOG_LEVEL -from music_assistant.constants import VERBOSE_LOG_LEVEL - -from .provider import CONF_IPS, SonosPlayerProvider +from .provider import SonosPlayerProvider if TYPE_CHECKING: - from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig from music_assistant_models.provider import ProviderManifest from music_assistant import MusicAssistant @@ -51,20 +49,4 @@ async def get_config_entries( values: the (intermediate) raw values for config entries sent with the action. """ # ruff: noqa: ARG001 - 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 AT YOUR OWN RISK", - category="advanced", - default_value=None, - required=False, - ), - ) + return (CONF_ENTRY_MANUAL_DISCOVERY_IPS,) diff --git a/music_assistant/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py index 985aee11..004e2683 100644 --- a/music_assistant/providers/sonos/provider.py +++ b/music_assistant/providers/sonos/provider.py @@ -25,6 +25,7 @@ from zeroconf import ServiceStateChange from music_assistant.constants import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, + CONF_ENTRY_MANUAL_DISCOVERY_IPS, CONF_ENTRY_OUTPUT_CODEC, MASS_LOGO_ONLINE, VERBOSE_LOG_LEVEL, @@ -41,8 +42,6 @@ if TYPE_CHECKING: from music_assistant_models.queue_item import QueueItem from zeroconf.asyncio import AsyncServiceInfo -CONF_IPS = "ips" - class SonosPlayerProvider(PlayerProvider): """Sonos Player provider.""" @@ -73,30 +72,22 @@ class SonosPlayerProvider(PlayerProvider): 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 + # Handle config option for manual IP's + manual_ip_config: list[str] = self.config.get_value(CONF_ENTRY_MANUAL_DISCOVERY_IPS.key) + for ip_address in manual_ip_config: + 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_address) + except ClientError as err: + self.logger.debug( + "Ignoring %s (manual IP) as it is not reachable: %s", ip_address, str(err) ) - await sonos_player.setup() + 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_address + ) + await sonos_player.setup() async def unload(self, is_removed: bool = False) -> None: """Handle close/cleanup of the provider.""" -- 2.34.1