Skip to content

Commit 07c070e

Browse files
authored
Refactor squeezebox integration media_player to use coordinator (#127695)
1 parent 9bda3bd commit 07c070e

File tree

6 files changed

+210
-118
lines changed

6 files changed

+210
-118
lines changed

homeassistant/components/squeezebox/__init__.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from asyncio import timeout
44
from dataclasses import dataclass
5+
from datetime import datetime
56
import logging
67

7-
from pysqueezebox import Server
8+
from pysqueezebox import Player, Server
89

910
from homeassistant.config_entries import ConfigEntry
1011
from homeassistant.const import (
@@ -23,20 +24,30 @@
2324
DeviceEntryType,
2425
format_mac,
2526
)
27+
from homeassistant.helpers.dispatcher import async_dispatcher_send
28+
from homeassistant.helpers.event import async_call_later
2629

2730
from .const import (
2831
CONF_HTTPS,
32+
DISCOVERY_INTERVAL,
2933
DISCOVERY_TASK,
3034
DOMAIN,
35+
KNOWN_PLAYERS,
36+
KNOWN_SERVERS,
3137
MANUFACTURER,
3238
SERVER_MODEL,
39+
SIGNAL_PLAYER_DISCOVERED,
40+
SIGNAL_PLAYER_REDISCOVERED,
3341
STATUS_API_TIMEOUT,
3442
STATUS_QUERY_LIBRARYNAME,
3543
STATUS_QUERY_MAC,
3644
STATUS_QUERY_UUID,
3745
STATUS_QUERY_VERSION,
3846
)
39-
from .coordinator import LMSStatusDataUpdateCoordinator
47+
from .coordinator import (
48+
LMSStatusDataUpdateCoordinator,
49+
SqueezeBoxPlayerUpdateCoordinator,
50+
)
4051

4152
_LOGGER = logging.getLogger(__name__)
4253

@@ -117,15 +128,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
117128
)
118129
_LOGGER.debug("LMS Device %s", device)
119130

120-
coordinator = LMSStatusDataUpdateCoordinator(hass, lms)
131+
server_coordinator = LMSStatusDataUpdateCoordinator(hass, lms)
121132

122133
entry.runtime_data = SqueezeboxData(
123-
coordinator=coordinator,
134+
coordinator=server_coordinator,
124135
server=lms,
125136
)
126137

127-
await coordinator.async_config_entry_first_refresh()
138+
# set up player discovery
139+
known_servers = hass.data.setdefault(DOMAIN, {}).setdefault(KNOWN_SERVERS, {})
140+
known_players = known_servers.setdefault(lms.uuid, {}).setdefault(KNOWN_PLAYERS, [])
141+
142+
async def _player_discovery(now: datetime | None = None) -> None:
143+
"""Discover squeezebox players by polling server."""
144+
145+
async def _discovered_player(player: Player) -> None:
146+
"""Handle a (re)discovered player."""
147+
if player.player_id in known_players:
148+
await player.async_update()
149+
async_dispatcher_send(
150+
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
151+
)
152+
else:
153+
_LOGGER.debug("Adding new entity: %s", player)
154+
player_coordinator = SqueezeBoxPlayerUpdateCoordinator(
155+
hass, player, lms.uuid
156+
)
157+
known_players.append(player.player_id)
158+
async_dispatcher_send(
159+
hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator
160+
)
161+
162+
if players := await lms.async_get_players():
163+
for player in players:
164+
hass.async_create_task(_discovered_player(player))
165+
166+
entry.async_on_unload(
167+
async_call_later(hass, DISCOVERY_INTERVAL, _player_discovery)
168+
)
169+
170+
await server_coordinator.async_config_entry_first_refresh()
128171
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
172+
173+
_LOGGER.debug(
174+
"Adding player discovery job for LMS server: %s", entry.data[CONF_HOST]
175+
)
176+
entry.async_create_background_task(
177+
hass, _player_discovery(), "squeezebox.media_player.player_discovery"
178+
)
179+
129180
return True
130181

131182

homeassistant/components/squeezebox/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
DOMAIN = "squeezebox"
66
DEFAULT_PORT = 9000
77
KNOWN_PLAYERS = "known_players"
8+
KNOWN_SERVERS = "known_servers"
89
MANUFACTURER = "https://lyrion.org/"
910
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
1011
SENSOR_UPDATE_INTERVAL = 60
@@ -27,3 +28,7 @@
2728
STATUS_QUERY_UUID = "uuid"
2829
STATUS_QUERY_VERSION = "version"
2930
SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:")
31+
SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered"
32+
SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
33+
DISCOVERY_INTERVAL = 60
34+
PLAYER_UPDATE_INTERVAL = 5

homeassistant/components/squeezebox/coordinator.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
"""DataUpdateCoordinator for the Squeezebox integration."""
22

33
from asyncio import timeout
4+
from collections.abc import Callable
45
from datetime import timedelta
56
import logging
67
import re
8+
from typing import Any
79

8-
from pysqueezebox import Server
10+
from pysqueezebox import Player, Server
911

10-
from homeassistant.core import HomeAssistant
12+
from homeassistant.core import HomeAssistant, callback
13+
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1114
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
1215
from homeassistant.util import dt as dt_util
1316

1417
from .const import (
18+
PLAYER_UPDATE_INTERVAL,
1519
SENSOR_UPDATE_INTERVAL,
20+
SIGNAL_PLAYER_REDISCOVERED,
1621
STATUS_API_TIMEOUT,
1722
STATUS_SENSOR_LASTSCAN,
1823
STATUS_SENSOR_NEEDSRESTART,
@@ -38,7 +43,7 @@ def __init__(self, hass: HomeAssistant, lms: Server) -> None:
3843
self.newversion_regex = re.compile("<.*$")
3944

4045
async def _async_update_data(self) -> dict:
41-
"""Fetch data fromn LMS status call.
46+
"""Fetch data from LMS status call.
4247
4348
Then we process only a subset to make then nice for HA
4449
"""
@@ -70,3 +75,46 @@ def _prepare_status_data(self, data: dict) -> dict:
7075

7176
_LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data)
7277
return data
78+
79+
80+
class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
81+
"""Coordinator for Squeezebox players."""
82+
83+
def __init__(self, hass: HomeAssistant, player: Player, server_uuid: str) -> None:
84+
"""Initialize the coordinator."""
85+
super().__init__(
86+
hass,
87+
_LOGGER,
88+
name=player.name,
89+
update_interval=timedelta(seconds=PLAYER_UPDATE_INTERVAL),
90+
always_update=True,
91+
)
92+
self.player = player
93+
self.available = True
94+
self._remove_dispatcher: Callable | None = None
95+
self.server_uuid = server_uuid
96+
97+
async def _async_update_data(self) -> dict[str, Any]:
98+
"""Update Player if available, or listen for rediscovery if not."""
99+
if self.available:
100+
# Only update players available at last update, unavailable players are rediscovered instead
101+
await self.player.async_update()
102+
103+
if self.player.connected is False:
104+
_LOGGER.debug("Player %s is not available", self.name)
105+
self.available = False
106+
107+
# start listening for restored players
108+
self._remove_dispatcher = async_dispatcher_connect(
109+
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
110+
)
111+
return {}
112+
113+
@callback
114+
def rediscovered(self, unique_id: str, connected: bool) -> None:
115+
"""Make a player available again."""
116+
if unique_id == self.player.player_id and connected:
117+
self.available = True
118+
_LOGGER.debug("Player %s is available again", self.name)
119+
if self._remove_dispatcher:
120+
self._remove_dispatcher()

0 commit comments

Comments
 (0)