Refactor Sonos polling (#65722)

* Refactor Sonos polling

Explicitly rename fallback polling
Catch soco exceptions centrally where possible
Create SonosPollingEntity subclass
Remove unnecessary soco_error fixture argument
Remove unnecessary polling in update_volume()
Adjust log levels and wording
Set explicit timeout on library

* Adjust logging to use raised exceptions

* Simplify availabiliity checks when using built-in poller

* Fix typing for return values
This commit is contained in:
jjlawren 2022-02-08 12:17:05 -06:00 committed by GitHub
parent 4efebcb86c
commit a7fd477c64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 89 additions and 85 deletions

View file

@ -119,6 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Sonos from a config entry.""" """Set up Sonos from a config entry."""
soco_config.EVENTS_MODULE = events_asyncio soco_config.EVENTS_MODULE = events_asyncio
soco_config.REQUEST_TIMEOUT = 9.5
if DATA_SONOS not in hass.data: if DATA_SONOS not in hass.data:
hass.data[DATA_SONOS] = SonosData() hass.data[DATA_SONOS] = SonosData()

View file

@ -8,11 +8,11 @@ from typing import TYPE_CHECKING, Any
from soco import SoCo from soco import SoCo
from soco.alarms import Alarm, Alarms from soco.alarms import Alarm, Alarms
from soco.events_base import Event as SonosEvent from soco.events_base import Event as SonosEvent
from soco.exceptions import SoCoException
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM
from .helpers import soco_error
from .household_coordinator import SonosHouseholdCoordinator from .household_coordinator import SonosHouseholdCoordinator
if TYPE_CHECKING: if TYPE_CHECKING:
@ -71,13 +71,10 @@ class SonosAlarms(SonosHouseholdCoordinator):
speaker.event_stats.process(event) speaker.event_stats.process(event)
await self.async_update_entities(speaker.soco, event_id) await self.async_update_entities(speaker.soco, event_id)
@soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known alarms and return if cache has changed.""" """Update cache of known alarms and return if cache has changed."""
try: self.alarms.update(soco)
self.alarms.update(soco)
except (OSError, SoCoException) as err:
_LOGGER.error("Could not update alarms using %s: %s", soco, err)
return False
if update_id and self.alarms.last_id < update_id: if update_id and self.alarms.last_id < update_id:
# Skip updates if latest query result is outdated or lagging # Skip updates if latest query result is outdated or lagging

View file

@ -64,7 +64,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity):
self._attr_unique_id = f"{self.soco.uid}-power" self._attr_unique_id = f"{self.soco.uid}-power"
self._attr_name = f"{self.speaker.zone_name} Power" self._attr_name = f"{self.speaker.zone_name} Power"
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Poll the device for the current state.""" """Poll the device for the current state."""
await self.speaker.async_poll_battery() await self.speaker.async_poll_battery()
@ -98,7 +98,7 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity):
self._attr_unique_id = f"{self.soco.uid}-microphone" self._attr_unique_id = f"{self.soco.uid}-microphone"
self._attr_name = f"{self.speaker.zone_name} Microphone" self._attr_name = f"{self.speaker.zone_name} Microphone"
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Stub for abstract class implementation. Not a pollable attribute.""" """Stub for abstract class implementation. Not a pollable attribute."""
@property @property

View file

@ -153,7 +153,7 @@ SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor"
SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_SWITCHES = "sonos_create_switches"
SONOS_CREATE_LEVELS = "sonos_create_levels" SONOS_CREATE_LEVELS = "sonos_create_levels"
SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_POLL_UPDATE = "sonos_poll_update" SONOS_FALLBACK_POLL = "sonos_fallback_poll"
SONOS_ALARMS_UPDATED = "sonos_alarms_updated" SONOS_ALARMS_UPDATED = "sonos_alarms_updated"
SONOS_FAVORITES_UPDATED = "sonos_favorites_updated" SONOS_FAVORITES_UPDATED = "sonos_favorites_updated"
SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity" SONOS_SPEAKER_ACTIVITY = "sonos_speaker_activity"

View file

