diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py new file mode 100644 index 00000000000..b60889358fd --- /dev/null +++ b/homeassistant/components/nest/diagnostics.py @@ -0,0 +1,51 @@ +"""Diagnostics support for Nest.""" + +from __future__ import annotations + +from typing import Any + +from google_nest_sdm.device import Device +from google_nest_sdm.device_traits import InfoTrait +from google_nest_sdm.exceptions import ApiException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_SUBSCRIBER, DOMAIN + +REDACT_DEVICE_TRAITS = {InfoTrait.NAME} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + if DATA_SUBSCRIBER not in hass.data[DOMAIN]: + return {"error": "No subscriber configured"} + + subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + try: + device_manager = await subscriber.async_get_device_manager() + except ApiException as err: + return {"error": str(err)} + + return { + "devices": [ + get_device_data(device) for device in device_manager.devices.values() + ] + } + + +def get_device_data(device: Device) -> dict[str, Any]: + """Return diagnostic information about a device.""" + # Return a simplified view of the API object, but skipping any id fields or + # traits that include unique identifiers or personally identifiable information. + # See https://developers.google.com/nest/device-access/traits for API details + return { + "type": device.type, + "traits": { + trait: data + for trait, data in device.raw_data.get("traits", {}).items() + if trait not in REDACT_DEVICE_TRAITS + }, + } diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 180821a8d9e..f61c50f686a 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -109,9 +109,12 @@ async def async_setup_sdm_platform( if structures: for structure in structures.values(): device_manager.add_structure(structure) + platforms = [] + if platform: + platforms = [platform] with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", [platform]), patch( + ), patch("homeassistant.components.nest.PLATFORMS", platforms), patch( "homeassistant.components.nest.api.GoogleNestSubscriber", return_value=subscriber, ): diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py new file mode 100644 index 00000000000..8d506dc37fe --- /dev/null +++ b/tests/components/nest/test_diagnostics.py @@ -0,0 +1,86 @@ +"""Test nest diagnostics.""" + +from unittest.mock import patch + +from google_nest_sdm.device import Device +from google_nest_sdm.exceptions import SubscriberException + +from homeassistant.components.nest import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.setup import async_setup_component + +from .common import CONFIG, async_setup_sdm_platform, create_config_entry + +from tests.components.diagnostics import get_diagnostics_for_config_entry + +THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + devices = { + "some-device-id": Device.MakeDevice( + { + "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", + } + ], + }, + auth=None, + ) + } + assert await async_setup_sdm_platform(hass, platform=None, devices=devices) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + config_entry = entries[0] + 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": [ + { + "traits": { + "sdm.devices.traits.Humidity": {"ambientHumidityPercent": 35.0}, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.1 + }, + }, + "type": "sdm.devices.types.THERMOSTAT", + } + ], + } + + +async def test_setup_susbcriber_failure(hass, hass_client): + """Test configuration error.""" + config_entry = create_config_entry(hass) + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" + ), patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", + side_effect=SubscriberException(), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "error": "No subscriber configured" + }