2021-05-16 09:11:35 +00:00
|
|
|
"""Class representing Sonos favorites."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from collections.abc import Iterator
|
|
|
|
import logging
|
2021-09-28 07:49:32 +00:00
|
|
|
import re
|
2021-06-16 15:30:05 +00:00
|
|
|
from typing import Any
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-07-22 22:40:30 +00:00
|
|
|
from soco import SoCo
|
|
|
|
from soco.data_structures import DidlFavorite
|
|
|
|
from soco.exceptions import SoCoException
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-09-28 07:49:32 +00:00
|
|
|
from homeassistant.core import callback
|
2021-06-16 15:30:05 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-06-16 15:30:05 +00:00
|
|
|
from .const import SONOS_FAVORITES_UPDATED
|
|
|
|
from .household_coordinator import SonosHouseholdCoordinator
|
2021-05-16 09:11:35 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2021-06-16 15:30:05 +00:00
|
|
|
class SonosFavorites(SonosHouseholdCoordinator):
|
|
|
|
"""Coordinator class for Sonos favorites."""
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-06-16 15:30:05 +00:00
|
|
|
def __init__(self, *args: Any) -> None:
|
2021-05-16 09:11:35 +00:00
|
|
|
"""Initialize the data."""
|
2021-06-16 15:30:05 +00:00
|
|
|
super().__init__(*args)
|
2021-05-16 09:11:35 +00:00
|
|
|
self._favorites: list[DidlFavorite] = []
|
2021-09-28 07:49:32 +00:00
|
|
|
self.last_polled_ids: dict[str, int] = {}
|
2021-05-16 09:11:35 +00:00
|
|
|
|
|
|
|
def __iter__(self) -> Iterator:
|
|
|
|
"""Return an iterator for the known favorites."""
|
|
|
|
favorites = self._favorites.copy()
|
|
|
|
return iter(favorites)
|
|
|
|
|
2021-09-28 07:49:32 +00:00
|
|
|
async def async_update_entities(
|
|
|
|
self, soco: SoCo, update_id: int | None = None
|
|
|
|
) -> None:
|
2021-06-16 15:30:05 +00:00
|
|
|
"""Update the cache and update entities."""
|
2021-09-28 07:49:32 +00:00
|
|
|
updated = await self.hass.async_add_executor_job(
|
|
|
|
self.update_cache, soco, update_id
|
|
|
|
)
|
|
|
|
if not updated:
|
|
|
|
return
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-06-16 15:30:05 +00:00
|
|
|
async_dispatcher_send(
|
|
|
|
self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
|
|
|
|
)
|
2021-05-16 09:11:35 +00:00
|
|
|
|
2021-09-28 07:49:32 +00:00
|
|
|
@callback
|
|
|
|
def async_handle_event(self, event_id: str, container_ids: str, soco: SoCo) -> None:
|
|
|
|
"""Create a task to update from an event callback."""
|
|
|
|
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])
|
|
|
|
|
|
|
|
self.hass.async_create_task(
|
|
|
|
self.async_process_event(event_id, container_id, soco)
|
|
|
|
)
|
|
|
|
|
|
|
|
async def async_process_event(
|
|
|
|
self, event_id: int, container_id: int, soco: SoCo
|
|
|
|
) -> None:
|
|
|
|
"""Process the event payload in an async lock and update entities."""
|
|
|
|
async with self.cache_update_lock:
|
|
|
|
last_poll_id = self.last_polled_ids.get(soco.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[soco.uid] = container_id
|
|
|
|
return
|
|
|
|
|
|
|
|
if last_poll_id and container_id <= last_poll_id:
|
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
"New favorites event %s from %s (was %s)",
|
|
|
|
event_id,
|
|
|
|
soco,
|
|
|
|
self.last_processed_event_id,
|
|
|
|
)
|
|
|
|
self.last_processed_event_id = event_id
|
|
|
|
await self.async_update_entities(soco, container_id)
|
|
|
|
|
|
|
|
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
|
|
|
"""Update cache of known favorites and return if cache has changed."""
|
2021-06-16 15:30:05 +00:00
|
|
|
new_favorites = soco.music_library.get_sonos_favorites()
|
|
|
|
|
2021-09-28 07:49:32 +00:00
|
|
|
# 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 = []
|
2021-05-16 09:11:35 +00:00
|
|
|
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)
|
2021-06-16 15:30:05 +00:00
|
|
|
|
2021-05-16 09:11:35 +00:00
|
|
|
_LOGGER.debug(
|
2021-09-28 07:49:32 +00:00
|
|
|
"Cached %s favorites for household %s using %s",
|
2021-05-16 09:11:35 +00:00
|
|
|
len(self._favorites),
|
|
|
|
self.household_id,
|
2021-09-28 07:49:32 +00:00
|
|
|
soco,
|
2021-05-16 09:11:35 +00:00
|
|
|
)
|
2021-09-28 07:49:32 +00:00
|
|
|
return True
|