Rework Sonos battery and ping activity tracking (#70942)

This commit is contained in:
jjlawren 2022-05-14 13:40:26 -05:00 committed by GitHub
parent 355445db2d
commit 532b3d780f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 42 deletions

View File

@ -40,6 +40,7 @@ from .const import (
SONOS_VANISHED, SONOS_VANISHED,
UPNP_ST, UPNP_ST,
) )
from .exception import SonosUpdateError
from .favorites import SonosFavorites from .favorites import SonosFavorites
from .speaker import SonosSpeaker from .speaker import SonosSpeaker
@ -264,19 +265,11 @@ class SonosDiscoveryManager:
self._create_visible_speakers(ip_addr) self._create_visible_speakers(ip_addr)
elif not known_speaker.available: elif not known_speaker.available:
try: try:
known_speaker.soco.renderingControl.GetVolume( known_speaker.ping()
[("InstanceID", 0), ("Channel", "Master")], timeout=1 except SonosUpdateError:
)
except OSError:
_LOGGER.debug( _LOGGER.debug(
"Manual poll to %s failed, keeping unavailable", ip_addr "Manual poll to %s failed, keeping unavailable", ip_addr
) )
else:
dispatcher_send(
self.hass,
f"{SONOS_SPEAKER_ACTIVITY}-{known_speaker.uid}",
"manual rediscovery",
)
self.data.hosts_heartbeat = call_later( self.data.hosts_heartbeat = call_later(
self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts

View File

@ -9,3 +9,7 @@ class UnknownMediaType(BrowseError):
class SonosUpdateError(HomeAssistantError): class SonosUpdateError(HomeAssistantError):
"""Update failed.""" """Update failed."""
class S1BatteryMissing(SonosUpdateError):
"""Battery update failed on S1 firmware."""

View File

@ -57,6 +57,7 @@ from .const import (
SONOS_VANISHED, SONOS_VANISHED,
SUBSCRIPTION_TIMEOUT, SUBSCRIPTION_TIMEOUT,
) )
from .exception import S1BatteryMissing, SonosUpdateError
from .favorites import SonosFavorites from .favorites import SonosFavorites
from .helpers import soco_error from .helpers import soco_error
from .media import SonosMedia from .media import SonosMedia
@ -83,16 +84,6 @@ UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None:
"""Fetch battery_info from the given SoCo object.
Returns None if the device doesn't support battery info
or if the device is offline.
"""
with contextlib.suppress(ConnectionError, TimeoutError, SoCoException):
return soco.get_battery_info()
class SonosSpeaker: class SonosSpeaker:
"""Representation of a Sonos speaker.""" """Representation of a Sonos speaker."""
@ -207,8 +198,11 @@ class SonosSpeaker:
self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format
) )
if battery_info := fetch_battery_info_or_none(self.soco): try:
self.battery_info = battery_info self.battery_info = self.fetch_battery_info()
except SonosUpdateError:
_LOGGER.debug("No battery available for %s", self.zone_name)
else:
# Battery events can be infrequent, polling is still necessary # Battery events can be infrequent, polling is still necessary
self._battery_poll_timer = track_time_interval( self._battery_poll_timer = track_time_interval(
self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL
@ -530,6 +524,13 @@ class SonosSpeaker:
# #
# Speaker availability methods # Speaker availability methods
# #
@soco_error()
def ping(self) -> None:
"""Test device availability. Failure will raise SonosUpdateError."""
self.soco.renderingControl.GetVolume(
[("InstanceID", 0), ("Channel", "Master")], timeout=1
)
@callback @callback
def speaker_activity(self, source): def speaker_activity(self, source):
"""Track the last activity on this speaker, set availability and resubscribe.""" """Track the last activity on this speaker, set availability and resubscribe."""
@ -560,23 +561,13 @@ class SonosSpeaker:
return return
try: try:
# Make a short-timeout call as a final check await self.hass.async_add_executor_job(self.ping)
# before marking this speaker as unavailable except SonosUpdateError:
await self.hass.async_add_executor_job(
partial(
self.soco.renderingControl.GetVolume,
[("InstanceID", 0), ("Channel", "Master")],
timeout=1,
)
)
except OSError:
_LOGGER.warning( _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,
) )
await self.async_offline() await self.async_offline()
else:
self.speaker_activity("timeout poll")
async def async_offline(self) -> None: async def async_offline(self) -> None:
"""Handle removal of speaker when unavailable.""" """Handle removal of speaker when unavailable."""
@ -619,6 +610,15 @@ class SonosSpeaker:
# #
# Battery management # Battery management
# #
@soco_error()
def fetch_battery_info(self) -> dict[str, Any]:
"""Fetch battery_info for the speaker."""
battery_info = self.soco.get_battery_info()
if not battery_info:
# S1 firmware returns an empty payload
raise S1BatteryMissing
return battery_info
async def async_update_battery_info(self, more_info: str) -> None: async def async_update_battery_info(self, more_info: str) -> None:
"""Update battery info using a SonosEvent payload value.""" """Update battery info using a SonosEvent payload value."""
battery_dict = dict(x.split(":") for x in more_info.split(",")) battery_dict = dict(x.split(":") for x in more_info.split(","))
@ -658,11 +658,17 @@ class SonosSpeaker:
if is_charging == self.charging: if is_charging == self.charging:
self.battery_info.update({"Level": int(battery_dict["BattPct"])}) self.battery_info.update({"Level": int(battery_dict["BattPct"])})
elif not is_charging:
# Avoid polling the speaker if possible
self.battery_info["PowerSource"] = "BATTERY"
else: else:
if battery_info := await self.hass.async_add_executor_job( # Poll to obtain current power source not provided by event
fetch_battery_info_or_none, self.soco try:
): self.battery_info = await self.hass.async_add_executor_job(
self.battery_info = battery_info self.fetch_battery_info
)
except SonosUpdateError as err:
_LOGGER.debug("Could not request current power source: %s", err)
@property @property
def power_source(self) -> str | None: def power_source(self) -> str | None:
@ -692,10 +698,13 @@ class SonosSpeaker:
): ):
return return
if battery_info := await self.hass.async_add_executor_job( try:
fetch_battery_info_or_none, self.soco self.battery_info = await self.hass.async_add_executor_job(
): self.fetch_battery_info
self.battery_info = battery_info )
except SonosUpdateError as err:
_LOGGER.debug("Could not poll battery info: %s", err)
else:
self.async_write_entity_states() self.async_write_entity_states()
# #