Refactor Sonos alarms and favorites into system-level coordinators (#51757)

* Refactor alarms and favorites into household-level coordinators

Create SonosHouseholdCoodinator class for system-level data
Fix polling for both alarms and favorites
Adjust tests

* Fix docstring

* Review cleanup

* Move exception handling up a level, do not save a failed coordinator

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/51871/head^2
jjlawren 2021-06-16 10:30:05 -05:00 committed by GitHub
parent bc3e5b39ed
commit 31db3fcb23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 255 additions and 161 deletions

View File

@ -2,7 +2,7 @@
from __future__ import annotations
import asyncio
from collections import OrderedDict, deque
from collections import OrderedDict
import datetime
from enum import Enum
import logging
@ -11,7 +11,6 @@ from urllib.parse import urlparse
import pysonos
from pysonos import events_asyncio
from pysonos.alarms import Alarm
from pysonos.core import SoCo
from pysonos.exceptions import SoCoException
import voluptuous as vol
@ -29,12 +28,12 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from .alarms import SonosAlarms
from .const import (
DATA_SONOS,
DISCOVERY_INTERVAL,
DOMAIN,
PLATFORMS,
SONOS_ALARM_UPDATE,
SONOS_GROUP_UPDATE,
SONOS_REBOOTED,
SONOS_SEEN,
@ -82,11 +81,10 @@ class SonosData:
def __init__(self) -> None:
"""Initialize the data."""
# OrderedDict behavior used by SonosFavorites
# OrderedDict behavior used by SonosAlarms and SonosFavorites
self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict()
self.favorites: dict[str, SonosFavorites] = {}
self.alarms: dict[str, Alarm] = {}
self.processed_alarm_events = deque(maxlen=5)
self.alarms: dict[str, SonosAlarms] = {}
self.topology_condition = asyncio.Condition()
self.hosts_heartbeat = None
self.ssdp_known: set[str] = set()
@ -148,14 +146,17 @@ async def async_setup_entry( # noqa: C901
_LOGGER.debug("Adding new speaker: %s", speaker_info)
speaker = SonosSpeaker(hass, soco, speaker_info)
data.discovered[soco.uid] = speaker
if soco.household_id not in data.favorites:
data.favorites[soco.household_id] = SonosFavorites(
hass, soco.household_id
)
data.favorites[soco.household_id].update()
for coordinator, coord_dict in [
(SonosAlarms, data.alarms),
(SonosFavorites, data.favorites),
]:
if soco.household_id not in coord_dict:
new_coordinator = coordinator(hass, soco.household_id)
new_coordinator.setup(soco)
coord_dict[soco.household_id] = new_coordinator
speaker.setup()
except SoCoException as ex:
_LOGGER.debug("SoCoException, ex=%s", ex)
except (OSError, SoCoException):
_LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True)
def _create_soco(ip_address: str, source: SoCoCreationSource) -> SoCo | None:
"""Create a soco instance and return if successful."""
@ -236,10 +237,6 @@ async def async_setup_entry( # noqa: C901
_async_create_discovered_player(uid, discovered_ip, boot_seqnum)
)
@callback
def _async_signal_update_alarms(event):
async_dispatcher_send(hass, SONOS_ALARM_UPDATE)
async def setup_platforms_and_discovery():
await asyncio.gather(
*[
@ -252,11 +249,6 @@ async def async_setup_entry( # noqa: C901
EVENT_HOMEASSISTANT_START, _async_signal_update_groups
)
)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_signal_update_alarms
)
)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener

View File

@ -0,0 +1,70 @@
"""Class representing Sonos alarms."""
from __future__ import annotations
from collections.abc import Iterator
import logging
from typing import Any
from pysonos import SoCo
from pysonos.alarms import Alarm, get_alarms
from pysonos.exceptions import SoCoException
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM
from .household_coordinator import SonosHouseholdCoordinator
_LOGGER = logging.getLogger(__name__)
class SonosAlarms(SonosHouseholdCoordinator):
"""Coordinator class for Sonos alarms."""
def __init__(self, *args: Any) -> None:
"""Initialize the data."""
super().__init__(*args)
self._alarms: dict[str, Alarm] = {}
def __iter__(self) -> Iterator:
"""Return an iterator for the known alarms."""
alarms = list(self._alarms.values())
return iter(alarms)
def get(self, alarm_id: str) -> Alarm | None:
"""Get an Alarm instance."""
return self._alarms.get(alarm_id)
async def async_update_entities(self, soco: SoCo) -> bool:
"""Create and update alarms entities, return success."""
try:
new_alarms = await self.hass.async_add_executor_job(self.update_cache, soco)
except (OSError, SoCoException) as err:
_LOGGER.error("Could not refresh alarms using %s: %s", soco, err)
return False
for alarm in new_alarms:
speaker = self.hass.data[DATA_SONOS].discovered[alarm.zone.uid]
async_dispatcher_send(
self.hass, SONOS_CREATE_ALARM, speaker, [alarm.alarm_id]
)
async_dispatcher_send(self.hass, f"{SONOS_ALARMS_UPDATED}-{self.household_id}")
return True
def update_cache(self, soco: SoCo) -> set[Alarm]:
"""Populate cache of known alarms.
Prune deleted alarms and return new alarms.
"""
soco_alarms = get_alarms(soco)
new_alarms = set()
for alarm in soco_alarms:
if alarm.alarm_id not in self._alarms:
new_alarms.add(alarm)
self._alarms[alarm.alarm_id] = alarm
for alarm_id, alarm in list(self._alarms.items()):
if alarm not in soco_alarms:
self._alarms.pop(alarm_id)
return new_alarms

View File

@ -140,8 +140,8 @@ SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_ENTITY_CREATED = "sonos_entity_created"
SONOS_POLL_UPDATE = "sonos_poll_update"
SONOS_GROUP_UPDATE = "sonos_group_update"
SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated"
SONOS_ALARM_UPDATE = "sonos_alarm_update"
SONOS_ALARMS_UPDATED = "sonos_alarms_updated"
SONOS_FAVORITES_UPDATED = "sonos_favorites_updated"
SONOS_STATE_UPDATED = "sonos_state_updated"
SONOS_REBOOTED = "sonos_rebooted"
SONOS_SEEN = "sonos_seen"

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import (
DOMAIN,
SONOS_ENTITY_CREATED,
SONOS_HOUSEHOLD_UPDATED,
SONOS_FAVORITES_UPDATED,
SONOS_POLL_UPDATE,
SONOS_STATE_UPDATED,
)
@ -54,7 +54,7 @@ class SonosEntity(Entity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SONOS_HOUSEHOLD_UPDATED}-{self.soco.household_id}",
f"{SONOS_FAVORITES_UPDATED}-{self.soco.household_id}",
self.async_write_ha_state,
)
)

