From 66a183cfe3ed947871ea34dabaed11be87e96766 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 29 Mar 2022 19:56:03 -0700 Subject: [PATCH] Add nest device level diagnostics (#68024) --- homeassistant/components/nest/diagnostics.py | 44 ++++-- tests/components/nest/test_diagnostics.py | 154 +++++++++++++------ 2 files changed, 136 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index 859aa834581..2840c34378b 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Any - from google_nest_sdm import diagnostics from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait @@ -11,37 +9,57 @@ from google_nest_sdm.exceptions import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN REDACT_DEVICE_TRAITS = {InfoTrait.NAME} -async def async_get_config_entry_diagnostics( +async def _get_nest_devices( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: - """Return diagnostics for a config entry.""" +) -> dict[str, Device]: + """Return dict of available devices.""" if DATA_SDM not in config_entry.data: return {} if DATA_SUBSCRIBER not in hass.data[DOMAIN]: - return {"error": "No subscriber configured"} + return {} subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + device_manager = await subscriber.async_get_device_manager() + devices: dict[str, Device] = device_manager.devices + return devices + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" try: - device_manager = await subscriber.async_get_device_manager() + nest_devices = await _get_nest_devices(hass, config_entry) except ApiException as err: return {"error": str(err)} - + if not nest_devices: + return {} return { **diagnostics.get_diagnostics(), "devices": [ - get_device_data(device) for device in device_manager.devices.values() + nest_device.get_diagnostics() for nest_device in nest_devices.values() ], } -def get_device_data(device: Device) -> dict[str, Any]: - """Return diagnostic information about a device.""" - # Library performs its own redaction for device data - return device.get_diagnostics() +async def async_get_device_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, + device: DeviceEntry, +) -> dict: + """Return diagnostics for a device.""" + try: + nest_devices = await _get_nest_devices(hass, config_entry) + except ApiException as err: + return {"error": str(err)} + nest_device_id = next(iter(device.identifiers))[1] + nest_device = nest_devices.get(nest_device_id) + return nest_device.get_diagnostics() if nest_device else {} diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index cf6c9c5b20f..becb73b0b33 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -2,73 +2,103 @@ from unittest.mock import patch -from google_nest_sdm.exceptions import SubscriberException +from google_nest_sdm.exceptions import ApiException, SubscriberException import pytest +from homeassistant.components.nest.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.helpers import device_registry as dr from .common import TEST_CONFIG_LEGACY -from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + +NEST_DEVICE_ID = "enterprises/project-id/devices/device-id" + +DEVICE_API_DATA = { + "name": NEST_DEVICE_ID, + "type": "sdm.devices.types.THERMOSTAT", + "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1, + }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 35.0, + }, + }, + "parentRelations": [ + { + "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", + "displayName": "Lobby", + } + ], +} + +DEVICE_DIAGNOSTIC_DATA = { + "data": { + "assignee": "**REDACTED**", + "name": "**REDACTED**", + "parentRelations": [{"displayName": "**REDACTED**", "parent": "**REDACTED**"}], + "traits": { + "sdm.devices.traits.Info": {"customName": "**REDACTED**"}, + "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, + "sdm.devices.traits.Temperature": {"ambientTemperatureCelsius": 25.1}, + }, + "type": "sdm.devices.types.THERMOSTAT", + } +} + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return ["sensor"] async def test_entry_diagnostics( hass, hass_client, create_device, setup_platform, config_entry ): """Test config entry diagnostics.""" - create_device.create( - raw_data={ - "name": "enterprises/project-id/devices/device-id", - "type": "sdm.devices.types.THERMOSTAT", - "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", - "traits": { - "sdm.devices.traits.Info": { - "customName": "My Sensor", - }, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1, - }, - "sdm.devices.traits.Humidity": { - "ambientHumidityPercent": 35.0, - }, - }, - "parentRelations": [ - { - "parent": "enterprises/project-id/structures/structure-id/rooms/room-id", - "displayName": "Lobby", - } - ], - } - ) + create_device.create(raw_data=DEVICE_API_DATA) await setup_platform() assert config_entry.state is ConfigEntryState.LOADED # Test that only non identifiable device information is returned assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "devices": [ - { - "data": { - "assignee": "**REDACTED**", - "name": "**REDACTED**", - "parentRelations": [ - {"displayName": "**REDACTED**", "parent": "**REDACTED**"} - ], - "traits": { - "sdm.devices.traits.Info": {"customName": "**REDACTED**"}, - "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, - "sdm.devices.traits.Temperature": { - "ambientTemperatureCelsius": 25.1 - }, - }, - "type": "sdm.devices.types.THERMOSTAT", - } - } - ], + "devices": [DEVICE_DIAGNOSTIC_DATA] } +async def test_device_diagnostics( + hass, hass_client, create_device, setup_platform, config_entry +): + """Test config entry diagnostics.""" + create_device.create(raw_data=DEVICE_API_DATA) + await setup_platform() + assert config_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) + assert device is not None + + assert ( + await get_diagnostics_for_device(hass, hass_client, config_entry, device) + == DEVICE_DIAGNOSTIC_DATA + ) + + async def test_setup_susbcriber_failure( - hass, hass_client, config_entry, setup_base_platform + hass, + hass_client, + config_entry, + setup_base_platform, ): """Test configuration error.""" with patch( @@ -81,9 +111,35 @@ async def test_setup_susbcriber_failure( assert config_entry.state is ConfigEntryState.SETUP_RETRY - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "error": "No subscriber configured" - } + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} + + +async def test_device_manager_failure( + hass, + hass_client, + config_entry, + setup_platform, + create_device, +): + """Test configuration error.""" + create_device.create(raw_data=DEVICE_API_DATA) + await setup_platform() + assert config_entry.state is ConfigEntryState.LOADED + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) + assert device is not None + + with patch( + "homeassistant.components.nest.diagnostics._get_nest_devices", + side_effect=ApiException("Device manager failure"), + ): + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == {"error": "Device manager failure"} + assert await get_diagnostics_for_device( + hass, hass_client, config_entry, device + ) == {"error": "Device manager failure"} @pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY])