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
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,
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")):
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,
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
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,
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):
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:
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
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,)
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,
from music_assistant_models.queue_item import QueueItem
from zeroconf.asyncio import AsyncServiceInfo
-CONF_IPS = "ips"
-
class SonosPlayerProvider(PlayerProvider):
"""Sonos Player provider."""
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."""