View File

@ -2,85 +2,52 @@
from __future__ import annotations
from collections.abc import Iterator
import datetime
import logging
from typing import Callable
from typing import Any
from pysonos import SoCo
from pysonos.data_structures import DidlFavorite
from pysonos.events_base import Event as SonosEvent
from pysonos.exceptions import SoCoException
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DATA_SONOS, SONOS_HOUSEHOLD_UPDATED
from .const import SONOS_FAVORITES_UPDATED
from .household_coordinator import SonosHouseholdCoordinator
_LOGGER = logging.getLogger(__name__)
class SonosFavorites:
"""Storage class for Sonos favorites."""
class SonosFavorites(SonosHouseholdCoordinator):
"""Coordinator class for Sonos favorites."""
def __init__(self, hass: HomeAssistant, household_id: str) -> None:
def __init__(self, *args: Any) -> None:
"""Initialize the data."""
self.hass = hass
self.household_id = household_id
super().__init__(*args)
self._favorites: list[DidlFavorite] = []
self._event_version: str | None = None
self._next_update: Callable | None = None
def __iter__(self) -> Iterator:
"""Return an iterator for the known favorites."""
favorites = self._favorites.copy()
return iter(favorites)
@callback
def async_delayed_update(self, event: SonosEvent) -> None:
"""Add a delay when triggered by an event.
async def async_update_entities(self, soco: SoCo) -> bool:
"""Update the cache and update entities."""
try:
await self.hass.async_add_executor_job(self.update_cache, soco)
except (OSError, SoCoException) as err:
_LOGGER.warning("Error requesting favorites from %s: %s", soco, err)
return False
Updated favorites are not always immediately available.
async_dispatcher_send(
self.hass, f"{SONOS_FAVORITES_UPDATED}-{self.household_id}"
)
return True
"""
if not (event_id := event.variables.get("favorites_update_id")):
return
if not self._event_version:
self._event_version = event_id
return
if self._event_version == event_id:
_LOGGER.debug("Favorites haven't changed (event_id: %s)", event_id)
return
self._event_version = event_id
if self._next_update:
self._next_update()
self._next_update = self.hass.helpers.event.async_call_later(3, self.update)
def update(self, now: datetime.datetime | None = None) -> None:
def update_cache(self, soco: SoCo) -> None:
"""Request new Sonos favorites from a speaker."""
new_favorites = None
discovered = self.hass.data[DATA_SONOS].discovered
for uid, speaker in discovered.items():
try:
new_favorites = speaker.soco.music_library.get_sonos_favorites()
except SoCoException as err:
_LOGGER.warning(
"Error requesting favorites from %s: %s", speaker.soco, err
)
else:
# Prefer this SoCo instance next update
discovered.move_to_end(uid, last=False)
break
if new_favorites is None:
_LOGGER.error("Could not reach any speakers to update favorites")
return
new_favorites = soco.music_library.get_sonos_favorites()
self._favorites = []
for fav in new_favorites:
try:
# exclude non-playable favorites with no linked resources
@ -89,9 +56,9 @@ class SonosFavorites:
except SoCoException as ex:
# Skip unknown types
_LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex)
_LOGGER.debug(
"Cached %s favorites for household %s",
len(self._favorites),
self.household_id,
)
dispatcher_send(self.hass, f"{SONOS_HOUSEHOLD_UPDATED}-{self.household_id}")

