From 53aed22d5caa23d946390ae45d9025861d42dedb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 20 Jan 2022 15:26:54 -0600 Subject: [PATCH] Add diagnostics support to Sonos (#64576) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + homeassistant/components/sonos/diagnostics.py | 105 ++++++++++++++++++ homeassistant/components/sonos/speaker.py | 7 ++ 3 files changed, 113 insertions(+) create mode 100644 homeassistant/components/sonos/diagnostics.py diff --git a/.coveragerc b/.coveragerc index 1ffb47ec832..3bce0f06030 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1044,6 +1044,7 @@ omit = homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/__init__.py homeassistant/components/sonos/alarms.py + homeassistant/components/sonos/diagnostics.py homeassistant/components/sonos/entity.py homeassistant/components/sonos/favorites.py homeassistant/components/sonos/helpers.py diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py new file mode 100644 index 00000000000..e2949b8689c --- /dev/null +++ b/homeassistant/components/sonos/diagnostics.py @@ -0,0 +1,105 @@ +"""Provides diagnostics for Sonos.""" +from __future__ import annotations + +import time +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_SONOS +from .speaker import SonosSpeaker + +MEDIA_DIAGNOSTIC_ATTRIBUTES = ( + "album_name", + "artist", + "channel", + "duration", + "image_url", + "queue_position", + "playlist_name", + "source_name", + "title", + "uri", + "_last_event_variables", +) +SPEAKER_DIAGNOSTIC_ATTRIBUTES = ( + "available", + "battery_info", + "household_id", + "is_coordinator", + "model_name", + "sonos_group_entities", + "subscription_address", + "subscriptions_failed", + "zone_name", + "_group_members_missing", + "_last_activity", +) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + payload = {"current_timestamp": time.monotonic()} + + for section in ("discovered", "discovery_known", "discovery_ignored"): + payload[section] = {} + data = getattr(hass.data[DATA_SONOS], section) + if isinstance(data, set): + payload[section] = data + continue + for key, value in data.items(): + if isinstance(value, SonosSpeaker): + speaker_info = await async_generate_speaker_info(hass, value) + payload[section][key] = speaker_info + else: + payload[section][key] = value + + return payload + + +async def async_generate_media_info( + hass: HomeAssistant, speaker: SonosSpeaker +) -> dict[str, Any]: + """Generate a diagnostic payload for current media metadata.""" + payload = {} + + def get_contents(item): + if isinstance(item, (int, float, str)): + return item + if isinstance(item, dict): + payload = {} + for key, value in item.items(): + payload[key] = get_contents(value) + return payload + if hasattr(item, "__dict__"): + return vars(item) + return item + + for attrib in MEDIA_DIAGNOSTIC_ATTRIBUTES: + value = getattr(speaker.media, attrib) + payload[attrib] = get_contents(value) + + def poll_current_track_info(): + return speaker.soco.avTransport.GetPositionInfo( + [("InstanceID", 0), ("Channel", "Master")] + ) + + payload["current_track_poll"] = await hass.async_add_executor_job( + poll_current_track_info + ) + + return payload + + +async def async_generate_speaker_info( + hass: HomeAssistant, speaker: SonosSpeaker +) -> dict[str, Any]: + """Generate the diagnostic payload for a specific speaker.""" + payload = {} + for attrib in SPEAKER_DIAGNOSTIC_ATTRIBUTES: + payload[attrib] = getattr(speaker, attrib) + payload["media"] = await async_generate_media_info(hass, speaker) + return payload diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4c1f53f49d0..cf829a890e0 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -123,6 +123,7 @@ class SonosMedia: self.position: float | None = None self.position_updated_at: datetime.datetime | None = None + self._last_event_variables: dict[str, Any] | None = None def clear(self) -> None: """Clear basic media info.""" @@ -1002,6 +1003,12 @@ class SonosSpeaker: if new_status == SONOS_STATE_TRANSITIONING: return + if variables: + # Store for diagnostics + self.media._last_event_variables = ( # pylint: disable=protected-access + variables + ) + self.media.clear() update_position = new_status != self.media.playback_status self.media.playback_status = new_status