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
 |