View File

@ -0,0 +1,74 @@
"""Class representing a Sonos household storage helper."""
from __future__ import annotations
from collections import deque
from collections.abc import Callable, Coroutine
import logging
from typing import Any
from pysonos import SoCo
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from .const import DATA_SONOS
_LOGGER = logging.getLogger(__name__)
class SonosHouseholdCoordinator:
"""Base class for Sonos household-level storage."""
def __init__(self, hass: HomeAssistant, household_id: str) -> None:
"""Initialize the data."""
self.hass = hass
self.household_id = household_id
self._processed_events = deque(maxlen=5)
self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None
def setup(self, soco: SoCo) -> None:
"""Set up the SonosAlarm instance."""
self.update_cache(soco)
self.hass.add_job(self._async_create_polling_debouncer)
async def _async_create_polling_debouncer(self) -> None:
"""Create a polling debouncer in async context.
Used to ensure redundant poll requests from all speakers are coalesced.
"""
self.async_poll = Debouncer(
self.hass,
_LOGGER,
cooldown=3,
immediate=False,
function=self._async_poll,
).async_call
async def _async_poll(self) -> None:
"""Poll any known speaker."""
discovered = self.hass.data[DATA_SONOS].discovered
for uid, speaker in discovered.items():
_LOGGER.debug("Updating %s using %s", type(self).__name__, speaker.soco)
success = await self.async_update_entities(speaker.soco)
if success:
# Prefer this SoCo instance next update
discovered.move_to_end(uid, last=False)
break
@callback
def async_handle_event(self, event_id: str, soco: SoCo) -> None:
"""Create a task to update from an event callback."""
if event_id in self._processed_events:
return
self._processed_events.append(event_id)
self.hass.async_create_task(self.async_update_entities(soco))
async def async_update_entities(self, soco: SoCo) -> bool:
"""Update the cache and update entities."""
raise NotImplementedError()
def update_cache(self, soco: SoCo) -> Any:
"""Update the cache of the household-level feature."""
raise NotImplementedError()

View File

@ -55,6 +55,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.network import is_internal_request
from .const import (
DATA_SONOS,
DOMAIN as SONOS_DOMAIN,
MEDIA_TYPES_TO_SONOS,
PLAYABLE_MEDIA_TYPES,
@ -295,6 +296,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
async def async_update(self) -> None:
"""Retrieve latest state by polling."""
await self.hass.data[DATA_SONOS].favorites[
self.speaker.household_id
].async_poll()
await self.hass.async_add_executor_job(self._update)
def _update(self) -> None:

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from collections import deque
from collections.abc import Coroutine
import contextlib
import datetime
@ -12,7 +11,6 @@ from typing import Any, Callable
import urllib.parse
import async_timeout
from pysonos.alarms import get_alarms
from pysonos.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo
from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer
from pysonos.events_base import Event as SonosEvent, SubscriptionBase
@ -33,6 +31,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.util import dt as dt_util
from .alarms import SonosAlarms
from .const import (
BATTERY_SCAN_INTERVAL,
DATA_SONOS,
@ -40,7 +39,6 @@ from .const import (
PLATFORMS,
SCAN_INTERVAL,
SEEN_EXPIRE_TIME,
SONOS_ALARM_UPDATE,
SONOS_CREATE_ALARM,
SONOS_CREATE_BATTERY,
SONOS_CREATE_MEDIA_PLAYER,
@ -225,7 +223,9 @@ class SonosSpeaker:
else:
self._platforms_ready.update({BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN})
if new_alarms := self.update_alarms_for_speaker():
if new_alarms := [
alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid
]:
dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
else:
self._platforms_ready.add(SWITCH_DOMAIN)
@ -233,7 +233,7 @@ class SonosSpeaker:
self._event_dispatchers = {
"AlarmClock": self.async_dispatch_alarms,
"AVTransport": self.async_dispatch_media_update,
"ContentDirectory": self.favorites.async_delayed_update,
"ContentDirectory": self.async_dispatch_favorites,
"DeviceProperties": self.async_dispatch_device_properties,
"RenderingControl": self.async_update_volume,
"ZoneGroupTopology": self.async_update_groups,
@ -246,8 +246,11 @@ class SonosSpeaker:
#
async def async_handle_new_entity(self, entity_type: str) -> None:
"""Listen to new entities to trigger first subscription."""
if self._platforms_ready == PLATFORMS:
return
self._platforms_ready.add(entity_type)
if self._platforms_ready == PLATFORMS and not self._subscriptions:
if self._platforms_ready == PLATFORMS:
self._resubscription_lock = asyncio.Lock()
await self.async_subscribe()
self._is_ready = True
@ -274,6 +277,11 @@ class SonosSpeaker:
"""Return whether this speaker is available."""
return self._seen_timer is not None
@property
def alarms(self) -> SonosAlarms:
"""Return the SonosAlarms instance for this household."""
return self.hass.data[DATA_SONOS].alarms[self.household_id]
@property
def favorites(self) -> SonosFavorites:
"""Return the SonosFavorites instance for this household."""
@ -284,11 +292,6 @@ class SonosSpeaker:
"""Return true if player is a coordinator."""
return self.coordinator is None
@property
def processed_alarm_events(self) -> deque[str]:
"""Return the container of processed alarm events."""
return self.hass.data[DATA_SONOS].processed_alarm_events
@property
def subscription_address(self) -> str | None:
"""Return the current subscription callback address if any."""
@ -381,13 +384,10 @@ class SonosSpeaker:
@callback
def async_dispatch_alarms(self, event: SonosEvent) -> None:
"""Create a task to update alarms from an event."""
if not (update_id := event.variables.get("alarm_list_version")):
"""Add the soco instance associated with the event to the callback."""
if not (event_id := event.variables.get("alarm_list_version")):
return
if update_id in self.processed_alarm_events:
return
self.processed_alarm_events.append(update_id)
self.hass.async_add_executor_job(self.update_alarms)
self.alarms.async_handle_event(event_id, self.soco)
@callback
def async_dispatch_device_properties(self, event: SonosEvent) -> None:
@ -401,6 +401,13 @@ class SonosSpeaker:
await self.async_update_battery_info(battery_dict)
self.async_write_entity_states()
@callback
def async_dispatch_favorites(self, event: SonosEvent) -> None:
"""Add the soco instance associated with the event to the callback."""
if not (event_id := event.variables.get("favorites_update_id")):
return
self.favorites.async_handle_event(event_id, self.soco)
@callback
def async_dispatch_media_update(self, event: SonosEvent) -> None:
"""Update information about currently playing media from an event."""
@ -493,37 +500,6 @@ class SonosSpeaker:
await self.async_unseen(will_reconnect=True)
await self.async_seen(soco)
#
# Alarm management
#
def update_alarms_for_speaker(self) -> set[str]:
"""Update current alarm instances.
Updates hass.data[DATA_SONOS].alarms and returns a list of all alarms that are new.
"""
new_alarms = set()
stored_alarms = self.hass.data[DATA_SONOS].alarms
updated_alarms = get_alarms(self.soco)
for alarm in updated_alarms:
if alarm.zone.uid == self.soco.uid and alarm.alarm_id not in list(
stored_alarms.keys()
):
new_alarms.add(alarm.alarm_id)
stored_alarms[alarm.alarm_id] = alarm
for alarm_id, alarm in list(stored_alarms.items()):
if alarm not in updated_alarms:
stored_alarms.pop(alarm_id)
return new_alarms
def update_alarms(self) -> None:
"""Update alarms from an event."""
if new_alarms := self.update_alarms_for_speaker():
dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
dispatcher_send(self.hass, SONOS_ALARM_UPDATE)
#
# Battery management
#
@ -618,7 +594,7 @@ class SonosSpeaker:
coordinator_uid = self.soco.uid
slave_uids = []
with contextlib.suppress(SoCoException):
with contextlib.suppress(OSError, SoCoException):
if self.soco.group and self.soco.group.coordinator:
coordinator_uid = self.soco.group.coordinator.uid
slave_uids = [

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import datetime
import logging
from pysonos.exceptions import SoCoUPnPException
from pysonos.exceptions import SoCoException, SoCoUPnPException
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.const import ATTR_TIME
@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
DATA_SONOS,
DOMAIN as SONOS_DOMAIN,
SONOS_ALARM_UPDATE,
SONOS_ALARMS_UPDATED,
SONOS_CREATE_ALARM,
)
from .entity import SonosEntity
@ -35,15 +35,12 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Sonos from a config entry."""
configured_alarms = set()
async def _async_create_entity(speaker: SonosSpeaker, new_alarms: set) -> None:
for alarm_id in new_alarms:
if alarm_id not in configured_alarms:
_LOGGER.debug("Creating alarm with id %s", alarm_id)
entity = SonosAlarmEntity(alarm_id, speaker)
async_add_entities([entity])
configured_alarms.add(alarm_id)
async def _async_create_entity(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
entities = []
for alarm_id in alarm_ids:
_LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name)
entities.append(SonosAlarmEntity(alarm_id, speaker))
async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity)
@ -57,7 +54,8 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
"""Initialize the switch."""
super().__init__(speaker)
self._alarm_id = alarm_id
self.alarm_id = alarm_id
self.household_id = speaker.household_id
self.entity_id = ENTITY_ID_FORMAT.format(f"sonos_alarm_{self.alarm_id}")
async def async_added_to_hass(self) -> None:
@ -66,20 +64,15 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SONOS_ALARM_UPDATE,
self.async_update,
f"{SONOS_ALARMS_UPDATED}-{self.household_id}",
self.async_update_state,
)
)
@property
def alarm(self):
"""Return the alarm instance."""
return self.hass.data[DATA_SONOS].alarms[self.alarm_id]
@property
def alarm_id(self):
"""Return the ID of the alarm."""
return self._alarm_id
return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id)
@property
def unique_id(self) -> str:
@ -100,10 +93,14 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
str(self.alarm.start_time)[0:5],
)
async def async_update(self) -> None:
"""Call the central alarm polling method."""
await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll()
@callback
def async_check_if_available(self):
"""Check if alarm exists and remove alarm entity if not available."""
if self.alarm_id in self.hass.data[DATA_SONOS].alarms:
if self.alarm:
return True
_LOGGER.debug("%s has been deleted", self.entity_id)
@ -114,7 +111,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
return False
async def async_update(self) -> None:
async def async_update_state(self) -> None:
"""Poll the device for the current state."""
if not self.async_check_if_available():
return
@ -170,6 +167,11 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
or bool(recurrence == "WEEKENDS" and int(timestr) not in range(1, 7))
)
@property
def available(self) -> bool:
"""Return whether this alarm is available."""
return (self.alarm is not None) and self.speaker.available
@property
def is_on(self):
"""Return state of Sonos alarm switch."""
@ -203,5 +205,5 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
_LOGGER.debug("Toggling the state of %s", self.entity_id)
self.alarm.enabled = turn_on
await self.hass.async_add_executor_job(self.alarm.save)
except SoCoUPnPException as exc:
_LOGGER.error("Could not update %s: %s", self.entity_id, exc, exc_info=True)
except (OSError, SoCoException, SoCoUPnPException) as exc:
_LOGGER.error("Could not update %s: %s", self.entity_id, exc)

View File

@ -1,4 +1,6 @@
"""Tests for the Sonos Alarm switch platform."""
from copy import copy
from homeassistant.components.sonos import DOMAIN
from homeassistant.components.sonos.switch import (
ATTR_DURATION,
@ -52,24 +54,31 @@ async def test_alarm_create_delete(
hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event
):
"""Test for correct creation and deletion of alarms during runtime."""
soco.alarmClock = alarm_clock_extended
entity_registry = async_get_entity_registry(hass)
one_alarm = copy(alarm_clock.ListAlarms.return_value)
two_alarms = copy(alarm_clock_extended.ListAlarms.return_value)
await setup_platform(hass, config_entry, config)
subscription = alarm_clock_extended.subscribe.return_value
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" not in entity_registry.entities
subscription = alarm_clock.subscribe.return_value
sub_callback = subscription.callback
alarm_clock.ListAlarms.return_value = two_alarms
sub_callback(event=alarm_event)
await hass.async_block_till_done()
entity_registry = async_get_entity_registry(hass)
assert "switch.sonos_alarm_14" in entity_registry.entities
assert "switch.sonos_alarm_15" in entity_registry.entities
alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value
alarm_event.increment_variable("alarm_list_version")
alarm_clock.ListAlarms.return_value = one_alarm
sub_callback(event=alarm_event)
await hass.async_block_till_done()