mirror of
https://github.com/elisspace/core.git
synced 2026-10-09 02:21:08 +00:00
* Add support for Installed Auth authentication flows. Add support for additional credential types to make configuration simpler for end users. The existing Web App auth flow requires users to configure redirect urls with Google that has a very high security bar: requires ssl, and a publicly resolvable dns name. The new Installed App flow requires the user to copy/paste an access code and is the same flow used by the `google` calendar integration. This also allows us to let users create one authentication credential to use with multiple google integrations. * Remove hard migration for nest config entries, using soft migration * Add comment explaining soft migration * Revet changes to common.py made obsolete by removing migration * Reduce unnecessary diffs in nest common.py * Update config entries using library method * Run `python3 -m script.translations develop` * Revert nest auth domain * Remove compat function which is no longer needed * Remove stale nest comment * Adjust typing for python3.8 * Address PR feedback for nest auth revamp
270 lines
8.4 KiB
Python
270 lines
8.4 KiB
Python
"""Support for Nest devices."""
|
|
|
|
import logging
|
|
|
|
from google_nest_sdm.event import EventMessage
|
|
from google_nest_sdm.exceptions import (
|
|
AuthException,
|
|
ConfigurationException,
|
|
GoogleNestException,
|
|
)
|
|
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_BINARY_SENSORS,
|
|
CONF_CLIENT_ID,
|
|
CONF_CLIENT_SECRET,
|
|
CONF_MONITORED_CONDITIONS,
|
|
CONF_SENSORS,
|
|
CONF_STRUCTURE,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
from homeassistant.helpers import (
|
|
aiohttp_client,
|
|
config_entry_oauth2_flow,
|
|
config_validation as cv,
|
|
)
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from . import api, config_flow
|
|
from .const import (
|
|
DATA_SDM,
|
|
DATA_SUBSCRIBER,
|
|
DOMAIN,
|
|
OAUTH2_AUTHORIZE,
|
|
OAUTH2_TOKEN,
|
|
OOB_REDIRECT_URI,
|
|
)
|
|
from .events import EVENT_NAME_MAP, NEST_EVENT
|
|
from .legacy import async_setup_legacy, async_setup_legacy_entry
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_PROJECT_ID = "project_id"
|
|
CONF_SUBSCRIBER_ID = "subscriber_id"
|
|
DATA_NEST_CONFIG = "nest_config"
|
|
DATA_NEST_UNAVAILABLE = "nest_unavailable"
|
|
|
|
NEST_SETUP_NOTIFICATION = "nest_setup"
|
|
|
|
SENSOR_SCHEMA = vol.Schema(
|
|
{vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)}
|
|
)
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
|
# Required to use the new API (optional for compatibility)
|
|
vol.Optional(CONF_PROJECT_ID): cv.string,
|
|
vol.Optional(CONF_SUBSCRIBER_ID): cv.string,
|
|
# Config that only currently works on the old API
|
|
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, [cv.string]),
|
|
vol.Optional(CONF_SENSORS): SENSOR_SCHEMA,
|
|
vol.Optional(CONF_BINARY_SENSORS): SENSOR_SCHEMA,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
# Platforms for SDM API
|
|
PLATFORMS = ["sensor", "camera", "climate"]
|
|
WEB_AUTH_DOMAIN = DOMAIN
|
|
INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed"
|
|
|
|
|
|
class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
|
|
"""OAuth implementation using OAuth for web applications."""
|
|
|
|
name = "OAuth for Web"
|
|
|
|
def __init__(
|
|
self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str
|
|
) -> None:
|
|
"""Initialize WebAuth."""
|
|
super().__init__(
|
|
hass,
|
|
WEB_AUTH_DOMAIN,
|
|
client_id,
|
|
client_secret,
|
|
OAUTH2_AUTHORIZE.format(project_id=project_id),
|
|
OAUTH2_TOKEN,
|
|
)
|
|
|
|
|
|
class InstalledAppAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
|
|
"""OAuth implementation using OAuth for installed applications."""
|
|
|
|
name = "OAuth for Apps"
|
|
|
|
def __init__(
|
|
self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str
|
|
) -> None:
|
|
"""Initialize InstalledAppAuth."""
|
|
super().__init__(
|
|
hass,
|
|
INSTALLED_AUTH_DOMAIN,
|
|
client_id,
|
|
client_secret,
|
|
OAUTH2_AUTHORIZE.format(project_id=project_id),
|
|
OAUTH2_TOKEN,
|
|
)
|
|
|
|
@property
|
|
def redirect_uri(self) -> str:
|
|
"""Return the redirect uri."""
|
|
return OOB_REDIRECT_URI
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
"""Set up Nest components with dispatch between old/new flows."""
|
|
hass.data[DOMAIN] = {}
|
|
|
|
if DOMAIN not in config:
|
|
return True
|
|
|
|
if CONF_PROJECT_ID not in config[DOMAIN]:
|
|
return await async_setup_legacy(hass, config)
|
|
|
|
if CONF_SUBSCRIBER_ID not in config[DOMAIN]:
|
|
_LOGGER.error("Configuration option 'subscriber_id' required")
|
|
return False
|
|
|
|
# For setup of ConfigEntry below
|
|
hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN]
|
|
project_id = config[DOMAIN][CONF_PROJECT_ID]
|
|
config_flow.NestFlowHandler.register_sdm_api(hass)
|
|
config_flow.NestFlowHandler.async_register_implementation(
|
|
hass,
|
|
InstalledAppAuth(
|
|
hass,
|
|
config[DOMAIN][CONF_CLIENT_ID],
|
|
config[DOMAIN][CONF_CLIENT_SECRET],
|
|
project_id,
|
|
),
|
|
)
|
|
config_flow.NestFlowHandler.async_register_implementation(
|
|
hass,
|
|
WebAuth(
|
|
hass,
|
|
config[DOMAIN][CONF_CLIENT_ID],
|
|
config[DOMAIN][CONF_CLIENT_SECRET],
|
|
project_id,
|
|
),
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
class SignalUpdateCallback:
|
|
"""An EventCallback invoked when new events arrive from subscriber."""
|
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
|
"""Initialize EventCallback."""
|
|
self._hass = hass
|
|
|
|
async def async_handle_event(self, event_message: EventMessage) -> None:
|
|
"""Process an incoming EventMessage."""
|
|
if not event_message.resource_update_name:
|
|
return
|
|
device_id = event_message.resource_update_name
|
|
if not (events := event_message.resource_update_events):
|
|
return
|
|
_LOGGER.debug("Event Update %s", events.keys())
|
|
device_registry = await self._hass.helpers.device_registry.async_get_registry()
|
|
device_entry = device_registry.async_get_device({(DOMAIN, device_id)})
|
|
if not device_entry:
|
|
return
|
|
for api_event_type, image_event in events.items():
|
|
if not (event_type := EVENT_NAME_MAP.get(api_event_type)):
|
|
continue
|
|
message = {
|
|
"device_id": device_entry.id,
|
|
"type": event_type,
|
|
"timestamp": event_message.timestamp,
|
|
"nest_event_id": image_event.event_id,
|
|
}
|
|
self._hass.bus.async_fire(NEST_EVENT, message)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up Nest from a config entry with dispatch between old/new flows."""
|
|
|
|
if DATA_SDM not in entry.data:
|
|
return await async_setup_legacy_entry(hass, entry)
|
|
|
|
implementation = (
|
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
|
hass, entry
|
|
)
|
|
)
|
|
|
|
config = hass.data[DOMAIN][DATA_NEST_CONFIG]
|
|
|
|
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
|
auth = api.AsyncConfigEntryAuth(
|
|
aiohttp_client.async_get_clientsession(hass),
|
|
session,
|
|
config[CONF_CLIENT_ID],
|
|
config[CONF_CLIENT_SECRET],
|
|
)
|
|
subscriber = GoogleNestSubscriber(
|
|
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
|
|
)
|
|
callback = SignalUpdateCallback(hass)
|
|
subscriber.set_update_callback(callback.async_handle_event)
|
|
|
|
try:
|
|
await subscriber.start_async()
|
|
except AuthException as err:
|
|
_LOGGER.debug("Subscriber authentication error: %s", err)
|
|
raise ConfigEntryAuthFailed from err
|
|
except ConfigurationException as err:
|
|
_LOGGER.error("Configuration error: %s", err)
|
|
subscriber.stop_async()
|
|
return False
|
|
except GoogleNestException as err:
|
|
if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]:
|
|
_LOGGER.error("Subscriber error: %s", err)
|
|
hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True
|
|
subscriber.stop_async()
|
|
raise ConfigEntryNotReady from err
|
|
|
|
try:
|
|
await subscriber.async_get_device_manager()
|
|
except GoogleNestException as err:
|
|
if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]:
|
|
_LOGGER.error("Device manager error: %s", err)
|
|
hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True
|
|
subscriber.stop_async()
|
|
raise ConfigEntryNotReady from err
|
|
|
|
hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None)
|
|
hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber
|
|
|
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
if DATA_SDM not in entry.data:
|
|
# Legacy API
|
|
return True
|
|
_LOGGER.debug("Stopping nest subscriber")
|
|
subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
|
|
subscriber.stop_async()
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
if unload_ok:
|
|
hass.data[DOMAIN].pop(DATA_SUBSCRIBER)
|
|
hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None)
|
|
|
|
return unload_ok
|