mirror of https://github.com/home-assistant/core
142 lines
4.9 KiB
Python
142 lines
4.9 KiB
Python
"""Class representing Sonos favorites."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
import logging
|
|
import re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from soco import SoCo
|
|
from soco.data_structures import DidlFavorite
|
|
from soco.events_base import Event as SonosEvent
|
|
from soco.exceptions import SoCoException
|
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
|
|
|
|
from .const import SONOS_CREATE_FAVORITES_SENSOR, SONOS_FAVORITES_UPDATED
|
|
from .helpers import soco_error
|
|
from .household_coordinator import SonosHouseholdCoordinator
|
|
|
|
if TYPE_CHECKING:
|
|
from .speaker import SonosSpeaker
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class SonosFavorites(SonosHouseholdCoordinator):
|
|
"""Coordinator class for Sonos favorites."""
|
|
|
|
def __init__(self, *args: Any) -> None:
|
|
"""Initialize the data."""
|
|
super().__init__(*args)
|
|
self._favorites: list[DidlFavorite] = []
|
|
self.last_polled_ids: dict[str, int] = {}
|
|
|
|
def __iter__(self) -> Iterator[DidlFavorite]:
|
|
"""Return an iterator for the known favorites."""
|
|
favorites = self._favorites.copy()
|
|
return iter(favorites)
|
|
|
|
def setup(self, soco: SoCo) -> None:
|
|
"""Override to send a signal on base class setup completion."""
|
|
super().setup(soco)
|
|
dispatcher_send(self.hass, SONOS_CREATE_FAVORITES_SENSOR, self)
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
"""Return the number of favorites."""
|
|
return len(self._favorites)
|
|
|
|
def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None:
|
|
"""Return the favorite object with the provided item_id."""
|
|
return next((fav for fav in self._favorites if fav.item_id == item_id), None)
|
|
|
|
async def async_update_entities(
|
|
self, soco: SoCo, update_id: int | None = None
|
|
) -> None:
|
|
"""Update the cache and update entities."""
|
|
updated = await self.hass.async_add_executor_job(
|
|
self.update_cache, soco, update_id
|
|
)
|
|
if not updated:
|
|
return
|
|
|
|
async_dispatcher_send(
|
|
self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
|
|
)
|
|
|
|
async def async_process_event(
|
|
self, event: SonosEvent, speaker: SonosSpeaker
|
|
) -> None:
|
|
"""Process the event payload in an async lock and update entities."""
|
|
event_id = event.variables["favorites_update_id"]
|
|
container_ids = event.variables["container_update_i_ds"]
|
|
if not (match := re.search(r"FV:2,(\d+)", container_ids)):
|
|
return
|
|
|
|
container_id = int(match.groups()[0])
|
|
event_id = int(event_id.split(",")[-1])
|
|
|
|
async with self.cache_update_lock:
|
|
last_poll_id = self.last_polled_ids.get(speaker.uid)
|
|
if (
|
|
self.last_processed_event_id
|
|
and event_id <= self.last_processed_event_id
|
|
):
|
|
# Skip updates if this event_id has already been seen
|
|
if not last_poll_id:
|
|
self.last_polled_ids[speaker.uid] = container_id
|
|
return
|
|
|
|
if last_poll_id and container_id <= last_poll_id:
|
|
return
|
|
|
|
speaker.event_stats.process(event)
|
|
_LOGGER.debug(
|
|
"New favorites event %s from %s (was %s)",
|
|
event_id,
|
|
speaker.soco,
|
|
self.last_processed_event_id,
|
|
)
|
|
self.last_processed_event_id = event_id
|
|
await self.async_update_entities(speaker.soco, container_id)
|
|
|
|
@soco_error()
|
|
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
|
"""Update cache of known favorites and return if cache has changed."""
|
|
new_favorites = soco.music_library.get_sonos_favorites()
|
|
|
|
# Polled update_id values do not match event_id values
|
|
# Each speaker can return a different polled update_id
|
|
last_poll_id = self.last_polled_ids.get(soco.uid)
|
|
if last_poll_id and new_favorites.update_id <= last_poll_id:
|
|
# Skip updates already processed
|
|
return False
|
|
self.last_polled_ids[soco.uid] = new_favorites.update_id
|
|
|
|
_LOGGER.debug(
|
|
"Processing favorites update_id %s for %s (was: %s)",
|
|
new_favorites.update_id,
|
|
soco,
|
|
last_poll_id,
|
|
)
|
|
|
|
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 using %s",
|
|
len(self._favorites),
|
|
self.household_id,
|
|
soco,
|
|
)
|
|
return True
|