Google Assistant diagnostics and synchronization (#73574)

* Add config flow import for local google assistant
* Add diagnostic with sync response
* Add button for device sync
This commit is contained in:
Joakim Plate
2022-06-27 20:24:15 +02:00
committed by GitHub
parent 320fa25a99
commit 992ceb1a09
10 changed files with 384 additions and 18 deletions
@@ -5,9 +5,10 @@ import logging
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -23,6 +24,7 @@ from .const import (
CONF_ROOM_HINT,
CONF_SECURE_DEVICES_PIN,
CONF_SERVICE_ACCOUNT,
DATA_CONFIG,
DEFAULT_EXPOSE_BY_DEFAULT,
DEFAULT_EXPOSED_DOMAINS,
DOMAIN,
@@ -37,6 +39,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_ALLOW_UNLOCK = "allow_unlock"
PLATFORMS = [Platform.BUTTON]
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
@@ -95,11 +99,48 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
if DOMAIN not in yaml_config:
return True
config = yaml_config[DOMAIN]
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CONFIG] = yaml_config[DOMAIN]
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_PROJECT_ID: yaml_config[DOMAIN][CONF_PROJECT_ID]},
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up from a config entry."""
config: ConfigType = {**hass.data[DOMAIN][DATA_CONFIG]}
if entry.source == SOURCE_IMPORT:
# if project was changed, remove entry a new will be setup
if config[CONF_PROJECT_ID] != entry.data[CONF_PROJECT_ID]:
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
return False
config.update(entry.data)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, config[CONF_PROJECT_ID])},
manufacturer="Google",
model="Google Assistant",
name=config[CONF_PROJECT_ID],
entry_type=dr.DeviceEntryType.SERVICE,
)
google_config = GoogleConfig(hass, config)
await google_config.async_initialize()
hass.data[DOMAIN][entry.entry_id] = google_config
hass.http.register_view(GoogleAssistantView(google_config))
if google_config.should_report_state:
@@ -123,4 +164,6 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@@ -0,0 +1,53 @@
"""Support for buttons."""
from __future__ import annotations
from homeassistant import config_entries
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PROJECT_ID, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN
from .http import GoogleConfig
async def async_setup_entry(
hass: HomeAssistant,
config_entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the platform."""
yaml_config: ConfigType = hass.data[DOMAIN][DATA_CONFIG]
google_config: GoogleConfig = hass.data[DOMAIN][config_entry.entry_id]
entities = []
if CONF_SERVICE_ACCOUNT in yaml_config:
entities.append(SyncButton(config_entry.data[CONF_PROJECT_ID], google_config))
async_add_entities(entities)
class SyncButton(ButtonEntity):
"""Representation of a synchronization button."""
def __init__(self, project_id: str, google_config: GoogleConfig) -> None:
"""Initialize button."""
super().__init__()
self._google_config = google_config
self._attr_entity_category = EntityCategory.DIAGNOSTIC
self._attr_unique_id = f"{project_id}_sync"
self._attr_name = "Synchronize Devices"
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, project_id)})
async def async_press(self) -> None:
"""Press the button."""
assert self._context
agent_user_id = self._google_config.get_agent_user_id(self._context)
result = await self._google_config.async_sync_entities(agent_user_id)
if result != 200:
raise HomeAssistantError(
f"Unable to sync devices with result code: {result}, check log for more info."
)
@@ -0,0 +1,19 @@
"""Config flow for google assistant component."""
from homeassistant import config_entries
from .const import CONF_PROJECT_ID, DOMAIN
class GoogleAssistantHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
async def async_step_import(self, user_input):
"""Import a config entry."""
await self.async_set_unique_id(unique_id=user_input[CONF_PROJECT_ID])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_PROJECT_ID], data=user_input
)
@@ -40,6 +40,8 @@ CONF_ROOM_HINT = "room"
CONF_SECURE_DEVICES_PIN = "secure_devices_pin"
CONF_SERVICE_ACCOUNT = "service_account"
DATA_CONFIG = "config"
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
"alarm_control_panel",
@@ -0,0 +1,41 @@
"""Diagnostics support for Hue."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.diagnostics.const import REDACTED
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN
from .http import GoogleConfig
from .smart_home import async_devices_sync_response, create_sync_response
TO_REDACT = [
"uuid",
"baseUrl",
"webhookId",
CONF_SERVICE_ACCOUNT,
CONF_SECURE_DEVICES_PIN,
CONF_API_KEY,
]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostic information."""
data = hass.data[DOMAIN]
config: GoogleConfig = data[entry.entry_id]
yaml_config: ConfigType = data[DATA_CONFIG]
devices = await async_devices_sync_response(hass, config, REDACTED)
sync = create_sync_response(REDACTED, devices)
return {
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
"yaml_config": async_redact_data(yaml_config, TO_REDACT),
"sync": async_redact_data(sync, TO_REDACT),
}
@@ -160,7 +160,9 @@ class AbstractConfig(ABC):
def get_local_webhook_id(self, agent_user_id):
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
return self._store.agent_user_ids[agent_user_id][STORE_GOOGLE_LOCAL_WEBHOOK_ID]
if data := self._store.agent_user_ids.get(agent_user_id):
return data[STORE_GOOGLE_LOCAL_WEBHOOK_ID]
return None
@abstractmethod
def get_agent_user_id(self, context):
@@ -71,6 +71,24 @@ async def _process(hass, data, message):
return {"requestId": data.request_id, "payload": result}
async def async_devices_sync_response(hass, config, agent_user_id):
"""Generate the device serialization."""
entities = async_get_entities(hass, config)
instance_uuid = await instance_id.async_get(hass)
devices = []
for entity in entities:
if not entity.should_expose():
continue
try:
devices.append(entity.sync_serialize(agent_user_id, instance_uuid))
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error serializing %s", entity.entity_id)
return devices
@HANDLERS.register("action.devices.SYNC")
async def async_devices_sync(hass, data, payload):
"""Handle action.devices.SYNC request.
@@ -86,19 +104,7 @@ async def async_devices_sync(hass, data, payload):
agent_user_id = data.config.get_agent_user_id(data.context)
await data.config.async_connect_agent_user(agent_user_id)
entities = async_get_entities(hass, data.config)
instance_uuid = await instance_id.async_get(hass)
devices = []
for entity in entities:
if not entity.should_expose():
continue
try:
devices.append(entity.sync_serialize(agent_user_id, instance_uuid))
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error serializing %s", entity.entity_id)
devices = await async_devices_sync_response(hass, data.config, agent_user_id)
response = create_sync_response(agent_user_id, devices)
_LOGGER.debug("Syncing entities response: %s", response)