-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfavorites.py
140 lines (115 loc) · 4.86 KB
/
favorites.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""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