mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add pvpc hourly pricing optional API Token support (#85767)
* 🍱 Add new fixture for PVPC data from authenticated API call and update mocked server responses when data is not available (now returns a 200 OK with empty data) * ✨ Implement optional API token in config-flow + options to make the data download from an authenticated path in ESIOS server As this is an *alternative* access, and current public path works for the PVPC, no user (current or new) is compelled to obtain a token, and it can be enabled anytime in options, or doing the setup again When enabling the token, it is verified (or "invalid_auth" error), and a 'reauth' flow is implemented, which can change or disable the token if it starts failing. The 1st step of config/options flow adds a bool to enable this private access, - if unchecked (default), entry is set for public access (like before) - if checked, a 2nd step opens to input the token, with instructions of how to get one (with a direct link to create a 'request email'). If the token is valid, the entry is set for authenticated access The 'reauth' flow shows the boolean flag so the user could disable a bad token by unchecking the boolean flag 'use_api_token' * 🌐 Update strings for config/options flows * ✅ Adapt tests to check API token option and add test_reauth * 🎨 Link new strings to those in config-flow * 🐛 tests: Fix mocked date-change with async_fire_time_changed * ♻️ Remove storage of flag 'use_api_token' in config entry leaving it only to enable/disable the optional token in the config-flow * ♻️ Adjust async_update_options
This commit is contained in:
parent
0b213c6732
commit
32aa1aaec2
@ -2,38 +2,21 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiopvpc import DEFAULT_POWER_KW, TARIFFS, EsiosApiData, PVPCData
|
||||
import voluptuous as vol
|
||||
from aiopvpc import BadApiTokenAuthError, EsiosApiData, PVPCData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_POWER,
|
||||
ATTR_POWER_P3,
|
||||
ATTR_TARIFF,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_DEFAULT_TARIFF = TARIFFS[0]
|
||||
VALID_POWER = vol.All(vol.Coerce(float), vol.Range(min=1.0, max=15.0))
|
||||
VALID_TARIFF = vol.In(TARIFFS)
|
||||
UI_CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(ATTR_TARIFF, default=_DEFAULT_TARIFF): VALID_TARIFF,
|
||||
vol.Required(ATTR_POWER, default=DEFAULT_POWER_KW): VALID_POWER,
|
||||
vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER,
|
||||
}
|
||||
)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
||||
@ -52,7 +35,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
if any(
|
||||
entry.data.get(attrib) != entry.options.get(attrib)
|
||||
for attrib in (ATTR_POWER, ATTR_POWER_P3)
|
||||
for attrib in (ATTR_POWER, ATTR_POWER_P3, CONF_API_TOKEN)
|
||||
):
|
||||
# update entry replacing data with new options
|
||||
hass.config_entries.async_update_entry(
|
||||
@ -80,6 +63,7 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]):
|
||||
local_timezone=hass.config.time_zone,
|
||||
power=entry.data[ATTR_POWER],
|
||||
power_valley=entry.data[ATTR_POWER_P3],
|
||||
api_token=entry.data.get(CONF_API_TOKEN),
|
||||
)
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30)
|
||||
@ -93,7 +77,10 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]):
|
||||
|
||||
async def _async_update_data(self) -> EsiosApiData:
|
||||
"""Update electricity prices from the ESIOS API."""
|
||||
api_data = await self.api.async_update_all(self.data, dt_util.utcnow())
|
||||
try:
|
||||
api_data = await self.api.async_update_all(self.data, dt_util.utcnow())
|
||||
except BadApiTokenAuthError as exc:
|
||||
raise ConfigEntryAuthFailed from exc
|
||||
if (
|
||||
not api_data
|
||||
or not api_data.sensors
|
||||
|
@ -1,22 +1,49 @@
|
||||
"""Config flow for pvpc_hourly_pricing."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aiopvpc import DEFAULT_POWER_KW, PVPCData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER
|
||||
from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN
|
||||
from .const import (
|
||||
ATTR_POWER,
|
||||
ATTR_POWER_P3,
|
||||
ATTR_TARIFF,
|
||||
CONF_USE_API_TOKEN,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_TARIFF,
|
||||
DOMAIN,
|
||||
VALID_POWER,
|
||||
VALID_TARIFF,
|
||||
)
|
||||
|
||||
_MAIL_TO_LINK = (
|
||||
"[consultasios@ree.es]"
|
||||
"(mailto:consultasios@ree.es?subject=Personal%20token%20request)"
|
||||
)
|
||||
|
||||
|
||||
class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for `pvpc_hourly_pricing`."""
|
||||
|
||||
VERSION = 1
|
||||
_name: str | None = None
|
||||
_tariff: str | None = None
|
||||
_power: float | None = None
|
||||
_power_p3: float | None = None
|
||||
_use_api_token: bool = False
|
||||
_api_token: str | None = None
|
||||
_api: PVPCData | None = None
|
||||
_reauth_entry: config_entries.ConfigEntry | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@ -33,36 +60,180 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[ATTR_TARIFF])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
|
||||
if not user_input[CONF_USE_API_TOKEN]:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME],
|
||||
data={
|
||||
CONF_NAME: user_input[CONF_NAME],
|
||||
ATTR_TARIFF: user_input[ATTR_TARIFF],
|
||||
ATTR_POWER: user_input[ATTR_POWER],
|
||||
ATTR_POWER_P3: user_input[ATTR_POWER_P3],
|
||||
CONF_API_TOKEN: None,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=UI_CONFIG_SCHEMA)
|
||||
self._name = user_input[CONF_NAME]
|
||||
self._tariff = user_input[ATTR_TARIFF]
|
||||
self._power = user_input[ATTR_POWER]
|
||||
self._power_p3 = user_input[ATTR_POWER_P3]
|
||||
self._use_api_token = user_input[CONF_USE_API_TOKEN]
|
||||
return self.async_show_form(
|
||||
step_id="api_token",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_API_TOKEN, default=self._api_token): str}
|
||||
),
|
||||
description_placeholders={"mail_to_link": _MAIL_TO_LINK},
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(ATTR_TARIFF, default=DEFAULT_TARIFF): VALID_TARIFF,
|
||||
vol.Required(ATTR_POWER, default=DEFAULT_POWER_KW): VALID_POWER,
|
||||
vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER,
|
||||
vol.Required(CONF_USE_API_TOKEN, default=False): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=data_schema)
|
||||
|
||||
async def async_step_api_token(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Handle optional step to define API token for extra sensors."""
|
||||
self._api_token = user_input[CONF_API_TOKEN]
|
||||
return await self._async_verify(
|
||||
"api_token",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_API_TOKEN, default=self._api_token): str}
|
||||
),
|
||||
)
|
||||
|
||||
async def _async_verify(self, step_id: str, data_schema: vol.Schema) -> FlowResult:
|
||||
"""Attempt to verify the provided configuration."""
|
||||
errors: dict[str, str] = {}
|
||||
auth_ok = True
|
||||
if self._use_api_token:
|
||||
if not self._api:
|
||||
self._api = PVPCData(session=async_get_clientsession(self.hass))
|
||||
auth_ok = await self._api.check_api_token(dt_util.utcnow(), self._api_token)
|
||||
if not auth_ok:
|
||||
errors["base"] = "invalid_auth"
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
description_placeholders={"mail_to_link": _MAIL_TO_LINK},
|
||||
)
|
||||
|
||||
data = {
|
||||
CONF_NAME: self._name,
|
||||
ATTR_TARIFF: self._tariff,
|
||||
ATTR_POWER: self._power,
|
||||
ATTR_POWER_P3: self._power_p3,
|
||||
CONF_API_TOKEN: self._api_token if self._use_api_token else None,
|
||||
}
|
||||
if self._reauth_entry:
|
||||
self.hass.config_entries.async_update_entry(self._reauth_entry, data=data)
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
||||
)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
assert self._name is not None
|
||||
return self.async_create_entry(title=self._name, data=data)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle re-authentication with ESIOS Token."""
|
||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
self._api_token = entry_data.get(CONF_API_TOKEN)
|
||||
self._use_api_token = self._api_token is not None
|
||||
self._name = entry_data[CONF_NAME]
|
||||
self._tariff = entry_data[ATTR_TARIFF]
|
||||
self._power = entry_data[ATTR_POWER]
|
||||
self._power_p3 = entry_data[ATTR_POWER_P3]
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm reauth dialog."""
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USE_API_TOKEN, default=self._use_api_token): bool,
|
||||
vol.Optional(CONF_API_TOKEN, default=self._api_token): str,
|
||||
}
|
||||
)
|
||||
if user_input:
|
||||
self._api_token = user_input[CONF_API_TOKEN]
|
||||
self._use_api_token = user_input[CONF_USE_API_TOKEN]
|
||||
return await self._async_verify("reauth_confirm", data_schema)
|
||||
return self.async_show_form(step_id="reauth_confirm", data_schema=data_schema)
|
||||
|
||||
|
||||
class PVPCOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
class PVPCOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
|
||||
"""Handle PVPC options."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
_power: float | None = None
|
||||
_power_p3: float | None = None
|
||||
|
||||
async def async_step_api_token(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle optional step to define API token for extra sensors."""
|
||||
if user_input is not None and user_input.get(CONF_API_TOKEN):
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
ATTR_POWER: self._power,
|
||||
ATTR_POWER_P3: self._power_p3,
|
||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
||||
},
|
||||
)
|
||||
|
||||
# Fill options with entry data
|
||||
api_token = self.options.get(
|
||||
CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="api_token",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_API_TOKEN, default=api_token): str}
|
||||
),
|
||||
description_placeholders={"mail_to_link": _MAIL_TO_LINK},
|
||||
)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if user_input[CONF_USE_API_TOKEN]:
|
||||
self._power = user_input[ATTR_POWER]
|
||||
self._power_p3 = user_input[ATTR_POWER_P3]
|
||||
return await self.async_step_api_token(user_input)
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
ATTR_POWER: user_input[ATTR_POWER],
|
||||
ATTR_POWER_P3: user_input[ATTR_POWER_P3],
|
||||
CONF_API_TOKEN: None,
|
||||
},
|
||||
)
|
||||
|
||||
# Fill options with entry data
|
||||
power = self.config_entry.options.get(
|
||||
ATTR_POWER, self.config_entry.data[ATTR_POWER]
|
||||
)
|
||||
power_valley = self.config_entry.options.get(
|
||||
power = self.options.get(ATTR_POWER, self.config_entry.data[ATTR_POWER])
|
||||
power_valley = self.options.get(
|
||||
ATTR_POWER_P3, self.config_entry.data[ATTR_POWER_P3]
|
||||
)
|
||||
api_token = self.options.get(
|
||||
CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
|
||||
)
|
||||
use_api_token = api_token is not None
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_POWER, default=power): VALID_POWER,
|
||||
vol.Required(ATTR_POWER_P3, default=power_valley): VALID_POWER,
|
||||
vol.Required(CONF_USE_API_TOKEN, default=use_api_token): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=schema)
|
||||
|
@ -1,9 +1,15 @@
|
||||
"""Constant values for pvpc_hourly_pricing."""
|
||||
from homeassistant.const import Platform
|
||||
from aiopvpc import TARIFFS
|
||||
import voluptuous as vol
|
||||
|
||||
DOMAIN = "pvpc_hourly_pricing"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
ATTR_POWER = "power"
|
||||
ATTR_POWER_P3 = "power_p3"
|
||||
ATTR_TARIFF = "tariff"
|
||||
DEFAULT_NAME = "PVPC"
|
||||
CONF_USE_API_TOKEN = "use_api_token"
|
||||
|
||||
VALID_POWER = vol.All(vol.Coerce(float), vol.Range(min=1.0, max=15.0))
|
||||
VALID_TARIFF = vol.In(TARIFFS)
|
||||
DEFAULT_TARIFF = TARIFFS[0]
|
||||
|
@ -6,12 +6,31 @@
|
||||
"name": "Sensor Name",
|
||||
"tariff": "Applicable tariff by geographic zone",
|
||||
"power": "Contracted power (kW)",
|
||||
"power_p3": "Contracted power for valley period P3 (kW)"
|
||||
"power_p3": "Contracted power for valley period P3 (kW)",
|
||||
"use_api_token": "Enable ESIOS Personal API token for private access"
|
||||
}
|
||||
},
|
||||
"api_token": {
|
||||
"title": "ESIOS API token",
|
||||
"description": "To use the extended API you must request a personal token by mailing to {mail_to_link}.",
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"description": "Re-authenticate with a valid token or disable it",
|
||||
"use_api_token": "[%key:component::pvpc_hourly_pricing::config::step::user::data::use_api_token%]",
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@ -19,7 +38,15 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"power": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power%]",
|
||||
"power_p3": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power_p3%]"
|
||||
"power_p3": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power_p3%]",
|
||||
"use_api_token": "[%key:component::pvpc_hourly_pricing::config::step::user::data::use_api_token%]"
|
||||
}
|
||||
},
|
||||
"api_token": {
|
||||
"title": "[%key:component::pvpc_hourly_pricing::config::step::api_token::title%]",
|
||||
"description": "[%key:component::pvpc_hourly_pricing::config::step::api_token::description%]",
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ from tests.common import load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
FIXTURE_JSON_PUBLIC_DATA_2023_01_06 = "PVPC_DATA_2023_01_06.json"
|
||||
FIXTURE_JSON_ESIOS_DATA_PVPC_2023_01_06 = "PRICES_ESIOS_1001_2023_01_06.json"
|
||||
|
||||
|
||||
def check_valid_state(state, tariff: str, value=None, key_attr=None):
|
||||
@ -21,7 +22,7 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None):
|
||||
)
|
||||
try:
|
||||
_ = float(state.state)
|
||||
# safety margins for current electricity price (it shouldn't be out of [0, 0.2])
|
||||
# safety margins for current electricity price (it shouldn't be out of [0, 0.5])
|
||||
assert -0.1 < float(state.state) < 0.5
|
||||
assert state.attributes[ATTR_TARIFF] == tariff
|
||||
except ValueError:
|
||||
@ -41,20 +42,42 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker):
|
||||
mask_url_public = (
|
||||
"https://api.esios.ree.es/archives/70/download_json?locale=es&date={0}"
|
||||
)
|
||||
# new format for prices >= 2021-06-01
|
||||
mask_url_esios = (
|
||||
"https://api.esios.ree.es/indicators/1001"
|
||||
"?start_date={0}T00:00&end_date={0}T23:59"
|
||||
)
|
||||
example_day = "2023-01-06"
|
||||
aioclient_mock.get(
|
||||
mask_url_public.format(example_day),
|
||||
text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_PUBLIC_DATA_2023_01_06}"),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mask_url_esios.format(example_day),
|
||||
text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_ESIOS_DATA_PVPC_2023_01_06}"),
|
||||
)
|
||||
|
||||
# simulate missing days
|
||||
aioclient_mock.get(
|
||||
mask_url_public.format("2023-01-07"),
|
||||
status=HTTPStatus.BAD_GATEWAY,
|
||||
status=HTTPStatus.OK,
|
||||
text='{"message":"No values for specified archive"}',
|
||||
)
|
||||
aioclient_mock.get(
|
||||
mask_url_esios.format("2023-01-07"),
|
||||
status=HTTPStatus.OK,
|
||||
text=(
|
||||
'{"errors":[{"code":502,"status":"502","title":"Bad response from ESIOS",'
|
||||
'"detail":"There are no data for the selected filters."}]}'
|
||||
'{"indicator":{"name":"Término de facturación de energía activa del '
|
||||
'PVPC 2.0TD","short_name":"PVPC T. 2.0TD","id":1001,"composited":false,'
|
||||
'"step_type":"linear","disaggregated":true,"magnitud":'
|
||||
'[{"name":"Precio","id":23}],"tiempo":[{"name":"Hora","id":4}],"geos":[],'
|
||||
'"values_updated_at":null,"values":[]}}'
|
||||
),
|
||||
)
|
||||
# simulate bad authentication
|
||||
aioclient_mock.get(
|
||||
mask_url_esios.format("2023-01-08"),
|
||||
status=HTTPStatus.UNAUTHORIZED,
|
||||
text="HTTP Token: Access denied.",
|
||||
)
|
||||
|
||||
return aioclient_mock
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,14 +4,15 @@ from datetime import datetime, timedelta
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.pvpc_hourly_pricing import (
|
||||
from homeassistant.components.pvpc_hourly_pricing.const import (
|
||||
ATTR_POWER,
|
||||
ATTR_POWER_P3,
|
||||
ATTR_TARIFF,
|
||||
CONF_USE_API_TOKEN,
|
||||
DOMAIN,
|
||||
TARIFFS,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
@ -22,6 +23,7 @@ from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
_MOCK_TIME_VALID_RESPONSES = datetime(2023, 1, 6, 12, 0, tzinfo=dt_util.UTC)
|
||||
_MOCK_TIME_BAD_AUTH_RESPONSES = datetime(2023, 1, 8, 12, 0, tzinfo=dt_util.UTC)
|
||||
|
||||
|
||||
async def test_config_flow(
|
||||
@ -35,7 +37,7 @@ async def test_config_flow(
|
||||
- Check state and attributes
|
||||
- Check abort when trying to config another with same tariff
|
||||
- Check removal and add again to check state restoration
|
||||
- Configure options to change power and tariff to "2.0TD"
|
||||
- Configure options to introduce API Token, with bad auth and good one
|
||||
"""
|
||||
freezer.move_to(_MOCK_TIME_VALID_RESPONSES)
|
||||
hass.config.set_time_zone("Europe/Madrid")
|
||||
@ -44,6 +46,7 @@ async def test_config_flow(
|
||||
ATTR_TARIFF: TARIFFS[1],
|
||||
ATTR_POWER: 4.6,
|
||||
ATTR_POWER_P3: 5.75,
|
||||
CONF_USE_API_TOKEN: False,
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -107,8 +110,17 @@ async def test_config_flow(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6},
|
||||
user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: True},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "api_token"
|
||||
assert pvpc_aioclient_mock.call_count == 2
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert pvpc_aioclient_mock.call_count == 2
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.esios_pvpc")
|
||||
check_valid_state(state, tariff=TARIFFS[1])
|
||||
@ -125,3 +137,96 @@ async def test_config_flow(
|
||||
check_valid_state(state, tariff=TARIFFS[0], value="unavailable")
|
||||
assert "period" not in state.attributes
|
||||
assert pvpc_aioclient_mock.call_count == 4
|
||||
|
||||
# disable api token in options
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: False},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert pvpc_aioclient_mock.call_count == 4
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_reauth(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
pvpc_aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test reauth flow for API-token mode."""
|
||||
freezer.move_to(_MOCK_TIME_BAD_AUTH_RESPONSES)
|
||||
hass.config.set_time_zone("Europe/Madrid")
|
||||
tst_config = {
|
||||
CONF_NAME: "test",
|
||||
ATTR_TARIFF: TARIFFS[1],
|
||||
ATTR_POWER: 4.6,
|
||||
ATTR_POWER_P3: 5.75,
|
||||
CONF_USE_API_TOKEN: True,
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], tst_config
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "api_token"
|
||||
assert pvpc_aioclient_mock.call_count == 0
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "bad-token"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "api_token"
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
assert pvpc_aioclient_mock.call_count == 1
|
||||
|
||||
freezer.move_to(_MOCK_TIME_VALID_RESPONSES)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
config_entry = result["result"]
|
||||
assert pvpc_aioclient_mock.call_count == 3
|
||||
|
||||
# check reauth trigger with bad-auth responses
|
||||
freezer.move_to(_MOCK_TIME_BAD_AUTH_RESPONSES)
|
||||
async_fire_time_changed(hass, _MOCK_TIME_BAD_AUTH_RESPONSES)
|
||||
await hass.async_block_till_done()
|
||||
assert pvpc_aioclient_mock.call_count == 4
|
||||
|
||||
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
|
||||
assert result["context"]["entry_id"] == config_entry.entry_id
|
||||
assert result["context"]["source"] == config_entries.SOURCE_REAUTH
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "bad-token"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert pvpc_aioclient_mock.call_count == 5
|
||||
|
||||
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
|
||||
assert result["context"]["entry_id"] == config_entry.entry_id
|
||||
assert result["context"]["source"] == config_entries.SOURCE_REAUTH
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
freezer.move_to(_MOCK_TIME_VALID_RESPONSES)
|
||||
async_fire_time_changed(hass, _MOCK_TIME_VALID_RESPONSES)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert pvpc_aioclient_mock.call_count == 6
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert pvpc_aioclient_mock.call_count == 7
|
||||
|
Loading…
x
Reference in New Issue
Block a user