Home Assistant Unofficial Reference 2024.12.1
favorites.py
Go to the documentation of this file.
1 """Class representing Sonos favorites."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterator
6 import logging
7 import re
8 from typing import TYPE_CHECKING, Any
9 
10 from soco import SoCo
11 from soco.data_structures import DidlFavorite
12 from soco.events_base import Event as SonosEvent
13 from soco.exceptions import SoCoException
14 
15 from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
16 
17 from .const import SONOS_CREATE_FAVORITES_SENSOR, SONOS_FAVORITES_UPDATED
18 from .helpers import soco_error
19 from .household_coordinator import SonosHouseholdCoordinator
20 
21 if TYPE_CHECKING:
22  from .speaker import SonosSpeaker
23 
24 _LOGGER = logging.getLogger(__name__)
25 
26 
28  """Coordinator class for Sonos favorites."""
29 
30  def __init__(self, *args: Any) -> None:
31  """Initialize the data."""
32  super().__init__(*args)
33  self._favorites_favorites: list[DidlFavorite] = []
34  self.last_polled_ids: dict[str, int] = {}
35 
36  def __iter__(self) -> Iterator[DidlFavorite]:
37  """Return an iterator for the known favorites."""
38  favorites = self._favorites_favorites.copy()
39  return iter(favorites)
40 
41  def setup(self, soco: SoCo) -> None:
42  """Override to send a signal on base class setup completion."""
43  super().setup(soco)
44  dispatcher_send(self.hasshass, SONOS_CREATE_FAVORITES_SENSOR, self)
45 
46  @property
47  def count(self) -> int:
48  """Return the number of favorites."""
49  return len(self._favorites_favorites)
50 
51  def lookup_by_item_id(self, item_id: str) -> DidlFavorite | None:
52  """Return the favorite object with the provided item_id."""
53  return next((fav for fav in self._favorites_favorites if fav.item_id == item_id), None)
54 
56  self, soco: SoCo, update_id: int | None = None
57  ) -> None:
58  """Update the cache and update entities."""
59  updated = await self.hasshass.async_add_executor_job(
60  self.update_cacheupdate_cacheupdate_cache, soco, update_id
61  )
62  if not updated:
63  return
64 
66  self.hasshass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
67  )
68 
70  self, event: SonosEvent, speaker: SonosSpeaker
71  ) -> None:
72  """Process the event payload in an async lock and update entities."""
73  event_id = event.variables["favorites_update_id"]
74  container_ids = event.variables["container_update_i_ds"]
75  if not (match := re.search(r"FV:2,(\d+)", container_ids)):
76  return
77 
78  container_id = int(match.groups()[0])
79  event_id = int(event_id.split(",")[-1])
80 
81  async with self.cache_update_lockcache_update_lock:
82  last_poll_id = self.last_polled_ids.get(speaker.uid)
83  if (
84  self.last_processed_event_idlast_processed_event_id
85  and event_id <= self.last_processed_event_idlast_processed_event_id
86  ):
87  # Skip updates if this event_id has already been seen
88  if not last_poll_id:
89  self.last_polled_ids[speaker.uid] = container_id
90  return
91 
92  if last_poll_id and container_id <= last_poll_id:
93  return
94 
95  speaker.event_stats.process(event)
96  _LOGGER.debug(
97  "New favorites event %s from %s (was %s)",
98  event_id,
99  speaker.soco,
100  self.last_processed_event_idlast_processed_event_id,
101  )
102  self.last_processed_event_idlast_processed_event_id = event_id
103  await self.async_update_entitiesasync_update_entitiesasync_update_entities(speaker.soco, container_id)
104 
105  @soco_error()
106  def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
107  """Update cache of known favorites and return if cache has changed."""
108  new_favorites = soco.music_library.get_sonos_favorites()
109 
110  # Polled update_id values do not match event_id values
111  # Each speaker can return a different polled update_id
112  last_poll_id = self.last_polled_ids.get(soco.uid)
113  if last_poll_id and new_favorites.update_id <= last_poll_id:
114  # Skip updates already processed
115  return False
116  self.last_polled_ids[soco.uid] = new_favorites.update_id
117 
118  _LOGGER.debug(
119  "Processing favorites update_id %s for %s (was: %s)",
120  new_favorites.update_id,
121  soco,
122  last_poll_id,
123  )
124 
125  self._favorites_favorites = []
126  for fav in new_favorites:
127  try:
128  # exclude non-playable favorites with no linked resources
129  if fav.reference.resources:
130  self._favorites_favorites.append(fav)
131  except SoCoException as ex:
132  # Skip unknown types
133  _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
134 
135  _LOGGER.debug(
136  "Cached %s favorites for household %s using %s",
137  len(self._favorites_favorites),
138  self.household_idhousehold_id,
139  soco,
140  )
141  return True
None async_process_event(self, SonosEvent event, SonosSpeaker speaker)
Definition: favorites.py:71
None async_update_entities(self, SoCo soco, int|None update_id=None)
Definition: favorites.py:57
DidlFavorite|None lookup_by_item_id(self, str item_id)
Definition: favorites.py:51
bool update_cache(self, SoCo soco, int|None update_id=None)
Definition: favorites.py:106
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:137
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193