Centralize storage and updating of Sonos favorites (#50581)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/50709/head^2
jjlawren 2021-05-16 04:11:35 -05:00 committed by GitHub
parent 224cc779c4
commit b84cf915f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 23 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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
)

View File

@ -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}")

View File

@ -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:

View File

@ -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."""