Set the nest configuration title to a user friendly name (#62886)

* Set the nest configuration title to a user friendly name

Set the config entry title name to be the name of the Google Home or
Nest Home that was authorized. In case more than one home was authorized,
they are all listed since they are all accessed over a single shared
home.

* Fix pylint errors

* Resolve pylint errors
This commit is contained in:
Allen Porter 2022-01-04 19:23:20 -08:00 committed by GitHub
parent 5c44c27088
commit 224f960050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 193 additions and 3 deletions

View File

@ -27,6 +27,7 @@ from __future__ import annotations
import asyncio
from collections import OrderedDict
from collections.abc import Iterable
from enum import Enum
import logging
import os
@ -34,10 +35,12 @@ from typing import Any
import async_timeout
from google_nest_sdm.exceptions import (
ApiException,
AuthException,
ConfigurationException,
SubscriberException,
)
from google_nest_sdm.structure import InfoTrait, Structure
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
@ -128,6 +131,17 @@ class UnexpectedStateError(HomeAssistantError):
"""Raised when the config flow is invoked in a 'should not happen' case."""
def generate_config_title(structures: Iterable[Structure]) -> str | None:
"""Pick a user friendly config title based on the Google Home name(s)."""
names: list[str] = []
for structure in structures:
if (trait := structure.traits.get(InfoTrait.NAME)) and trait.custom_name:
names.append(trait.custom_name)
if not names:
return None
return ", ".join(names)
class NestFlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
@ -141,6 +155,8 @@ class NestFlowHandler(
super().__init__()
self._reauth = False
self._data: dict[str, Any] = {DATA_SDM: {}}
# Possible name to use for config entry based on the Google Home name
self._structure_config_title: str | None = None
@property
def config_mode(self) -> ConfigMode:
@ -298,8 +314,18 @@ class NestFlowHandler(
except SubscriberException as err:
_LOGGER.error("Error creating subscription: %s", err)
errors[CONF_CLOUD_PROJECT_ID] = "subscriber_error"
if not errors:
try:
device_manager = await subscriber.async_get_device_manager()
except ApiException as err:
# Generating a user friendly home name is best effort
_LOGGER.debug("Error fetching structures: %s", err)
else:
self._structure_config_title = generate_config_title(
device_manager.structures.values()
)
self._data.update(
{
CONF_SUBSCRIBER_ID: subscriber_id,
@ -339,7 +365,10 @@ class NestFlowHandler(
)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful")
return await super().async_oauth_create_entry(self._data)
title = self.flow_impl.name
if self._structure_config_title:
title = self._structure_config_title
return self.async_create_entry(title=title, data=self._data)
async def async_step_init(
self, user_input: dict[str, Any] | None = None

View File

@ -1,13 +1,14 @@
"""Test the Google Nest Device Access config flow."""
import copy
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from google_nest_sdm.exceptions import (
AuthException,
ConfigurationException,
SubscriberException,
)
from google_nest_sdm.structure import Structure
import pytest
from homeassistant import config_entries, setup
@ -562,3 +563,163 @@ async def test_pubsub_subscriber_config_entry_reauth(hass, oauth, subscriber):
assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN
assert entry.data["subscriber_id"] == SUBSCRIBER_ID
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
async def test_config_entry_title_from_home(hass, oauth, subscriber):
"""Test that the Google Home name is used for the config entry title."""
device_manager = await subscriber.async_get_device_manager()
device_manager.add_structure(
Structure.MakeStructure(
{
"name": f"enterprise/{PROJECT_ID}/structures/some-structure-id",
"traits": {
"sdm.structures.traits.Info": {
"customName": "Example Home",
},
},
}
)
)
assert await async_setup_configflow(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN)
await oauth.async_oauth_app_flow(result)
with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
):
result = await oauth.async_configure(result, {"code": "1234"})
await oauth.async_pubsub_flow(result)
entry = await oauth.async_finish_setup(
result, {"cloud_project_id": CLOUD_PROJECT_ID}
)
await hass.async_block_till_done()
assert entry.title == "Example Home"
assert "token" in entry.data
assert "subscriber_id" in entry.data
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
async def test_config_entry_title_multiple_homes(hass, oauth, subscriber):
"""Test handling of multiple Google Homes authorized."""
device_manager = await subscriber.async_get_device_manager()
device_manager.add_structure(
Structure.MakeStructure(
{
"name": f"enterprise/{PROJECT_ID}/structures/id-1",
"traits": {
"sdm.structures.traits.Info": {
"customName": "Example Home #1",
},
},
}
)
)
device_manager.add_structure(
Structure.MakeStructure(
{
"name": f"enterprise/{PROJECT_ID}/structures/id-2",
"traits": {
"sdm.structures.traits.Info": {
"customName": "Example Home #2",
},
},
}
)
)
assert await async_setup_configflow(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN)
await oauth.async_oauth_app_flow(result)
with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
):
result = await oauth.async_configure(result, {"code": "1234"})
await oauth.async_pubsub_flow(result)
entry = await oauth.async_finish_setup(
result, {"cloud_project_id": CLOUD_PROJECT_ID}
)
await hass.async_block_till_done()
assert entry.title == "Example Home #1, Example Home #2"
async def test_title_failure_fallback(hass, oauth):
"""Test exception handling when determining the structure names."""
assert await async_setup_configflow(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN)
await oauth.async_oauth_app_flow(result)
mock_subscriber = AsyncMock(FakeSubscriber)
mock_subscriber.async_get_device_manager.side_effect = AuthException()
with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=mock_subscriber,
):
result = await oauth.async_configure(result, {"code": "1234"})
await oauth.async_pubsub_flow(result)
entry = await oauth.async_finish_setup(
result, {"cloud_project_id": CLOUD_PROJECT_ID}
)
await hass.async_block_till_done()
assert entry.title == "OAuth for Apps"
assert "token" in entry.data
assert "subscriber_id" in entry.data
assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID
async def test_structure_missing_trait(hass, oauth, subscriber):
"""Test handling the case where a structure has no name set."""
device_manager = await subscriber.async_get_device_manager()
device_manager.add_structure(
Structure.MakeStructure(
{
"name": f"enterprise/{PROJECT_ID}/structures/id-1",
# Missing Info trait
"traits": {},
}
)
)
assert await async_setup_configflow(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN)
await oauth.async_oauth_app_flow(result)
with patch(
"homeassistant.components.nest.api.GoogleNestSubscriber",
return_value=subscriber,
):
result = await oauth.async_configure(result, {"code": "1234"})
await oauth.async_pubsub_flow(result)
entry = await oauth.async_finish_setup(
result, {"cloud_project_id": CLOUD_PROJECT_ID}
)
await hass.async_block_till_done()
# Fallback to default name
assert entry.title == "OAuth for Apps"