Improve Sonos polling (#51170)

* Improve Sonos polling

Warn user if polling is being used
Provide callback IP:port to help user fix networking
Fix radio handling when polling (no event payload)
Clarify dispatch target to reflect polling action

* Lint

* Revert method removal
This commit is contained in:
jjlawren 2021-05-28 05:07:58 -05:00 committed by GitHub
parent e45196f9c9
commit 39e62f9c90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 35 additions and 17 deletions

View File

@ -1,7 +1,6 @@
"""Entity representing a Sonos power sensor.""" """Entity representing a Sonos power sensor."""
from __future__ import annotations from __future__ import annotations
import datetime
import logging import logging
from typing import Any from typing import Any
@ -50,7 +49,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity):
"""Return the entity's device class.""" """Return the entity's device class."""
return DEVICE_CLASS_BATTERY_CHARGING return DEVICE_CLASS_BATTERY_CHARGING
async def async_update(self, now: datetime.datetime | None = None) -> None: async def async_update(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()

View File

@ -136,7 +136,7 @@ SONOS_CREATE_ALARM = "sonos_create_alarm"
SONOS_CREATE_BATTERY = "sonos_create_battery" SONOS_CREATE_BATTERY = "sonos_create_battery"
SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_ENTITY_CREATED = "sonos_entity_created"
SONOS_ENTITY_UPDATE = "sonos_entity_update" SONOS_POLL_UPDATE = "sonos_poll_update"
SONOS_GROUP_UPDATE = "sonos_group_update" SONOS_GROUP_UPDATE = "sonos_group_update"
SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated" SONOS_HOUSEHOLD_UPDATED = "sonos_household_updated"
SONOS_ALARM_UPDATE = "sonos_alarm_update" SONOS_ALARM_UPDATE = "sonos_alarm_update"

View File

@ -1,6 +1,7 @@
"""Entity representing a Sonos player.""" """Entity representing a Sonos player."""
from __future__ import annotations from __future__ import annotations
import datetime
import logging import logging
from pysonos.core import SoCo from pysonos.core import SoCo
@ -15,8 +16,8 @@ from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import ( from .const import (
DOMAIN, DOMAIN,
SONOS_ENTITY_CREATED, SONOS_ENTITY_CREATED,
SONOS_ENTITY_UPDATE,
SONOS_HOUSEHOLD_UPDATED, SONOS_HOUSEHOLD_UPDATED,
SONOS_POLL_UPDATE,
SONOS_STATE_UPDATED, SONOS_STATE_UPDATED,
) )
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -38,8 +39,8 @@ class SonosEntity(Entity):
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
self.async_update, # pylint: disable=no-member self.async_poll,
) )
) )
self.async_on_remove( self.async_on_remove(
@ -60,6 +61,17 @@ class SonosEntity(Entity):
self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain self.hass, f"{SONOS_ENTITY_CREATED}-{self.soco.uid}", self.platform.domain
) )
async def async_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail."""
if self.speaker.is_first_poll:
_LOGGER.warning(
"%s cannot reach [%s], falling back to polling, functionality may be limited",
self.speaker.zone_name,
self.speaker.subscription_address,
)
self.speaker.is_first_poll = False
await self.async_update() # pylint: disable=no-member
@property @property
def soco(self) -> SoCo: def soco(self) -> SoCo:
"""Return the speaker SoCo instance.""" """Return the speaker SoCo instance."""

View File

@ -293,13 +293,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
return STATE_PLAYING return STATE_PLAYING
return STATE_IDLE return STATE_IDLE
async def async_update(self, now: datetime.datetime | None = None) -> None: async def async_update(self) -> None:
"""Retrieve latest state.""" """Retrieve latest state."""
await self.hass.async_add_executor_job(self._update, now) await self.hass.async_add_executor_job(self._update)
def _update(self, now: datetime.datetime | None = None) -> None: def _update(self) -> None:
"""Retrieve latest state.""" """Retrieve latest state."""
_LOGGER.debug("Polling speaker %s", self.speaker.zone_name)
try: try:
self.speaker.update_groups() self.speaker.update_groups()
self.speaker.update_volume() self.speaker.update_volume()

View File

@ -1,7 +1,6 @@
"""Entity representing a Sonos battery level.""" """Entity representing a Sonos battery level."""
from __future__ import annotations from __future__ import annotations
import datetime
import logging import logging
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
@ -50,7 +49,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
"""Get the unit of measurement.""" """Get the unit of measurement."""
return PERCENTAGE return PERCENTAGE
async def async_update(self, now: datetime.datetime | None = None) -> None: async def async_update(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()

View File

@ -44,8 +44,8 @@ from .const import (
SONOS_CREATE_BATTERY, SONOS_CREATE_BATTERY,
SONOS_CREATE_MEDIA_PLAYER, SONOS_CREATE_MEDIA_PLAYER,
SONOS_ENTITY_CREATED, SONOS_ENTITY_CREATED,
SONOS_ENTITY_UPDATE,
SONOS_GROUP_UPDATE, SONOS_GROUP_UPDATE,
SONOS_POLL_UPDATE,
SONOS_SEEN, SONOS_SEEN,
SONOS_STATE_PLAYING, SONOS_STATE_PLAYING,
SONOS_STATE_TRANSITIONING, SONOS_STATE_TRANSITIONING,
@ -138,6 +138,7 @@ class SonosSpeaker:
self.household_id: str = soco.household_id self.household_id: str = soco.household_id
self.media = SonosMedia(soco) self.media = SonosMedia(soco)
self.is_first_poll: bool = True
self._is_ready: bool = False self._is_ready: bool = False
self._subscriptions: list[SubscriptionBase] = [] self._subscriptions: list[SubscriptionBase] = []
self._resubscription_lock: asyncio.Lock | None = None self._resubscription_lock: asyncio.Lock | None = None
@ -322,7 +323,7 @@ class SonosSpeaker:
partial( partial(
async_dispatcher_send, async_dispatcher_send,
self.hass, self.hass,
f"{SONOS_ENTITY_UPDATE}-{self.soco.uid}", f"{SONOS_POLL_UPDATE}-{self.soco.uid}",
), ),
SCAN_INTERVAL, SCAN_INTERVAL,
) )
@ -418,7 +419,7 @@ class SonosSpeaker:
): ):
async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) async_dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms)
async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE, self) async_dispatcher_send(self.hass, SONOS_ALARM_UPDATE)
async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None: async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None:
"""Update battery info using the decoded SonosEvent.""" """Update battery info using the decoded SonosEvent."""
@ -875,7 +876,7 @@ class SonosSpeaker:
if not self.media.artist: if not self.media.artist:
try: try:
self.media.artist = variables["current_track_meta_data"].creator self.media.artist = variables["current_track_meta_data"].creator
except (KeyError, AttributeError): except (TypeError, KeyError, AttributeError):
pass pass
# Radios without tagging can have part of the radio URI as title. # Radios without tagging can have part of the radio URI as title.
@ -948,3 +949,11 @@ class SonosSpeaker:
elif update_media_position: elif update_media_position:
self.media.position = current_position self.media.position = current_position
self.media.position_updated_at = dt_util.utcnow() self.media.position_updated_at = dt_util.utcnow()
@property
def subscription_address(self) -> str | None:
"""Return the current subscription callback address if any."""
if self._subscriptions:
addr, port = self._subscriptions[0].event_listener.address
return ":".join([addr, str(port)])
return None

View File

@ -112,7 +112,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
return False return False
async def async_update(self, now: datetime.datetime | None = None) -> None: async def async_update(self) -> None:
"""Poll the device for the current state.""" """Poll the device for the current state."""
if await self.async_check_if_available(): if await self.async_check_if_available():
await self.hass.async_add_executor_job(self.update_alarm) await self.hass.async_add_executor_job(self.update_alarm)