mirror of
https://github.com/home-assistant/core.git
synced 2025-11-15 22:10:09 +00:00
284 lines
9.5 KiB
Python
284 lines
9.5 KiB
Python
"""Adds config flow for Tibber integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
import logging
|
|
from typing import Any
|
|
|
|
import aiohttp
|
|
import tibber
|
|
from tibber.data_api import TibberDataAPI
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
|
AbstractOAuth2FlowHandler,
|
|
async_get_config_entry_implementation,
|
|
async_get_implementations,
|
|
)
|
|
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
|
|
|
from .const import (
|
|
API_TYPE_DATA_API,
|
|
API_TYPE_GRAPHQL,
|
|
CONF_API_TYPE,
|
|
DATA_API_DEFAULT_SCOPES,
|
|
DOMAIN,
|
|
)
|
|
|
|
TYPE_SELECTOR = vol.Schema(
|
|
{
|
|
vol.Required(CONF_API_TYPE, default=API_TYPE_GRAPHQL): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=[API_TYPE_GRAPHQL, API_TYPE_DATA_API],
|
|
translation_key="api_type",
|
|
)
|
|
)
|
|
}
|
|
)
|
|
|
|
GRAPHQL_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
|
|
|
|
ERR_TIMEOUT = "timeout"
|
|
ERR_CLIENT = "cannot_connect"
|
|
ERR_TOKEN = "invalid_access_token"
|
|
TOKEN_URL = "https://developer.tibber.com/settings/access-token"
|
|
DATA_API_DOC_URL = "https://data-api.tibber.com/docs/auth/"
|
|
APPLICATION_CREDENTIALS_DOC_URL = (
|
|
"https://www.home-assistant.io/integrations/application_credentials/"
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class TibberConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
|
"""Handle a config flow for Tibber integration."""
|
|
|
|
DOMAIN = DOMAIN
|
|
VERSION = 1
|
|
MINOR_VERSION = 1
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the config flow."""
|
|
super().__init__()
|
|
self._api_type: str | None = None
|
|
self._data_api_home_ids: list[str] = []
|
|
self._data_api_user_sub: str | None = None
|
|
|
|
@property
|
|
def logger(self) -> logging.Logger:
|
|
"""Return the logger."""
|
|
return _LOGGER
|
|
|
|
@property
|
|
def extra_authorize_data(self) -> dict:
|
|
"""Extra data appended to the authorize URL."""
|
|
if self._api_type != API_TYPE_DATA_API:
|
|
return super().extra_authorize_data
|
|
return {
|
|
**super().extra_authorize_data,
|
|
"scope": " ".join(DATA_API_DEFAULT_SCOPES),
|
|
}
|
|
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the initial step."""
|
|
|
|
if user_input is None:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=TYPE_SELECTOR,
|
|
description_placeholders={"url": DATA_API_DOC_URL},
|
|
)
|
|
|
|
self._api_type = user_input[CONF_API_TYPE]
|
|
|
|
if self._api_type == API_TYPE_GRAPHQL:
|
|
return await self.async_step_graphql()
|
|
|
|
return await self.async_step_data_api()
|
|
|
|
async def async_step_graphql(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle GraphQL token based configuration."""
|
|
|
|
if self.source != SOURCE_REAUTH:
|
|
for entry in self._async_current_entries(include_ignore=False):
|
|
if entry.entry_id == self.context.get("entry_id"):
|
|
continue
|
|
if entry.data.get(CONF_API_TYPE, API_TYPE_GRAPHQL) == API_TYPE_GRAPHQL:
|
|
return self.async_abort(reason="already_configured")
|
|
|
|
if user_input is not None:
|
|
access_token = user_input[CONF_ACCESS_TOKEN].replace(" ", "")
|
|
|
|
tibber_connection = tibber.Tibber(
|
|
access_token=access_token,
|
|
websession=async_get_clientsession(self.hass),
|
|
)
|
|
|
|
errors = {}
|
|
|
|
try:
|
|
await tibber_connection.update_info()
|
|
except TimeoutError:
|
|
errors[CONF_ACCESS_TOKEN] = ERR_TIMEOUT
|
|
except tibber.InvalidLoginError:
|
|
errors[CONF_ACCESS_TOKEN] = ERR_TOKEN
|
|
except (
|
|
aiohttp.ClientError,
|
|
tibber.RetryableHttpExceptionError,
|
|
tibber.FatalHttpExceptionError,
|
|
):
|
|
errors[CONF_ACCESS_TOKEN] = ERR_CLIENT
|
|
|
|
if errors:
|
|
return self.async_show_form(
|
|
step_id="graphql",
|
|
data_schema=GRAPHQL_SCHEMA,
|
|
description_placeholders={"url": TOKEN_URL},
|
|
errors=errors,
|
|
)
|
|
|
|
unique_id = tibber_connection.user_id
|
|
await self.async_set_unique_id(unique_id)
|
|
|
|
if self.source == SOURCE_REAUTH:
|
|
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
|
return self.async_update_reload_and_abort(
|
|
self._get_reauth_entry(),
|
|
data_updates={
|
|
CONF_API_TYPE: API_TYPE_GRAPHQL,
|
|
CONF_ACCESS_TOKEN: access_token,
|
|
},
|
|
title=tibber_connection.name,
|
|
)
|
|
|
|
self._abort_if_unique_id_configured()
|
|
|
|
data = {
|
|
CONF_API_TYPE: API_TYPE_GRAPHQL,
|
|
CONF_ACCESS_TOKEN: access_token,
|
|
}
|
|
|
|
return self.async_create_entry(
|
|
title=tibber_connection.name,
|
|
data=data,
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="graphql",
|
|
data_schema=GRAPHQL_SCHEMA,
|
|
description_placeholders={"url": TOKEN_URL},
|
|
errors={},
|
|
)
|
|
|
|
async def async_step_data_api(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the Data API OAuth configuration."""
|
|
|
|
implementations = await async_get_implementations(self.hass, self.DOMAIN)
|
|
if not implementations:
|
|
return self.async_abort(
|
|
reason="missing_credentials",
|
|
description_placeholders={
|
|
"application_credentials_url": APPLICATION_CREDENTIALS_DOC_URL,
|
|
"data_api_url": DATA_API_DOC_URL,
|
|
},
|
|
)
|
|
|
|
if self.source != SOURCE_REAUTH:
|
|
for entry in self._async_current_entries(include_ignore=False):
|
|
if entry.entry_id == self.context.get("entry_id"):
|
|
continue
|
|
if entry.data.get(CONF_API_TYPE, API_TYPE_GRAPHQL) == API_TYPE_DATA_API:
|
|
return self.async_abort(reason="already_configured")
|
|
|
|
return await self.async_step_pick_implementation(user_input)
|
|
|
|
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
|
"""Finalize the OAuth flow and create the config entry."""
|
|
|
|
assert self._api_type == API_TYPE_DATA_API
|
|
|
|
token: dict[str, Any] = data["token"]
|
|
|
|
client = TibberDataAPI(
|
|
token[CONF_ACCESS_TOKEN],
|
|
websession=async_get_clientsession(self.hass),
|
|
)
|
|
|
|
try:
|
|
userinfo = await client.get_userinfo()
|
|
except (
|
|
tibber.InvalidLoginError,
|
|
tibber.FatalHttpExceptionError,
|
|
) as err:
|
|
self.logger.error("Authentication failed against Data API: %s", err)
|
|
return self.async_abort(reason="oauth_invalid_token")
|
|
except (aiohttp.ClientError, TimeoutError) as err:
|
|
self.logger.error("Error retrieving homes via Data API: %s", err)
|
|
return self.async_abort(reason="cannot_connect")
|
|
|
|
unique_id = userinfo["email"]
|
|
title = userinfo["email"]
|
|
await self.async_set_unique_id(unique_id)
|
|
if self.source == SOURCE_REAUTH:
|
|
reauth_entry = self._get_reauth_entry()
|
|
self._abort_if_unique_id_mismatch(
|
|
reason="wrong_account",
|
|
description_placeholders={"email": reauth_entry.unique_id or ""},
|
|
)
|
|
return self.async_update_reload_and_abort(
|
|
reauth_entry,
|
|
data_updates={
|
|
CONF_API_TYPE: API_TYPE_DATA_API,
|
|
"auth_implementation": data["auth_implementation"],
|
|
CONF_TOKEN: token,
|
|
},
|
|
title=title,
|
|
)
|
|
self._abort_if_unique_id_configured()
|
|
|
|
entry_data: dict[str, Any] = {
|
|
CONF_API_TYPE: API_TYPE_DATA_API,
|
|
"auth_implementation": data["auth_implementation"],
|
|
CONF_TOKEN: token,
|
|
}
|
|
return self.async_create_entry(
|
|
title=title,
|
|
data=entry_data,
|
|
)
|
|
|
|
async def async_step_reauth(
|
|
self, entry_data: Mapping[str, Any]
|
|
) -> ConfigFlowResult:
|
|
"""Handle reauthentication."""
|
|
|
|
api_type = entry_data.get(CONF_API_TYPE, API_TYPE_GRAPHQL)
|
|
self._api_type = api_type
|
|
|
|
if api_type == API_TYPE_DATA_API:
|
|
self.flow_impl = await async_get_config_entry_implementation(
|
|
self.hass, self._get_reauth_entry()
|
|
)
|
|
return await self.async_step_auth()
|
|
|
|
self.context["title_placeholders"] = {"name": self._get_reauth_entry().title}
|
|
return await self.async_step_reauth_confirm()
|
|
|
|
async def async_step_reauth_confirm(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Confirm the reauth dialog for GraphQL entries."""
|
|
if user_input is None:
|
|
return self.async_show_form(step_id="reauth_confirm")
|
|
|
|
return await self.async_step_graphql()
|