Add squeezebox server device with common init (#122396)

* squeezebox moves common elements into __init__ to allow for server sensors and device, improves player device

* Update with feedback from PR

* squeezebox Formating fixes, Logging Fixes, remove nasty stored callback

* squeezebox Formating fixes, Logging Fixes, remove nasty stored callback

* squeezebox refactor to use own ConfigEntry and Data

* squeezebox remove own data class

* Update homeassistant/components/squeezebox/__init__.py

Correct typo

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/squeezebox/media_player.py

Stronger typing on entry setup SqueezeboxConfigEntry

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* squeezebox add SqueezeboxConfigEntry

* squeezebox fix mypy type errors

* squeezebox use right Callable

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Phill (pssc) 2024-07-23 14:53:58 +01:00 committed by GitHub
parent ff467463f8
commit f260d63c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 112 additions and 67 deletions

View File

@ -1,20 +1,80 @@
"""The Squeezebox integration.""" """The Squeezebox integration."""
from asyncio import timeout
import logging import logging
from homeassistant.config_entries import ConfigEntry from pysqueezebox import Server
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
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__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: type SqueezeboxConfigEntry = ConfigEntry[Server]
"""Set up Squeezebox from a config entry."""
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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
# Stop player discovery task for this config entry. # Stop player discovery task for this config entry.
hass.data[DOMAIN][entry.entry_id][PLAYER_DISCOVERY_UNSUB]() _LOGGER.debug(
"Reached async_unload_entry for LMS=%s(%s)",
# Remove stored data for this config entry entry.runtime_data.name or "Unknown",
hass.data[DOMAIN].pop(entry.entry_id) entry.entry_id,
)
# Stop server discovery task if this is the last config entry. # Stop server discovery task if this is the last config entry.
current_entries = hass.config_entries.async_entries(DOMAIN) current_entries = hass.config_entries.async_entries(DOMAIN)

View File

@ -1,10 +1,12 @@
"""Constants for the Squeezebox component.""" """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" 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:")

View File

@ -2,12 +2,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from datetime import datetime from datetime import datetime
import json import json
import logging import logging
from typing import Any from typing import Any
from pysqueezebox import Server, async_discover from pysqueezebox import Player, async_discover
import voluptuous as vol import voluptuous as vol
from homeassistant.components import media_source from homeassistant.components import media_source
@ -21,22 +22,19 @@ from homeassistant.components.media_player import (
RepeatMode, RepeatMode,
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY
from homeassistant.const import ( from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT
ATTR_COMMAND,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import ( from homeassistant.helpers import (
config_validation as cv, config_validation as cv,
discovery_flow, discovery_flow,
entity_platform, entity_platform,
) )
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import (
from homeassistant.helpers.device_registry import DeviceInfo, format_mac CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, 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.helpers.start import async_at_start
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import SqueezeboxConfigEntry
from .browse_media import ( from .browse_media import (
build_item_response, build_item_response,
generate_playlist, generate_playlist,
library_payload, library_payload,
media_source_content_filter, media_source_content_filter,
) )
from .const import ( from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, SQUEEZEBOX_SOURCE_STRINGS
CONF_HTTPS,
DISCOVERY_TASK,
DOMAIN,
KNOWN_PLAYERS,
PLAYER_DISCOVERY_UNSUB,
SQUEEZEBOX_SOURCE_STRINGS,
)
SERVICE_CALL_METHOD = "call_method" SERVICE_CALL_METHOD = "call_method"
SERVICE_CALL_QUERY = "call_query" SERVICE_CALL_QUERY = "call_query"
@ -118,29 +110,15 @@ async def start_server_discovery(hass: HomeAssistant) -> None:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: SqueezeboxConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up an LMS Server from a config entry.""" """Set up an player discovery 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)
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(config_entry.entry_id, {})
known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, []) known_players = hass.data[DOMAIN].setdefault(KNOWN_PLAYERS, [])
lms = entry.runtime_data
session = async_get_clientsession(hass) async def _player_discovery(now=None):
_LOGGER.debug("Creating LMS object for %s", host)
lms = Server(session, host, port, username, password, https=https)
async def _discovery(now=None):
"""Discover squeezebox players by polling server.""" """Discover squeezebox players by polling server."""
async def _discovered_player(player): async def _discovered_player(player):
@ -169,13 +147,15 @@ async def async_setup_entry(
for player in players: for player in players:
hass.async_create_task(_discovered_player(player)) hass.async_create_task(_discovered_player(player))
hass.data[DOMAIN][config_entry.entry_id][PLAYER_DISCOVERY_UNSUB] = ( entry.async_on_unload(
async_call_later(hass, DISCOVERY_INTERVAL, _discovery) async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
) )
_LOGGER.debug("Adding player discovery job for LMS server: %s", host) _LOGGER.debug(
config_entry.async_create_background_task( "Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
hass, _discovery(), "squeezebox.media_player.discovery" )
entry.async_create_background_task(
hass, _player_discovery(), "squeezebox.media_player.player_discovery"
) )
# Register entity services # Register entity services
@ -208,7 +188,7 @@ async def async_setup_entry(
platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync")
# Start server discovery task if not already running # 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): class SqueezeBoxEntity(MediaPlayerEntity):
@ -241,14 +221,16 @@ class SqueezeBoxEntity(MediaPlayerEntity):
_last_update: datetime | None = None _last_update: datetime | None = None
_attr_available = True _attr_available = True
def __init__(self, player): def __init__(self, player: Player) -> None:
"""Initialize the SqueezeBox device.""" """Initialize the SqueezeBox device."""
self._player = player self._player = player
self._query_result = {} self._query_result: bool | dict = {}
self._remove_dispatcher = None self._remove_dispatcher: Callable | None = None
self._attr_unique_id = format_mac(player.player_id) self._attr_unique_id = format_mac(player.player_id)
self._attr_device_info = DeviceInfo( 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 @property
@ -265,7 +247,7 @@ class SqueezeBoxEntity(MediaPlayerEntity):
"""Make a player available again.""" """Make a player available again."""
if unique_id == self.unique_id and connected: if unique_id == self.unique_id and connected:
self._attr_available = True 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() self._remove_dispatcher()
@property @property
@ -286,7 +268,7 @@ class SqueezeBoxEntity(MediaPlayerEntity):
if self.media_position != last_media_position: if self.media_position != last_media_position:
self._last_update = utcnow() self._last_update = utcnow()
if self._player.connected is False: 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 self._attr_available = False
# start listening for restored players # start listening for restored players
@ -573,7 +555,7 @@ class SqueezeBoxEntity(MediaPlayerEntity):
if other_player_id := player_ids.get(other_player): if other_player_id := player_ids.get(other_player):
await self._player.async_sync(other_player_id) await self._player.async_sync(other_player_id)
else: else:
_LOGGER.info( _LOGGER.debug(
"Could not find player_id for %s. Not syncing", other_player "Could not find player_id for %s. Not syncing", other_player
) )