@ -7,7 +7,6 @@ import logging
import soco.config as soco_config import soco.config as soco_config
from soco.core import SoCo from soco.core import SoCo
from soco.exceptions import SoCoException
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
@ -17,10 +16,11 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import ( from .const import (
DATA_SONOS, DATA_SONOS,
DOMAIN, DOMAIN,
SONOS_FALLBACK_POLL,
SONOS_FAVORITES_UPDATED, SONOS_FAVORITES_UPDATED,
SONOS_POLL_UPDATE,
SONOS_STATE_UPDATED, SONOS_STATE_UPDATED,
) )
from .exception import SonosUpdateError
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements" SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements"
@ -43,8 +43,8 @@ class SonosEntity(Entity):
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}", f"{SONOS_FALLBACK_POLL}-{self.soco.uid}",
self.async_poll, self.async_fallback_poll,
) )
) )
self.async_on_remove( self.async_on_remove(
@ -66,7 +66,7 @@ class SonosEntity(Entity):
"""Clean up when entity is removed.""" """Clean up when entity is removed."""
del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id]
async def async_poll(self, now: datetime.datetime) -> None: async def async_fallback_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail.""" """Poll the entity if subscriptions fail."""
if not self.speaker.subscriptions_failed: if not self.speaker.subscriptions_failed:
if soco_config.EVENT_ADVERTISE_IP: if soco_config.EVENT_ADVERTISE_IP:
@ -86,13 +86,16 @@ class SonosEntity(Entity):
self.speaker.subscriptions_failed = True self.speaker.subscriptions_failed = True
await self.speaker.async_unsubscribe() await self.speaker.async_unsubscribe()
try: try:
await self._async_poll() await self._async_fallback_poll()
except (OSError, SoCoException) as ex: except SonosUpdateError as err:
_LOGGER.debug("Error connecting to %s: %s", self.entity_id, ex) _LOGGER.debug("Could not fallback poll: %s", err)
@abstractmethod @abstractmethod
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Poll the specific functionality. Should be implemented by platforms if needed.""" """Poll the specific functionality if subscriptions fail.
Should be implemented by platforms if needed.
"""
@property @property
def soco(self) -> SoCo: def soco(self) -> SoCo:
@ -120,3 +123,20 @@ class SonosEntity(Entity):
def available(self) -> bool: def available(self) -> bool:
"""Return whether this device is available.""" """Return whether this device is available."""
return self.speaker.available return self.speaker.available
class SonosPollingEntity(SonosEntity):
"""Representation of a Sonos entity which may not support updating by subscriptions."""
@abstractmethod
def poll_state(self) -> None:
"""Poll the device for the current state."""
def update(self) -> None:
"""Update the state using the built-in entity poller."""
if not self.available:
return
try:
self.poll_state()
except SonosUpdateError as err:
_LOGGER.debug("Could not poll: %s", err)

View file

@ -7,5 +7,5 @@ class UnknownMediaType(BrowseError):
"""Unknown media type.""" """Unknown media type."""
class SpeakerUnavailable(HomeAssistantError): class SonosUpdateError(HomeAssistantError):
"""Speaker is unavailable.""" """Update failed."""

View file

@ -14,6 +14,7 @@ from soco.exceptions import SoCoException
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import SONOS_FAVORITES_UPDATED from .const import SONOS_FAVORITES_UPDATED
from .helpers import soco_error
from .household_coordinator import SonosHouseholdCoordinator from .household_coordinator import SonosHouseholdCoordinator
if TYPE_CHECKING: if TYPE_CHECKING:
@ -90,6 +91,7 @@ class SonosFavorites(SonosHouseholdCoordinator):
self.last_processed_event_id = event_id self.last_processed_event_id = event_id
await self.async_update_entities(speaker.soco, container_id) await self.async_update_entities(speaker.soco, container_id)
@soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known favorites and return if cache has changed.""" """Update cache of known favorites and return if cache has changed."""
new_favorites = soco.music_library.get_sonos_favorites() new_favorites = soco.music_library.get_sonos_favorites()

View file

@ -5,17 +5,18 @@ from collections.abc import Callable
import logging import logging
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING, TypeVar
from soco import SoCo
from soco.exceptions import SoCoException, SoCoUPnPException from soco.exceptions import SoCoException, SoCoUPnPException
from typing_extensions import Concatenate, ParamSpec from typing_extensions import Concatenate, ParamSpec
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from .const import SONOS_SPEAKER_ACTIVITY from .const import SONOS_SPEAKER_ACTIVITY
from .exception import SpeakerUnavailable from .exception import SonosUpdateError
if TYPE_CHECKING: if TYPE_CHECKING:
from .entity import SonosEntity from .entity import SonosEntity
from .household_coordinator import SonosHouseholdCoordinator
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
UID_PREFIX = "RINCON_" UID_PREFIX = "RINCON_"
@ -23,13 +24,13 @@ UID_POSTFIX = "01400"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", "SonosSpeaker", "SonosEntity") _T = TypeVar("_T", "SonosSpeaker", "SonosEntity", "SonosHouseholdCoordinator")
_R = TypeVar("_R") _R = TypeVar("_R")
_P = ParamSpec("_P") _P = ParamSpec("_P")
def soco_error( def soco_error(
errorcodes: list[str] | None = None, raise_on_err: bool = True errorcodes: list[str] | None = None,
) -> Callable[ # type: ignore[misc] ) -> Callable[ # type: ignore[misc]
[Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R | None] [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R | None]
]: ]:
@ -42,10 +43,9 @@ def soco_error(
def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None:
"""Wrap for all soco UPnP exception.""" """Wrap for all soco UPnP exception."""
args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None)
try: try:
result = funct(self, *args, **kwargs) result = funct(self, *args, **kwargs)
except SpeakerUnavailable:
return None
except (OSError, SoCoException, SoCoUPnPException) as err: except (OSError, SoCoException, SoCoUPnPException) as err:
error_code = getattr(err, "error_code", None) error_code = getattr(err, "error_code", None)
function = funct.__qualname__ function = funct.__qualname__
@ -55,20 +55,22 @@ def soco_error(
) )
return None return None
# In order of preference:
# * SonosSpeaker instance
# * SoCo instance passed as an arg
# * SoCo instance (as self)
speaker_or_soco = getattr(self, "speaker", args_soco or self)
zone_name = speaker_or_soco.zone_name
# Prefer the entity_id if available, zone name as a fallback # Prefer the entity_id if available, zone name as a fallback
# Needed as SonosSpeaker instances are not entities # Needed as SonosSpeaker instances are not entities
zone_name = getattr(self, "speaker", self).zone_name
target = getattr(self, "entity_id", zone_name) target = getattr(self, "entity_id", zone_name)
message = f"Error calling {function} on {target}: {err}" message = f"Error calling {function} on {target}: {err}"
if raise_on_err: raise SonosUpdateError(message) from err
raise HomeAssistantError(message) from err
_LOGGER.warning(message)
return None
dispatch_soco = args_soco or self.soco
dispatcher_send( dispatcher_send(
self.hass, self.hass,
f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", f"{SONOS_SPEAKER_ACTIVITY}-{dispatch_soco.uid}",
funct.__qualname__, funct.__qualname__,
) )
return result return result

View file

@ -6,12 +6,12 @@ from collections.abc import Callable, Coroutine
import logging import logging
from soco import SoCo from soco import SoCo
from soco.exceptions import SoCoException
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from .const import DATA_SONOS from .const import DATA_SONOS
from .exception import SonosUpdateError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,11 +56,10 @@ class SonosHouseholdCoordinator:
_LOGGER.debug("Polling %s using %s", self.class_type, speaker.soco) _LOGGER.debug("Polling %s using %s", self.class_type, speaker.soco)
try: try:
await self.async_update_entities(speaker.soco) await self.async_update_entities(speaker.soco)
except (OSError, SoCoException) as err: except SonosUpdateError as err:
_LOGGER.error( _LOGGER.error(
"Could not refresh %s using %s: %s", "Could not refresh %s: %s",
self.class_type, self.class_type,
speaker.soco,
err, err,
) )
else: else:

View file

@ -283,7 +283,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
return STATE_PLAYING return STATE_PLAYING
return STATE_IDLE return STATE_IDLE
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Retrieve latest state by polling.""" """Retrieve latest state by polling."""
await self.hass.data[DATA_SONOS].favorites[ await self.hass.data[DATA_SONOS].favorites[
self.speaker.household_id self.speaker.household_id

View file

@ -12,7 +12,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SONOS_CREATE_LEVELS from .const import SONOS_CREATE_LEVELS
from .entity import SonosEntity from .entity import SonosEntity
from .exception import SpeakerUnavailable
from .helpers import soco_error from .helpers import soco_error
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -75,16 +74,13 @@ class SonosLevelEntity(SonosEntity, NumberEntity):
self.level_type = level_type self.level_type = level_type
self._attr_min_value, self._attr_max_value = valid_range self._attr_min_value, self._attr_max_value = valid_range
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Poll the value if subscriptions are not working.""" """Poll the value if subscriptions are not working."""
await self.hass.async_add_executor_job(self.update) await self.hass.async_add_executor_job(self.poll_state)
@soco_error(raise_on_err=False)
def update(self) -> None:
"""Fetch number state if necessary."""
if not self.available:
raise SpeakerUnavailable
@soco_error()
def poll_state(self) -> None:
"""Poll the device for the current state."""
state = getattr(self.soco, self.level_type) state = getattr(self.soco, self.level_type)
setattr(self.speaker, self.level_type, state) setattr(self.speaker, self.level_type, state)

View file

@ -12,7 +12,8 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY from .const import SONOS_CREATE_AUDIO_FORMAT_SENSOR, SONOS_CREATE_BATTERY
from .entity import SonosEntity from .entity import SonosEntity, SonosPollingEntity
from .helpers import soco_error
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -64,7 +65,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
self._attr_unique_id = f"{self.soco.uid}-battery" self._attr_unique_id = f"{self.soco.uid}-battery"
self._attr_name = f"{self.speaker.zone_name} Battery" self._attr_name = f"{self.speaker.zone_name} Battery"
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Poll the device for the current state.""" """Poll the device for the current state."""
await self.speaker.async_poll_battery() await self.speaker.async_poll_battery()
@ -79,7 +80,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
return self.speaker.available and self.speaker.power_source return self.speaker.available and self.speaker.power_source
class SonosAudioInputFormatSensorEntity(SonosEntity, SensorEntity): class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
"""Representation of a Sonos audio import format sensor entity.""" """Representation of a Sonos audio import format sensor entity."""
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
@ -93,9 +94,10 @@ class SonosAudioInputFormatSensorEntity(SonosEntity, SensorEntity):
self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_name = f"{self.speaker.zone_name} Audio Input Format"
self._attr_native_value = audio_format self._attr_native_value = audio_format
def update(self) -> None: @soco_error()
def poll_state(self) -> None:
"""Poll the device for the current state.""" """Poll the device for the current state."""
self._attr_native_value = self.soco.soundbar_audio_input_format self._attr_native_value = self.soco.soundbar_audio_input_format
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Provide a stub for required ABC method.""" """Provide a stub for required ABC method."""

View file

@ -16,7 +16,7 @@ import defusedxml.ElementTree as ET
from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo
from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer
from soco.events_base import Event as SonosEvent, SubscriptionBase from soco.events_base import Event as SonosEvent, SubscriptionBase
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from soco.exceptions import SoCoException, SoCoUPnPException
from soco.music_library import MusicLibrary from soco.music_library import MusicLibrary
from soco.plugins.plex import PlexPlugin from soco.plugins.plex import PlexPlugin
from soco.plugins.sharelink import ShareLinkPlugin from soco.plugins.sharelink import ShareLinkPlugin
@ -50,7 +50,7 @@ from .const import (
SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_MEDIA_PLAYER,
SONOS_CREATE_MIC_SENSOR, SONOS_CREATE_MIC_SENSOR,
SONOS_CREATE_SWITCHES, SONOS_CREATE_SWITCHES,
SONOS_POLL_UPDATE, SONOS_FALLBACK_POLL,
SONOS_REBOOTED, SONOS_REBOOTED,
SONOS_SPEAKER_ACTIVITY, SONOS_SPEAKER_ACTIVITY,
SONOS_SPEAKER_ADDED, SONOS_SPEAKER_ADDED,
@ -354,7 +354,7 @@ class SonosSpeaker:
partial( partial(
async_dispatcher_send, async_dispatcher_send,
self.hass, self.hass,
f"{SONOS_POLL_UPDATE}-{self.soco.uid}", f"{SONOS_FALLBACK_POLL}-{self.soco.uid}",
), ),
SCAN_INTERVAL, SCAN_INTERVAL,
) )
@ -568,7 +568,7 @@ class SonosSpeaker:
if not self.available: if not self.available:
return return
_LOGGER.debug( _LOGGER.warning(
"No recent activity and cannot reach %s, marking unavailable", "No recent activity and cannot reach %s, marking unavailable",
self.zone_name, self.zone_name,
) )
@ -1044,18 +1044,11 @@ class SonosSpeaker:
# #
# Media and playback state handlers # Media and playback state handlers
# #
@soco_error(raise_on_err=False) @soco_error()
def update_volume(self) -> None: def update_volume(self) -> None:
"""Update information about current volume settings.""" """Update information about current volume settings."""
self.volume = self.soco.volume self.volume = self.soco.volume
self.muted = self.soco.mute self.muted = self.soco.mute
self.night_mode = self.soco.night_mode
self.dialog_level = self.soco.dialog_level
try:
self.cross_fade = self.soco.cross_fade
except SoCoSlaveException:
pass
@soco_error() @soco_error()
def update_media(self, event: SonosEvent | None = None) -> None: def update_media(self, event: SonosEvent | None = None) -> None:

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from soco.exceptions import SoCoSlaveException, SoCoUPnPException
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -22,8 +22,7 @@ from .const import (
SONOS_CREATE_ALARM, SONOS_CREATE_ALARM,
SONOS_CREATE_SWITCHES, SONOS_CREATE_SWITCHES,
) )
from .entity import SonosEntity from .entity import SonosEntity, SonosPollingEntity
from .exception import SpeakerUnavailable
from .helpers import soco_error from .helpers import soco_error
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -143,7 +142,7 @@ async def async_setup_entry(
) )
class SonosSwitchEntity(SonosEntity, SwitchEntity): class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
"""Representation of a Sonos feature switch.""" """Representation of a Sonos feature switch."""
def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None: def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None:
@ -163,17 +162,14 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity):
self._attr_entity_registry_enabled_default = False self._attr_entity_registry_enabled_default = False
self._attr_should_poll = True self._attr_should_poll = True
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Handle polling for subscription-based switches when subscription fails.""" """Handle polling for subscription-based switches when subscription fails."""
if not self.should_poll: if not self.should_poll:
await self.hass.async_add_executor_job(self.update) await self.hass.async_add_executor_job(self.poll_state)
@soco_error(raise_on_err=False)
def update(self) -> None:
"""Fetch switch state if necessary."""
if not self.available:
raise SpeakerUnavailable
@soco_error()
def poll_state(self) -> None:
"""Poll the current state of the switch."""
state = getattr(self.soco, self.feature_type) state = getattr(self.soco, self.feature_type)
setattr(self.speaker, self.feature_type, state) setattr(self.speaker, self.feature_type, state)
@ -244,7 +240,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
str(self.alarm.start_time)[0:5], str(self.alarm.start_time)[0:5],
) )
async def _async_poll(self) -> None: async def _async_fallback_poll(self) -> None:
"""Call the central alarm polling method.""" """Call the central alarm polling method."""
await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll()
@ -267,7 +263,6 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
if not self.async_check_if_available(): if not self.async_check_if_available():
return return
_LOGGER.debug("Updating alarm: %s", self.entity_id)
if self.speaker.soco.uid != self.alarm.zone.uid: if self.speaker.soco.uid != self.alarm.zone.uid:
self.speaker = self.hass.data[DATA_SONOS].discovered.get( self.speaker = self.hass.data[DATA_SONOS].discovered.get(
self.alarm.zone.uid self.alarm.zone.uid
@ -350,14 +345,11 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
"""Turn alarm switch off.""" """Turn alarm switch off."""
await self.async_handle_switch_on_off(turn_on=False) await self.async_handle_switch_on_off(turn_on=False)
@soco_error()
async def async_handle_switch_on_off(self, turn_on: bool) -> None: async def async_handle_switch_on_off(self, turn_on: bool) -> None:
"""Handle turn on/off of alarm switch.""" """Handle turn on/off of alarm switch."""
try: self.alarm.enabled = turn_on
_LOGGER.debug("Toggling the state of %s", self.entity_id) await self.hass.async_add_executor_job(self.alarm.save)
self.alarm.enabled = turn_on
await self.hass.async_add_executor_job(self.alarm.save)
except (OSError, SoCoException, SoCoUPnPException) as exc:
_LOGGER.error("Could not update %s: %s", self.entity_id, exc)
@callback @callback