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:
parent
4efebcb86c
commit
a7fd477c64
14 changed files with 89 additions and 85 deletions
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -7,5 +7,5 @@ class UnknownMediaType(BrowseError):
|
||||||
"""Unknown media type."""
|
"""Unknown media type."""
|
||||||
|
|
||||||
|
|
||||||
class SpeakerUnavailable(HomeAssistantError):
|
class SonosUpdateError(HomeAssistantError):
|
||||||
"""Speaker is unavailable."""
|
"""Update failed."""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue