1
mirror of https://github.com/home-assistant/core synced 2024-09-03 08:14:07 +02:00

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,
UPNP_ST,
)
from .exception import SonosUpdateError
from .favorites import SonosFavorites
from .speaker import SonosSpeaker
@ -264,19 +265,11 @@ class SonosDiscoveryManager:
self._create_visible_speakers(ip_addr)
elif not known_speaker.available:
try:
known_speaker.soco.renderingControl.GetVolume(
[("InstanceID", 0), ("Channel", "Master")], timeout=1
)
except OSError:
known_speaker.ping()
except SonosUpdateError:
_LOGGER.debug(
"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.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts

View File

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

View File

@ -57,6 +57,7 @@ from .const import (
SONOS_VANISHED,
SUBSCRIPTION_TIMEOUT,
)
from .exception import S1BatteryMissing, SonosUpdateError
from .favorites import SonosFavorites
from .helpers import soco_error
from .media import SonosMedia
@ -83,16 +84,6 @@ UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"]
_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:
"""Representation of a Sonos speaker."""
@ -207,8 +198,11 @@ class SonosSpeaker:
self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format
)
if battery_info := fetch_battery_info_or_none(self.soco):
self.battery_info = battery_info
try:
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
self._battery_poll_timer = track_time_interval(
self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL
@ -530,6 +524,13 @@ class SonosSpeaker:
#
# 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
def speaker_activity(self, source):
"""Track the last activity on this speaker, set availability and resubscribe."""
@ -560,23 +561,13 @@ class SonosSpeaker:
return
try:
# Make a short-timeout call as a final check
# before marking this speaker as unavailable
await self.hass.async_add_executor_job(
partial(
self.soco.renderingControl.GetVolume,
[("InstanceID", 0), ("Channel", "Master")],
timeout=1,
)
)
except OSError:
await self.hass.async_add_executor_job(self.ping)
except SonosUpdateError:
_LOGGER.warning(
"No recent activity and cannot reach %s, marking unavailable",
self.zone_name,
)
await self.async_offline()
else:
self.speaker_activity("timeout poll")
async def async_offline(self) -> None:
"""Handle removal of speaker when unavailable."""
@ -619,6 +610,15 @@ class SonosSpeaker:
#
# 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:
"""Update battery info using a SonosEvent payload value."""
battery_dict = dict(x.split(":") for x in more_info.split(","))
@ -658,11 +658,17 @@ class SonosSpeaker:
if is_charging == self.charging:
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:
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self.battery_info = battery_info
# Poll to obtain current power source not provided by event
try:
self.battery_info = await self.hass.async_add_executor_job(
self.fetch_battery_info
)
except SonosUpdateError as err:
_LOGGER.debug("Could not request current power source: %s", err)
@property
def power_source(self) -> str | None:
@ -692,10 +698,13 @@ class SonosSpeaker:
):
return
if battery_info := await self.hass.async_add_executor_job(
fetch_battery_info_or_none, self.soco
):
self.battery_info = battery_info
try:
self.battery_info = await self.hass.async_add_executor_job(
self.fetch_battery_info
)
except SonosUpdateError as err:
_LOGGER.debug("Could not poll battery info: %s", err)
else:
self.async_write_entity_states()
#