Add new price sensors with API token access to pvpc hourly pricing (#85769)

*  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'

* ♻️ 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

*  Add new price sensors with API token access

New price sensors added:
- Injection price: price of excess energy from self-consumption
- OMIE price: electricity price in the 'open' market
- MAG price: Temporal tax cost for gas compensation

*  Adapt tests to work with multiple sensors

* 🐛 Fix all integration sensors going unavailable

when any sensor lacks data for the current day (usually the 'OMIE price')

* Fix rebase

* Customize icons and display precision for new sensors

* Disable MAG Tax and OMIE price sensors by default

* Move logic to assign sensor unique ids to integration

* Move helper functions to helpers.py

* Fix sensor activation for API download
pull/106037/head
Eugenio Panadero 2023-12-18 21:06:02 +01:00 committed by GitHub
parent 1d1cd6be57
commit b96d2cadac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 181 additions and 45 deletions

View File

@ -10,10 +10,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN
from .helpers import get_enabled_sensor_keys
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
@ -22,7 +24,12 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up pvpc hourly pricing from a config entry.""" """Set up pvpc hourly pricing from a config entry."""
coordinator = ElecPricesDataUpdateCoordinator(hass, entry) entity_registry = er.async_get(hass)
sensor_keys = get_enabled_sensor_keys(
using_private_api=entry.data.get(CONF_API_TOKEN) is not None,
entries=er.async_entries_for_config_entry(entity_registry, entry.entry_id),
)
coordinator = ElecPricesDataUpdateCoordinator(hass, entry, sensor_keys)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
@ -55,7 +62,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]): class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]):
"""Class to manage fetching Electricity prices data from API.""" """Class to manage fetching Electricity prices data from API."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, sensor_keys: set[str]
) -> None:
"""Initialize.""" """Initialize."""
self.api = PVPCData( self.api = PVPCData(
session=async_get_clientsession(hass), session=async_get_clientsession(hass),
@ -64,6 +73,7 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]):
power=entry.data[ATTR_POWER], power=entry.data[ATTR_POWER],
power_valley=entry.data[ATTR_POWER_P3], power_valley=entry.data[ATTR_POWER_P3],
api_token=entry.data.get(CONF_API_TOKEN), api_token=entry.data.get(CONF_API_TOKEN),
sensor_keys=tuple(sensor_keys),
) )
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30)
@ -84,7 +94,7 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]):
if ( if (
not api_data not api_data
or not api_data.sensors or not api_data.sensors
or not all(api_data.availability.values()) or not any(api_data.availability.values())
): ):
raise UpdateFailed raise UpdateFailed
return api_data return api_data

View File

@ -1,5 +1,5 @@
"""Constant values for pvpc_hourly_pricing.""" """Constant values for pvpc_hourly_pricing."""
from aiopvpc import TARIFFS from aiopvpc.const import TARIFFS
import voluptuous as vol import voluptuous as vol
DOMAIN = "pvpc_hourly_pricing" DOMAIN = "pvpc_hourly_pricing"

View File

@ -0,0 +1,49 @@
"""Helper functions to relate sensors keys and unique ids."""
from aiopvpc.const import (
ALL_SENSORS,
KEY_INJECTION,
KEY_MAG,
KEY_OMIE,
KEY_PVPC,
TARIFFS,
)
from homeassistant.helpers.entity_registry import RegistryEntry
_ha_uniqueid_to_sensor_key = {
TARIFFS[0]: KEY_PVPC,
TARIFFS[1]: KEY_PVPC,
f"{TARIFFS[0]}_{KEY_INJECTION}": KEY_INJECTION,
f"{TARIFFS[1]}_{KEY_INJECTION}": KEY_INJECTION,
f"{TARIFFS[0]}_{KEY_MAG}": KEY_MAG,
f"{TARIFFS[1]}_{KEY_MAG}": KEY_MAG,
f"{TARIFFS[0]}_{KEY_OMIE}": KEY_OMIE,
f"{TARIFFS[1]}_{KEY_OMIE}": KEY_OMIE,
}
def get_enabled_sensor_keys(
using_private_api: bool, entries: list[RegistryEntry]
) -> set[str]:
"""Get enabled API indicators."""
if not using_private_api:
return {KEY_PVPC}
if len(entries) > 1:
# activate only enabled sensors
return {
_ha_uniqueid_to_sensor_key[sensor.unique_id]
for sensor in entries
if not sensor.disabled
}
# default sensors when enabling token access
return {KEY_PVPC, KEY_INJECTION}
def make_sensor_unique_id(config_entry_id: str | None, sensor_key: str) -> str:
"""Generate unique_id for each sensor kind and config entry."""
assert sensor_key in ALL_SENSORS
assert config_entry_id is not None
if sensor_key == KEY_PVPC:
# for old compatibility
return config_entry_id
return f"{config_entry_id}_{sensor_key}"

