Files
core/homeassistant/components/tibber/config_flow.py
Daniel Hjelseth Høyer 1f9c244c5c Tibber data api
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-11-14 06:01:05 +01:00

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()