Centralize storage and updating of Sonos favorites (#50581)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/50709/head^2
parent
224cc779c4
commit
b84cf915f3
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
import logging
|
||||
import socket
|
||||
|
@ -32,6 +33,7 @@ from .const import (
|
|||
SONOS_GROUP_UPDATE,
|
||||
SONOS_SEEN,
|
||||
)
|
||||
from .favorites import SonosFavorites
|
||||
from .speaker import SonosSpeaker
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -65,7 +67,9 @@ class SonosData:
|
|||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the data."""
|
||||
self.discovered: dict[str, SonosSpeaker] = {}
|
||||
# OrderedDict behavior used by SonosFavorites
|
||||
self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict()
|
||||
self.favorites: dict[str, SonosFavorites] = {}
|
||||
self.topology_condition = asyncio.Condition()
|
||||
self.discovery_thread = None
|
||||
self.hosts_heartbeat = None
|
||||
|
@ -122,10 +126,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
data = hass.data[DATA_SONOS]
|
||||
|
||||
if soco.uid not in data.discovered:
|
||||
_LOGGER.debug("Adding new speaker")
|
||||
speaker_info = soco.get_speaker_info(True)
|
||||
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
||||
speaker = SonosSpeaker(hass, soco, speaker_info)
|
||||
data.discovered[soco.uid] = speaker
|
||||
if soco.household_id not in data.favorites:
|
||||
data.favorites[soco.household_id] = SonosFavorites(
|
||||
hass, soco.household_id
|
||||
)
|
||||
data.favorites[soco.household_id].update()
|
||||
speaker.setup()
|
||||
else:
|
||||
dispatcher_send(hass, f"{SONOS_SEEN}-{soco.uid}", soco)
|
||||
|
|
|
@ -136,6 +136,7 @@ SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
|
|||
SONOS_ENTITY_CREATED = "sonos_entity_created"
|
||||
SONOS_ENTITY_UPDATE = "sonos_entity_update"
|
||||
SONOS_GROUP_UPDATE = "sonos_group_update"
|
||||
SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated"
|
||||
SONOS_STATE_UPDATED = "sonos_state_updated"
|
||||
SONOS_SEEN = "sonos_seen"
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ from .const import (
|
|||
DOMAIN,
|
||||
SONOS_ENTITY_CREATED,
|
||||
SONOS_ENTITY_UPDATE,
|
||||
SONOS_HOUSEHOLD_UPDATED,
|
||||
SONOS_STATE_UPDATED,
|
||||
)
|
||||
from .speaker import SonosSpeaker
|
||||
|
@ -48,6 +49,13 @@ class SonosEntity(Entity):
|
|||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SONOS_HOUSEHOLD_UPDATED}-{self.soco.household_id}",
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
async_dispatcher_send(
|
||||
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain
|
||||
)
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
"""Class representing Sonos favorites."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from pysonos.data_structures import DidlFavorite
|
||||
from pysonos.events_base import Event as SonosEvent
|
||||
from pysonos.exceptions import SoCoException
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DATA_SONOS, SONOS_HOUSEHOLD_UPDATED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SonosFavorites:
|
||||
"""Storage class for Sonos favorites."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, household_id: str) -> None:
|
||||
"""Initialize the data."""
|
||||
self.hass = hass
|
||||
self.household_id = household_id
|
||||
self._favorites: list[DidlFavorite] = []
|
||||
self._event_version: str | None = None
|
||||
self._next_update: Callable | None = None
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
"""Return an iterator for the known favorites."""
|
||||
favorites = self._favorites.copy()
|
||||
return iter(favorites)
|
||||
|
||||
@callback
|
||||
def async_delayed_update(self, event: SonosEvent) -> None:
|
||||
"""Add a delay when triggered by an event.
|
||||
|
||||
Updated favorites are not always immediately available.
|
||||
|
||||
"""
|
||||
event_id = event.variables["favorites_update_id"]
|
||||
if not self._event_version:
|
||||
self._event_version = event_id
|
||||
return
|
||||
|
||||
if self._event_version == event_id:
|
||||
_LOGGER.debug("Favorites haven't changed (event_id: %s)", event_id)
|
||||
return
|
||||
|
||||
self._event_version = event_id
|
||||
|
||||
if self._next_update:
|
||||
self._next_update()
|
||||
|
||||
self._next_update = self.hass.helpers.event.async_call_later(3, self.update)
|
||||
|
||||
def update(self, now: datetime.datetime | None = None) -> None:
|
||||
"""Request new Sonos favorites from a speaker."""
|
||||
new_favorites = None
|
||||
discovered = self.hass.data[DATA_SONOS].discovered
|
||||
|
||||
for uid, speaker in discovered.items():
|
||||
try:
|
||||
new_favorites = speaker.soco.music_library.get_sonos_favorites()
|
||||
except SoCoException as err:
|
||||
_LOGGER.warning(
|
||||
"Error requesting favorites from %s: %s", speaker.soco, err
|
||||
)
|
||||
else:
|
||||
# Prefer this SoCo instance next update
|
||||
discovered.move_to_end(uid, last=False)
|
||||
break
|
||||
|
||||
if new_favorites is None:
|
||||
_LOGGER.error("Could not reach any speakers to update favorites")
|
||||
return
|
||||
|
||||
self._favorites = []
|
||||
for fav in new_favorites:
|
||||
try:
|
||||
# exclude non-playable favorites with no linked resources
|
||||
if fav.reference.resources:
|
||||
self._favorites.append(fav)
|
||||
except SoCoException as ex:
|
||||
# Skip unknown types
|
||||
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
|
||||
_LOGGER.debug(
|
||||
"Cached %s favorites for household %s",
|
||||
len(self._favorites),
|
||||
self.household_id,
|
||||
)
|
||||
dispatcher_send(self.hass, f"{SONOS_HOUSEHOLD_UPDATED}-{self.household_id}")
|
|
@ -439,7 +439,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||
elif source == SOURCE_TV:
|
||||
soco.switch_to_tv()
|
||||
else:
|
||||
fav = [fav for fav in self.coordinator.favorites if fav.title == source]
|
||||
fav = [fav for fav in self.speaker.favorites if fav.title == source]
|
||||
if len(fav) == 1:
|
||||
src = fav.pop()
|
||||
uri = src.reference.get_uri()
|
||||
|
@ -456,7 +456,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||
@property # type: ignore[misc]
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
sources = [fav.title for fav in self.coordinator.favorites]
|
||||
sources = [fav.title for fav in self.speaker.favorites]
|
||||
|
||||
model = self.coordinator.model_name.upper()
|
||||
if "PLAY:5" in model or "CONNECT" in model:
|
||||
|
|
|
@ -12,7 +12,7 @@ import urllib.parse
|
|||
|
||||
import async_timeout
|
||||
from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo
|
||||
from pysonos.data_structures import DidlAudioBroadcast, DidlFavorite
|
||||
from pysonos.data_structures import DidlAudioBroadcast
|
||||
from pysonos.events_base import Event as SonosEvent, SubscriptionBase
|
||||
from pysonos.exceptions import SoCoException
|
||||
from pysonos.music_library import MusicLibrary
|
||||
|
@ -49,6 +49,7 @@ from .const import (
|
|||
SOURCE_LINEIN,
|
||||
SOURCE_TV,
|
||||
)
|
||||
from .favorites import SonosFavorites
|
||||
from .helpers import soco_error
|
||||
|
||||
EVENT_CHARGING = {
|
||||
|
@ -127,8 +128,9 @@ class SonosSpeaker:
|
|||
self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize a SonosSpeaker."""
|
||||
self.hass: HomeAssistant = hass
|
||||
self.soco: SoCo = soco
|
||||
self.hass = hass
|
||||
self.soco = soco
|
||||
self.household_id: str = soco.household_id
|
||||
self.media = SonosMedia(soco)
|
||||
|
||||
self._is_ready: bool = False
|
||||
|
@ -161,8 +163,6 @@ class SonosSpeaker:
|
|||
self.soco_snapshot: Snapshot | None = None
|
||||
self.snapshot_group: list[SonosSpeaker] | None = None
|
||||
|
||||
self.favorites: list[DidlFavorite] = []
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Run initial setup of the speaker."""
|
||||
self.set_basic_info()
|
||||
|
@ -213,7 +213,6 @@ class SonosSpeaker:
|
|||
"""Set basic information when speaker is reconnected."""
|
||||
self.media.play_mode = self.soco.play_mode
|
||||
self.update_volume()
|
||||
self.set_favorites()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@ -654,24 +653,16 @@ class SonosSpeaker:
|
|||
for speaker in hass.data[DATA_SONOS].discovered.values():
|
||||
speaker.soco._zgs_cache.clear() # pylint: disable=protected-access
|
||||
|
||||
def set_favorites(self) -> None:
|
||||
"""Set available favorites."""
|
||||
self.favorites = []
|
||||
for fav in self.soco.music_library.get_sonos_favorites():
|
||||
try:
|
||||
# Exclude non-playable favorites with no linked resources
|
||||
if fav.reference.resources:
|
||||
self.favorites.append(fav)
|
||||
except SoCoException as ex:
|
||||
# Skip unknown types
|
||||
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
|
||||
@property
|
||||
def favorites(self) -> SonosFavorites:
|
||||
"""Return the SonosFavorites instance for this household."""
|
||||
return self.hass.data[DATA_SONOS].favorites[self.household_id]
|
||||
|
||||
@callback
|
||||
def async_update_content(self, event: SonosEvent | None = None) -> None:
|
||||
"""Update information about available content."""
|
||||
if event and "favorites_update_id" in event.variables:
|
||||
self.hass.async_add_job(self.set_favorites)
|
||||
self.async_write_entity_states()
|
||||
self.favorites.async_delayed_update(event)
|
||||
|
||||
def update_volume(self) -> None:
|
||||
"""Update information about current volume settings."""
|
||||
|
|
Loading…
Reference in New Issue