View File

@ -6,6 +6,8 @@ from datetime import datetime
import logging import logging
from typing import Any from typing import Any
from aiopvpc.const import KEY_INJECTION, KEY_MAG, KEY_OMIE, KEY_PVPC
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
@ -22,19 +24,49 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ElecPricesDataUpdateCoordinator from . import ElecPricesDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .helpers import make_sensor_unique_id
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription( SensorEntityDescription(
key="PVPC", key=KEY_PVPC,
icon="mdi:currency-eur", icon="mdi:currency-eur",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=5,
name="PVPC", name="PVPC",
), ),
SensorEntityDescription(
key=KEY_INJECTION,
icon="mdi:transmission-tower-export",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=5,
name="Injection Price",
),
SensorEntityDescription(
key=KEY_MAG,
icon="mdi:bank-transfer",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=5,
name="MAG tax",
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key=KEY_OMIE,
icon="mdi:shopping",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=5,
name="OMIE Price",
entity_registry_enabled_default=False,
),
) )
_PRICE_SENSOR_ATTRIBUTES_MAP = { _PRICE_SENSOR_ATTRIBUTES_MAP = {
"data_id": "data_id",
"name": "data_name",
"tariff": "tariff", "tariff": "tariff",
"period": "period", "period": "period",
"available_power": "available_power", "available_power": "available_power",
@ -119,7 +151,11 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the electricity price sensor from config_entry.""" """Set up the electricity price sensor from config_entry."""
coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id)]) sensors = [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id)]
if coordinator.api.using_private_api:
for sensor_desc in SENSOR_TYPES[1:]:
sensors.append(ElecPriceSensor(coordinator, sensor_desc, entry.unique_id))
async_add_entities(sensors)
class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], SensorEntity): class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], SensorEntity):
@ -137,7 +173,7 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._attr_attribution = coordinator.api.attribution self._attr_attribution = coordinator.api.attribution
self._attr_unique_id = unique_id self._attr_unique_id = make_sensor_unique_id(unique_id, description.key)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
configuration_url="https://api.esios.ree.es", configuration_url="https://api.esios.ree.es",
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
@ -146,9 +182,23 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor
name="ESIOS", name="ESIOS",
) )
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.data.availability.get(
self.entity_description.key, False
)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""
await super().async_added_to_hass() await super().async_added_to_hass()
# Enable API downloads for this sensor
self.coordinator.api.update_active_sensors(self.entity_description.key, True)
self.async_on_remove(
lambda: self.coordinator.api.update_active_sensors(
self.entity_description.key, False
)
)
# Update 'state' value in hour changes # Update 'state' value in hour changes
self.async_on_remove( self.async_on_remove(
@ -157,10 +207,10 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor
) )
) )
_LOGGER.debug( _LOGGER.debug(
"Setup of price sensor %s (%s) with tariff '%s'", "Setup of ESIOS sensor %s (%s, unique_id: %s)",
self.name, self.entity_description.key,
self.entity_id, self.entity_id,
self.coordinator.api.tariff, self._attr_unique_id,
) )
@callback @callback

View File

@ -11,6 +11,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker
FIXTURE_JSON_PUBLIC_DATA_2023_01_06 = "PVPC_DATA_2023_01_06.json" 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" FIXTURE_JSON_ESIOS_DATA_PVPC_2023_01_06 = "PRICES_ESIOS_1001_2023_01_06.json"
_ESIOS_INDICATORS_FOR_EACH_SENSOR = ("1001", "1739", "1900", "10211")
def check_valid_state(state, tariff: str, value=None, key_attr=None): def check_valid_state(state, tariff: str, value=None, key_attr=None):
@ -43,16 +44,17 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker):
"https://api.esios.ree.es/archives/70/download_json?locale=es&date={0}" "https://api.esios.ree.es/archives/70/download_json?locale=es&date={0}"
) )
mask_url_esios = ( mask_url_esios = (
"https://api.esios.ree.es/indicators/1001" "https://api.esios.ree.es/indicators/{0}"
"?start_date={0}T00:00&end_date={0}T23:59" "?start_date={1}T00:00&end_date={1}T23:59"
) )
example_day = "2023-01-06" example_day = "2023-01-06"
aioclient_mock.get( aioclient_mock.get(
mask_url_public.format(example_day), mask_url_public.format(example_day),
text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_PUBLIC_DATA_2023_01_06}"), text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_PUBLIC_DATA_2023_01_06}"),
) )
for esios_ind in _ESIOS_INDICATORS_FOR_EACH_SENSOR:
aioclient_mock.get( aioclient_mock.get(
mask_url_esios.format(example_day), mask_url_esios.format(esios_ind, example_day),
text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_ESIOS_DATA_PVPC_2023_01_06}"), text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_ESIOS_DATA_PVPC_2023_01_06}"),
) )
@ -62,8 +64,9 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker):
status=HTTPStatus.OK, status=HTTPStatus.OK,
text='{"message":"No values for specified archive"}', text='{"message":"No values for specified archive"}',
) )
for esios_ind in _ESIOS_INDICATORS_FOR_EACH_SENSOR:
aioclient_mock.get( aioclient_mock.get(
mask_url_esios.format("2023-01-07"), mask_url_esios.format(esios_ind, "2023-01-07"),
status=HTTPStatus.OK, status=HTTPStatus.OK,
text=( text=(
'{"indicator":{"name":"Término de facturación de energía activa del ' '{"indicator":{"name":"Término de facturación de energía activa del '
@ -71,11 +74,12 @@ def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker):
'"step_type":"linear","disaggregated":true,"magnitud":' '"step_type":"linear","disaggregated":true,"magnitud":'
'[{"name":"Precio","id":23}],"tiempo":[{"name":"Hora","id":4}],"geos":[],' '[{"name":"Precio","id":23}],"tiempo":[{"name":"Hora","id":4}],"geos":[],'
'"values_updated_at":null,"values":[]}}' '"values_updated_at":null,"values":[]}}'
), ).replace("1001", esios_ind),
) )
# simulate bad authentication # simulate bad authentication
for esios_ind in _ESIOS_INDICATORS_FOR_EACH_SENSOR:
aioclient_mock.get( aioclient_mock.get(
mask_url_esios.format("2023-01-08"), mask_url_esios.format(esios_ind, "2023-01-08"),
status=HTTPStatus.UNAUTHORIZED, status=HTTPStatus.UNAUTHORIZED,
text="HTTP Token: Access denied.", text="HTTP Token: Access denied.",
) )

View File

@ -64,6 +64,10 @@ async def test_config_flow(
check_valid_state(state, tariff=TARIFFS[1]) check_valid_state(state, tariff=TARIFFS[1])
assert pvpc_aioclient_mock.call_count == 1 assert pvpc_aioclient_mock.call_count == 1
# no extra sensors created without enabled API token
state_inyection = hass.states.get("sensor.injection_price")
assert state_inyection is None
# Check abort when configuring another with same tariff # Check abort when configuring another with same tariff
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -117,18 +121,27 @@ async def test_config_flow(
assert pvpc_aioclient_mock.call_count == 2 assert pvpc_aioclient_mock.call_count == 2
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"} result["flow_id"], user_input={CONF_API_TOKEN: "test-token"}
) )
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert pvpc_aioclient_mock.call_count == 2 assert pvpc_aioclient_mock.call_count == 2
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.esios_pvpc") state = hass.states.get("sensor.esios_pvpc")
check_valid_state(state, tariff=TARIFFS[1]) check_valid_state(state, tariff=TARIFFS[1])
assert pvpc_aioclient_mock.call_count == 3 assert pvpc_aioclient_mock.call_count == 4
assert state.attributes["period"] == "P3" assert state.attributes["period"] == "P3"
assert state.attributes["next_period"] == "P2" assert state.attributes["next_period"] == "P2"
assert state.attributes["available_power"] == 4600 assert state.attributes["available_power"] == 4600
state_inyection = hass.states.get("sensor.esios_injection_price")
state_mag = hass.states.get("sensor.esios_mag_tax")
state_omie = hass.states.get("sensor.esios_omie_price")
assert state_inyection
assert not state_mag
assert not state_omie
assert "period" not in state_inyection.attributes
assert "available_power" not in state_inyection.attributes
# check update failed # check update failed
freezer.tick(timedelta(days=1)) freezer.tick(timedelta(days=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -136,7 +149,7 @@ async def test_config_flow(
state = hass.states.get("sensor.esios_pvpc") state = hass.states.get("sensor.esios_pvpc")
check_valid_state(state, tariff=TARIFFS[0], value="unavailable") check_valid_state(state, tariff=TARIFFS[0], value="unavailable")
assert "period" not in state.attributes assert "period" not in state.attributes
assert pvpc_aioclient_mock.call_count == 4 assert pvpc_aioclient_mock.call_count == 6
# disable api token in options # disable api token in options
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
@ -148,8 +161,18 @@ async def test_config_flow(
user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6, CONF_USE_API_TOKEN: False}, 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 result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert pvpc_aioclient_mock.call_count == 4 assert pvpc_aioclient_mock.call_count == 6
await hass.async_block_till_done() await hass.async_block_till_done()
assert pvpc_aioclient_mock.call_count == 7
state = hass.states.get("sensor.esios_pvpc")
state_inyection = hass.states.get("sensor.esios_injection_price")
state_mag = hass.states.get("sensor.esios_mag_tax")
state_omie = hass.states.get("sensor.esios_omie_price")
check_valid_state(state, tariff=TARIFFS[1])
assert state_inyection.state == "unavailable"
assert not state_mag
assert not state_omie
async def test_reauth( async def test_reauth(
@ -181,7 +204,7 @@ async def test_reauth(
assert pvpc_aioclient_mock.call_count == 0 assert pvpc_aioclient_mock.call_count == 0
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_API_TOKEN: "bad-token"} result["flow_id"], user_input={CONF_API_TOKEN: "test-token"}
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "api_token" assert result["step_id"] == "api_token"
@ -190,17 +213,17 @@ async def test_reauth(
freezer.move_to(_MOCK_TIME_VALID_RESPONSES) freezer.move_to(_MOCK_TIME_VALID_RESPONSES)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"} result["flow_id"], user_input={CONF_API_TOKEN: "test-token"}
) )
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
config_entry = result["result"] config_entry = result["result"]
assert pvpc_aioclient_mock.call_count == 3 assert pvpc_aioclient_mock.call_count == 4
# check reauth trigger with bad-auth responses # check reauth trigger with bad-auth responses
freezer.move_to(_MOCK_TIME_BAD_AUTH_RESPONSES) freezer.move_to(_MOCK_TIME_BAD_AUTH_RESPONSES)
async_fire_time_changed(hass, _MOCK_TIME_BAD_AUTH_RESPONSES) async_fire_time_changed(hass, _MOCK_TIME_BAD_AUTH_RESPONSES)
await hass.async_block_till_done() await hass.async_block_till_done()
assert pvpc_aioclient_mock.call_count == 4 assert pvpc_aioclient_mock.call_count == 6
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
assert result["context"]["entry_id"] == config_entry.entry_id assert result["context"]["entry_id"] == config_entry.entry_id
@ -208,11 +231,11 @@ async def test_reauth(
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_API_TOKEN: "bad-token"} result["flow_id"], user_input={CONF_API_TOKEN: "test-token"}
) )
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
assert pvpc_aioclient_mock.call_count == 5 assert pvpc_aioclient_mock.call_count == 7
result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
assert result["context"]["entry_id"] == config_entry.entry_id assert result["context"]["entry_id"] == config_entry.entry_id
@ -222,11 +245,11 @@ async def test_reauth(
freezer.move_to(_MOCK_TIME_VALID_RESPONSES) freezer.move_to(_MOCK_TIME_VALID_RESPONSES)
async_fire_time_changed(hass, _MOCK_TIME_VALID_RESPONSES) async_fire_time_changed(hass, _MOCK_TIME_VALID_RESPONSES)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_API_TOKEN: "good-token"} result["flow_id"], user_input={CONF_API_TOKEN: "test-token"}
) )
assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "reauth_successful" assert result["reason"] == "reauth_successful"
assert pvpc_aioclient_mock.call_count == 6 assert pvpc_aioclient_mock.call_count == 8
await hass.async_block_till_done() await hass.async_block_till_done()
assert pvpc_aioclient_mock.call_count == 7 assert pvpc_aioclient_mock.call_count == 10