diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index baaddbef0b6..b6c7f049311 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -1,20 +1,80 @@ """The Squeezebox integration.""" +from asyncio import timeout import logging -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from pysqueezebox import Server -from .const import DISCOVERY_TASK, DOMAIN, PLAYER_DISCOVERY_UNSUB +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + CONF_HTTPS, + DISCOVERY_TASK, + DOMAIN, + STATUS_API_TIMEOUT, + STATUS_QUERY_LIBRARYNAME, + STATUS_QUERY_UUID, +) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.MEDIA_PLAYER] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Squeezebox from a config entry.""" +type SqueezeboxConfigEntry = ConfigEntry[Server] + + +async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -> bool: + """Set up an LMS Server from a config entry.""" + config = entry.data + session = async_get_clientsession(hass) + _LOGGER.debug( + "Reached async_setup_entry for host=%s(%s)", config[CONF_HOST], entry.entry_id + ) + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + https = config.get(CONF_HTTPS, False) + host = config[CONF_HOST] + port = config[CONF_PORT] + + lms = Server(session, host, port, username, password, https=https) + _LOGGER.debug("LMS object for %s", lms) + + try: + async with timeout(STATUS_API_TIMEOUT): + status = await lms.async_query( + "serverstatus", "-", "-", "prefs:libraryname" + ) + except Exception as err: + raise ConfigEntryNotReady( + f"Error communicating config not read for {host}" + ) from err + + if not status: + raise ConfigEntryNotReady(f"Error Config Not read for {host}") + _LOGGER.debug("LMS Status for setup = %s", status) + + lms.uuid = status[STATUS_QUERY_UUID] + lms.name = ( + (STATUS_QUERY_LIBRARYNAME in status and status[STATUS_QUERY_LIBRARYNAME]) + and status[STATUS_QUERY_LIBRARYNAME] + or host + ) + _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid) + + entry.runtime_data = lms + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -22,10 +82,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # Stop player discovery task for this config entry. - hass.data[DOMAIN][entry.entry_id][PLAYER_DISCOVERY_UNSUB]() - - # Remove stored data for this config entry - hass.data[DOMAIN].pop(entry.entry_id) + _LOGGER.debug( + "Reached async_unload_entry for LMS=%s(%s)", + entry.runtime_data.name or "Unknown", + entry.entry_id, + ) # Stop server discovery task if this is the last config entry. current_entries = hass.config_entries.async_entries(DOMAIN) diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 96a541a16ba..a814cf6ecc4 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -1,10 +1,12 @@ """Constants for the Squeezebox component.""" -DOMAIN = "squeezebox" -ENTRY_PLAYERS = "entry_players" -KNOWN_PLAYERS = "known_players" -PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub" -DISCOVERY_TASK = "discovery_task" -DEFAULT_PORT = 9000 -SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") CONF_HTTPS = "https" +DISCOVERY_TASK = "discovery_task" +DOMAIN = "squeezebox" +DEFAULT_PORT = 9000 +KNOWN_PLAYERS = "known_players" +SENSOR_UPDATE_INTERVAL = 60 +STATUS_API_TIMEOUT = 10 +STATUS_QUERY_LIBRARYNAME = "libraryname" +STATUS_QUERY_UUID = "uuid" +SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:") diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index bf1ad1d77c4..aaf64c34ddf 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -2,12 +2,13 @@ from __future__ import annotations +from collections.abc import Callable from datetime import datetime import json import logging from typing import Any -from pysqueezebox import Server, async_discover +from pysqueezebox import Player, async_discover import voluptuous as vol from homeassistant.components import media_source @@ -21,22 +22,19 @@ from homeassistant.components.media_player import ( RepeatMode, async_process_play_media_url, ) -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import ( - ATTR_COMMAND, - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY +from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, discovery_flow, entity_platform, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -46,20 +44,14 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.start import async_at_start from homeassistant.util.dt import utcnow +from . import SqueezeboxConfigEntry from .browse_media import ( build_item_response, generate_playlist, library_payload, media_source_content_filter, ) -from .const import ( - CONF_HTTPS, - DISCOVERY_TASK, - DOMAIN, - KNOWN_PLAYERS, - PLAYER_DISCOVERY_UNSUB, - SQUEEZEBOX_SOURCE_STRINGS, -) +from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, SQUEEZEBOX_SOURCE_STRINGS SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_QUERY = "call_query" @@ -118,29 +110,15 @@ async def start_server_discovery(hass: HomeAssistant) -> None: async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: SqueezeboxConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up an LMS Server from a config entry.""" - config = config_entry.data - _LOGGER.debug("Reached async_setup_entry for host=%s", config[CONF_HOST]) - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - host = config[CONF_HOST] - port = config[CONF_PORT] - https = config.get(CONF_HTTPS, False) - + """Set up an player discovery from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) - known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, []) + lms = entry.runtime_data - session = async_get_clientsession(hass) - _LOGGER.debug("Creating LMS object for %s", host) - lms = Server(session, host, port, username, password, https=https) - - async def _discovery(now=None): + async def _player_discovery(now=None): """Discover squeezebox players by polling server.""" async def _discovered_player(player): @@ -169,13 +147,15 @@ async def async_setup_entry( for player in players: hass.async_create_task(_discovered_player(player)) - hass.data[DOMAIN][config_entry.entry_id][PLAYER_DISCOVERY_UNSUB] = ( - async_call_later(hass, DISCOVERY_INTERVAL, _discovery) + entry.async_on_unload( + async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery) ) - _LOGGER.debug("Adding player discovery job for LMS server: %s", host) - config_entry.async_create_background_task( - hass, _discovery(), "squeezebox.media_player.discovery" + _LOGGER.debug( + "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST] + ) + entry.async_create_background_task( + hass, _player_discovery(), "squeezebox.media_player.player_discovery" ) # Register entity services @@ -208,7 +188,7 @@ async def async_setup_entry( platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") # Start server discovery task if not already running - config_entry.async_on_unload(async_at_start(hass, start_server_discovery)) + entry.async_on_unload(async_at_start(hass, start_server_discovery)) class SqueezeBoxEntity(MediaPlayerEntity): @@ -241,14 +221,16 @@ class SqueezeBoxEntity(MediaPlayerEntity): _last_update: datetime | None = None _attr_available = True - def __init__(self, player): + def __init__(self, player: Player) -> None: """Initialize the SqueezeBox device.""" self._player = player - self._query_result = {} - self._remove_dispatcher = None + self._query_result: bool | dict = {} + self._remove_dispatcher: Callable | None = None self._attr_unique_id = format_mac(player.player_id) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, name=player.name + identifiers={(DOMAIN, self._attr_unique_id)}, + name=player.name, + connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)}, ) @property @@ -265,7 +247,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Make a player available again.""" if unique_id == self.unique_id and connected: self._attr_available = True - _LOGGER.info("Player %s is available again", self.name) + _LOGGER.debug("Player %s is available again", self.name) self._remove_dispatcher() @property @@ -286,7 +268,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): if self.media_position != last_media_position: self._last_update = utcnow() if self._player.connected is False: - _LOGGER.info("Player %s is not available", self.name) + _LOGGER.debug("Player %s is not available", self.name) self._attr_available = False # start listening for restored players @@ -573,7 +555,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): if other_player_id := player_ids.get(other_player): await self._player.async_sync(other_player_id) else: - _LOGGER.info( + _LOGGER.debug( "Could not find player_id for %s. Not syncing", other_player )