From 395d58840ca3ceb057a73cd7ed8220d3710b7386 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Sun, 5 Jun 2022 13:42:21 +1000 Subject: [PATCH] Add Hunter Douglas Powerview Diagnostics (#72918) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../hunterdouglas_powerview/__init__.py | 8 +- .../hunterdouglas_powerview/diagnostics.py | 108 ++++++++++++++++++ .../hunterdouglas_powerview/shade_data.py | 4 + 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/diagnostics.py diff --git a/.coveragerc b/.coveragerc index 4cbddd14601..f4cdfa1c460 100644 --- a/.coveragerc +++ b/.coveragerc @@ -506,6 +506,7 @@ omit = homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/coordinator.py + homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hunterdouglas_powerview/scene.py diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 587bf8aef12..88aa5214c9b 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -87,9 +87,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): shades = Shades(pv_request) - shade_data = async_map_data_by_id( - (await shades.get_resources())[SHADE_DATA] - ) + shade_entries = await shades.get_resources() + shade_data = async_map_data_by_id(shade_entries[SHADE_DATA]) + except HUB_EXCEPTIONS as err: raise ConfigEntryNotReady( f"Connection error to PowerView hub: {hub_address}: {err}" @@ -99,6 +99,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub_address) coordinator.async_set_updated_data(PowerviewShadeData()) + # populate raw shade data into the coordinator for diagnostics + coordinator.data.store_group_data(shade_entries[SHADE_DATA]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { PV_API: pv_request, diff --git a/homeassistant/components/hunterdouglas_powerview/diagnostics.py b/homeassistant/components/hunterdouglas_powerview/diagnostics.py new file mode 100644 index 00000000000..1b2887c3af2 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/diagnostics.py @@ -0,0 +1,108 @@ +"""Diagnostics support for Powerview Hunter Douglas.""" +from __future__ import annotations + +from typing import Any + +import attr + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DEVICE_MAC_ADDRESS, + DEVICE_SERIAL_NUMBER, + DOMAIN, + PV_HUB_ADDRESS, +) +from .coordinator import PowerviewShadeUpdateCoordinator + +REDACT_CONFIG = { + CONF_HOST, + DEVICE_MAC_ADDRESS, + DEVICE_SERIAL_NUMBER, + PV_HUB_ADDRESS, + "configuration_url", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = _async_get_diagnostics(hass, entry) + device_registry = dr.async_get(hass) + data.update( + device_info=[ + _async_device_as_dict(hass, device) + for device in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + ], + ) + return data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + data = _async_get_diagnostics(hass, entry) + data["device_info"] = _async_device_as_dict(hass, device) + # try to match on name to restrict to shade if we can + # otherwise just return all shade data + # shade name is unique in powerview + shade_data = data["shade_data"] + for shade in shade_data: + if shade_data[shade]["name_unicode"] == device.name: + data["shade_data"] = shade_data[shade] + return data + + +@callback +def _async_get_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + pv_data = hass.data[DOMAIN][entry.entry_id] + coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] + shade_data = coordinator.data.get_all_raw_data() + hub_info = async_redact_data(pv_data[DEVICE_INFO], REDACT_CONFIG) + return {"hub_info": hub_info, "shade_data": shade_data} + + +@callback +def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str, Any]: + """Represent a Powerview device as a dictionary.""" + + # Gather information how this device is represented in Home Assistant + entity_registry = er.async_get(hass) + + data = async_redact_data(attr.asdict(device), REDACT_CONFIG) + data["entities"] = [] + entities: list[dict[str, Any]] = data["entities"] + + entries = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + for entity_entry in entries: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + state_dict.pop("context", None) + + entity = attr.asdict(entity_entry) + entity["state"] = state_dict + entities.append(entity) + + return data diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py index 4a7b7be0945..b66024aec7f 100644 --- a/homeassistant/components/hunterdouglas_powerview/shade_data.py +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -59,6 +59,10 @@ class PowerviewShadeData: """Get data for the shade.""" return self._group_data_by_id[shade_id] + def get_all_raw_data(self) -> dict[int, dict[str | int, Any]]: + """Get data for all shades.""" + return self._group_data_by_id + def get_shade_positions(self, shade_id: int) -> PowerviewShadePositions: """Get positions for a shade.""" if shade_id not in self.positions: