diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 808ff1b4cc4..7071000ffd9 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -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 diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 9412aa2e97d..a98b9faf56e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -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) diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index 186ee1171f3..ea4d97620ec 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -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] diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index 1a0055ddbac..4236709fdfa 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -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%]" } } } diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index fb2c9188ce7..efe15547c13 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -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 diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PRICES_ESIOS_1001_2023_01_06.json b/tests/components/pvpc_hourly_pricing/fixtures/PRICES_ESIOS_1001_2023_01_06.json new file mode 100644 index 00000000000..20ad8af3696 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/fixtures/PRICES_ESIOS_1001_2023_01_06.json @@ -0,0 +1,1007 @@ +{ + "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": [ + { + "geo_id": 8741, + "geo_name": "Península" + }, + { + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "geo_id": 8745, + "geo_name": "Melilla" + } + ], + "values_updated_at": "2023-01-05T20:17:31.000+01:00", + "values": [ + { + "value": 159.69, + "datetime": "2023-01-06T00:00:00.000+01:00", + "datetime_utc": "2023-01-05T23:00:00Z", + "tz_time": "2023-01-05T23:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 159.69, + "datetime": "2023-01-06T00:00:00.000+01:00", + "datetime_utc": "2023-01-05T23:00:00Z", + "tz_time": "2023-01-05T23:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 159.69, + "datetime": "2023-01-06T00:00:00.000+01:00", + "datetime_utc": "2023-01-05T23:00:00Z", + "tz_time": "2023-01-05T23:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 159.69, + "datetime": "2023-01-06T00:00:00.000+01:00", + "datetime_utc": "2023-01-05T23:00:00Z", + "tz_time": "2023-01-05T23:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 159.69, + "datetime": "2023-01-06T00:00:00.000+01:00", + "datetime_utc": "2023-01-05T23:00:00Z", + "tz_time": "2023-01-05T23:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 155.71, + "datetime": "2023-01-06T01:00:00.000+01:00", + "datetime_utc": "2023-01-06T00:00:00Z", + "tz_time": "2023-01-06T00:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 155.71, + "datetime": "2023-01-06T01:00:00.000+01:00", + "datetime_utc": "2023-01-06T00:00:00Z", + "tz_time": "2023-01-06T00:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 155.71, + "datetime": "2023-01-06T01:00:00.000+01:00", + "datetime_utc": "2023-01-06T00:00:00Z", + "tz_time": "2023-01-06T00:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 155.71, + "datetime": "2023-01-06T01:00:00.000+01:00", + "datetime_utc": "2023-01-06T00:00:00Z", + "tz_time": "2023-01-06T00:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 155.71, + "datetime": "2023-01-06T01:00:00.000+01:00", + "datetime_utc": "2023-01-06T00:00:00Z", + "tz_time": "2023-01-06T00:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 154.41, + "datetime": "2023-01-06T02:00:00.000+01:00", + "datetime_utc": "2023-01-06T01:00:00Z", + "tz_time": "2023-01-06T01:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 154.41, + "datetime": "2023-01-06T02:00:00.000+01:00", + "datetime_utc": "2023-01-06T01:00:00Z", + "tz_time": "2023-01-06T01:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 154.41, + "datetime": "2023-01-06T02:00:00.000+01:00", + "datetime_utc": "2023-01-06T01:00:00Z", + "tz_time": "2023-01-06T01:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 154.41, + "datetime": "2023-01-06T02:00:00.000+01:00", + "datetime_utc": "2023-01-06T01:00:00Z", + "tz_time": "2023-01-06T01:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 154.41, + "datetime": "2023-01-06T02:00:00.000+01:00", + "datetime_utc": "2023-01-06T01:00:00Z", + "tz_time": "2023-01-06T01:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 139.37, + "datetime": "2023-01-06T03:00:00.000+01:00", + "datetime_utc": "2023-01-06T02:00:00Z", + "tz_time": "2023-01-06T02:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 139.37, + "datetime": "2023-01-06T03:00:00.000+01:00", + "datetime_utc": "2023-01-06T02:00:00Z", + "tz_time": "2023-01-06T02:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 139.37, + "datetime": "2023-01-06T03:00:00.000+01:00", + "datetime_utc": "2023-01-06T02:00:00Z", + "tz_time": "2023-01-06T02:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 139.37, + "datetime": "2023-01-06T03:00:00.000+01:00", + "datetime_utc": "2023-01-06T02:00:00Z", + "tz_time": "2023-01-06T02:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 139.37, + "datetime": "2023-01-06T03:00:00.000+01:00", + "datetime_utc": "2023-01-06T02:00:00Z", + "tz_time": "2023-01-06T02:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 134.02, + "datetime": "2023-01-06T04:00:00.000+01:00", + "datetime_utc": "2023-01-06T03:00:00Z", + "tz_time": "2023-01-06T03:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 134.02, + "datetime": "2023-01-06T04:00:00.000+01:00", + "datetime_utc": "2023-01-06T03:00:00Z", + "tz_time": "2023-01-06T03:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 134.02, + "datetime": "2023-01-06T04:00:00.000+01:00", + "datetime_utc": "2023-01-06T03:00:00Z", + "tz_time": "2023-01-06T03:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 134.02, + "datetime": "2023-01-06T04:00:00.000+01:00", + "datetime_utc": "2023-01-06T03:00:00Z", + "tz_time": "2023-01-06T03:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 134.02, + "datetime": "2023-01-06T04:00:00.000+01:00", + "datetime_utc": "2023-01-06T03:00:00Z", + "tz_time": "2023-01-06T03:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 140.02, + "datetime": "2023-01-06T05:00:00.000+01:00", + "datetime_utc": "2023-01-06T04:00:00Z", + "tz_time": "2023-01-06T04:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 140.02, + "datetime": "2023-01-06T05:00:00.000+01:00", + "datetime_utc": "2023-01-06T04:00:00Z", + "tz_time": "2023-01-06T04:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 140.02, + "datetime": "2023-01-06T05:00:00.000+01:00", + "datetime_utc": "2023-01-06T04:00:00Z", + "tz_time": "2023-01-06T04:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 140.02, + "datetime": "2023-01-06T05:00:00.000+01:00", + "datetime_utc": "2023-01-06T04:00:00Z", + "tz_time": "2023-01-06T04:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 140.02, + "datetime": "2023-01-06T05:00:00.000+01:00", + "datetime_utc": "2023-01-06T04:00:00Z", + "tz_time": "2023-01-06T04:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 154.05, + "datetime": "2023-01-06T06:00:00.000+01:00", + "datetime_utc": "2023-01-06T05:00:00Z", + "tz_time": "2023-01-06T05:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 154.05, + "datetime": "2023-01-06T06:00:00.000+01:00", + "datetime_utc": "2023-01-06T05:00:00Z", + "tz_time": "2023-01-06T05:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 154.05, + "datetime": "2023-01-06T06:00:00.000+01:00", + "datetime_utc": "2023-01-06T05:00:00Z", + "tz_time": "2023-01-06T05:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 154.05, + "datetime": "2023-01-06T06:00:00.000+01:00", + "datetime_utc": "2023-01-06T05:00:00Z", + "tz_time": "2023-01-06T05:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 154.05, + "datetime": "2023-01-06T06:00:00.000+01:00", + "datetime_utc": "2023-01-06T05:00:00Z", + "tz_time": "2023-01-06T05:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 163.15, + "datetime": "2023-01-06T07:00:00.000+01:00", + "datetime_utc": "2023-01-06T06:00:00Z", + "tz_time": "2023-01-06T06:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 163.15, + "datetime": "2023-01-06T07:00:00.000+01:00", + "datetime_utc": "2023-01-06T06:00:00Z", + "tz_time": "2023-01-06T06:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 163.15, + "datetime": "2023-01-06T07:00:00.000+01:00", + "datetime_utc": "2023-01-06T06:00:00Z", + "tz_time": "2023-01-06T06:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 163.15, + "datetime": "2023-01-06T07:00:00.000+01:00", + "datetime_utc": "2023-01-06T06:00:00Z", + "tz_time": "2023-01-06T06:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 163.15, + "datetime": "2023-01-06T07:00:00.000+01:00", + "datetime_utc": "2023-01-06T06:00:00Z", + "tz_time": "2023-01-06T06:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 180.5, + "datetime": "2023-01-06T08:00:00.000+01:00", + "datetime_utc": "2023-01-06T07:00:00Z", + "tz_time": "2023-01-06T07:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 180.5, + "datetime": "2023-01-06T08:00:00.000+01:00", + "datetime_utc": "2023-01-06T07:00:00Z", + "tz_time": "2023-01-06T07:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 180.5, + "datetime": "2023-01-06T08:00:00.000+01:00", + "datetime_utc": "2023-01-06T07:00:00Z", + "tz_time": "2023-01-06T07:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 180.5, + "datetime": "2023-01-06T08:00:00.000+01:00", + "datetime_utc": "2023-01-06T07:00:00Z", + "tz_time": "2023-01-06T07:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 180.5, + "datetime": "2023-01-06T08:00:00.000+01:00", + "datetime_utc": "2023-01-06T07:00:00Z", + "tz_time": "2023-01-06T07:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 174.9, + "datetime": "2023-01-06T09:00:00.000+01:00", + "datetime_utc": "2023-01-06T08:00:00Z", + "tz_time": "2023-01-06T08:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 174.9, + "datetime": "2023-01-06T09:00:00.000+01:00", + "datetime_utc": "2023-01-06T08:00:00Z", + "tz_time": "2023-01-06T08:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 174.9, + "datetime": "2023-01-06T09:00:00.000+01:00", + "datetime_utc": "2023-01-06T08:00:00Z", + "tz_time": "2023-01-06T08:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 174.9, + "datetime": "2023-01-06T09:00:00.000+01:00", + "datetime_utc": "2023-01-06T08:00:00Z", + "tz_time": "2023-01-06T08:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 174.9, + "datetime": "2023-01-06T09:00:00.000+01:00", + "datetime_utc": "2023-01-06T08:00:00Z", + "tz_time": "2023-01-06T08:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 166.47, + "datetime": "2023-01-06T10:00:00.000+01:00", + "datetime_utc": "2023-01-06T09:00:00Z", + "tz_time": "2023-01-06T09:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 166.47, + "datetime": "2023-01-06T10:00:00.000+01:00", + "datetime_utc": "2023-01-06T09:00:00Z", + "tz_time": "2023-01-06T09:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 166.47, + "datetime": "2023-01-06T10:00:00.000+01:00", + "datetime_utc": "2023-01-06T09:00:00Z", + "tz_time": "2023-01-06T09:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 166.47, + "datetime": "2023-01-06T10:00:00.000+01:00", + "datetime_utc": "2023-01-06T09:00:00Z", + "tz_time": "2023-01-06T09:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 166.47, + "datetime": "2023-01-06T10:00:00.000+01:00", + "datetime_utc": "2023-01-06T09:00:00Z", + "tz_time": "2023-01-06T09:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 152.3, + "datetime": "2023-01-06T11:00:00.000+01:00", + "datetime_utc": "2023-01-06T10:00:00Z", + "tz_time": "2023-01-06T10:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 152.3, + "datetime": "2023-01-06T11:00:00.000+01:00", + "datetime_utc": "2023-01-06T10:00:00Z", + "tz_time": "2023-01-06T10:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 152.3, + "datetime": "2023-01-06T11:00:00.000+01:00", + "datetime_utc": "2023-01-06T10:00:00Z", + "tz_time": "2023-01-06T10:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 152.3, + "datetime": "2023-01-06T11:00:00.000+01:00", + "datetime_utc": "2023-01-06T10:00:00Z", + "tz_time": "2023-01-06T10:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 152.3, + "datetime": "2023-01-06T11:00:00.000+01:00", + "datetime_utc": "2023-01-06T10:00:00Z", + "tz_time": "2023-01-06T10:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 144.54, + "datetime": "2023-01-06T12:00:00.000+01:00", + "datetime_utc": "2023-01-06T11:00:00Z", + "tz_time": "2023-01-06T11:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 144.54, + "datetime": "2023-01-06T12:00:00.000+01:00", + "datetime_utc": "2023-01-06T11:00:00Z", + "tz_time": "2023-01-06T11:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 144.54, + "datetime": "2023-01-06T12:00:00.000+01:00", + "datetime_utc": "2023-01-06T11:00:00Z", + "tz_time": "2023-01-06T11:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 144.54, + "datetime": "2023-01-06T12:00:00.000+01:00", + "datetime_utc": "2023-01-06T11:00:00Z", + "tz_time": "2023-01-06T11:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 144.54, + "datetime": "2023-01-06T12:00:00.000+01:00", + "datetime_utc": "2023-01-06T11:00:00Z", + "tz_time": "2023-01-06T11:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 132.08, + "datetime": "2023-01-06T13:00:00.000+01:00", + "datetime_utc": "2023-01-06T12:00:00Z", + "tz_time": "2023-01-06T12:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 132.08, + "datetime": "2023-01-06T13:00:00.000+01:00", + "datetime_utc": "2023-01-06T12:00:00Z", + "tz_time": "2023-01-06T12:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 132.08, + "datetime": "2023-01-06T13:00:00.000+01:00", + "datetime_utc": "2023-01-06T12:00:00Z", + "tz_time": "2023-01-06T12:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 132.08, + "datetime": "2023-01-06T13:00:00.000+01:00", + "datetime_utc": "2023-01-06T12:00:00Z", + "tz_time": "2023-01-06T12:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 132.08, + "datetime": "2023-01-06T13:00:00.000+01:00", + "datetime_utc": "2023-01-06T12:00:00Z", + "tz_time": "2023-01-06T12:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 119.6, + "datetime": "2023-01-06T14:00:00.000+01:00", + "datetime_utc": "2023-01-06T13:00:00Z", + "tz_time": "2023-01-06T13:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 119.6, + "datetime": "2023-01-06T14:00:00.000+01:00", + "datetime_utc": "2023-01-06T13:00:00Z", + "tz_time": "2023-01-06T13:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 119.6, + "datetime": "2023-01-06T14:00:00.000+01:00", + "datetime_utc": "2023-01-06T13:00:00Z", + "tz_time": "2023-01-06T13:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 119.6, + "datetime": "2023-01-06T14:00:00.000+01:00", + "datetime_utc": "2023-01-06T13:00:00Z", + "tz_time": "2023-01-06T13:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 119.6, + "datetime": "2023-01-06T14:00:00.000+01:00", + "datetime_utc": "2023-01-06T13:00:00Z", + "tz_time": "2023-01-06T13:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 108.74, + "datetime": "2023-01-06T15:00:00.000+01:00", + "datetime_utc": "2023-01-06T14:00:00Z", + "tz_time": "2023-01-06T14:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 108.74, + "datetime": "2023-01-06T15:00:00.000+01:00", + "datetime_utc": "2023-01-06T14:00:00Z", + "tz_time": "2023-01-06T14:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 108.74, + "datetime": "2023-01-06T15:00:00.000+01:00", + "datetime_utc": "2023-01-06T14:00:00Z", + "tz_time": "2023-01-06T14:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 108.74, + "datetime": "2023-01-06T15:00:00.000+01:00", + "datetime_utc": "2023-01-06T14:00:00Z", + "tz_time": "2023-01-06T14:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 108.74, + "datetime": "2023-01-06T15:00:00.000+01:00", + "datetime_utc": "2023-01-06T14:00:00Z", + "tz_time": "2023-01-06T14:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 123.79, + "datetime": "2023-01-06T16:00:00.000+01:00", + "datetime_utc": "2023-01-06T15:00:00Z", + "tz_time": "2023-01-06T15:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 123.79, + "datetime": "2023-01-06T16:00:00.000+01:00", + "datetime_utc": "2023-01-06T15:00:00Z", + "tz_time": "2023-01-06T15:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 123.79, + "datetime": "2023-01-06T16:00:00.000+01:00", + "datetime_utc": "2023-01-06T15:00:00Z", + "tz_time": "2023-01-06T15:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 123.79, + "datetime": "2023-01-06T16:00:00.000+01:00", + "datetime_utc": "2023-01-06T15:00:00Z", + "tz_time": "2023-01-06T15:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 123.79, + "datetime": "2023-01-06T16:00:00.000+01:00", + "datetime_utc": "2023-01-06T15:00:00Z", + "tz_time": "2023-01-06T15:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 166.41, + "datetime": "2023-01-06T17:00:00.000+01:00", + "datetime_utc": "2023-01-06T16:00:00Z", + "tz_time": "2023-01-06T16:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 166.41, + "datetime": "2023-01-06T17:00:00.000+01:00", + "datetime_utc": "2023-01-06T16:00:00Z", + "tz_time": "2023-01-06T16:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 166.41, + "datetime": "2023-01-06T17:00:00.000+01:00", + "datetime_utc": "2023-01-06T16:00:00Z", + "tz_time": "2023-01-06T16:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 166.41, + "datetime": "2023-01-06T17:00:00.000+01:00", + "datetime_utc": "2023-01-06T16:00:00Z", + "tz_time": "2023-01-06T16:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 166.41, + "datetime": "2023-01-06T17:00:00.000+01:00", + "datetime_utc": "2023-01-06T16:00:00Z", + "tz_time": "2023-01-06T16:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 173.49, + "datetime": "2023-01-06T18:00:00.000+01:00", + "datetime_utc": "2023-01-06T17:00:00Z", + "tz_time": "2023-01-06T17:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 173.49, + "datetime": "2023-01-06T18:00:00.000+01:00", + "datetime_utc": "2023-01-06T17:00:00Z", + "tz_time": "2023-01-06T17:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 173.49, + "datetime": "2023-01-06T18:00:00.000+01:00", + "datetime_utc": "2023-01-06T17:00:00Z", + "tz_time": "2023-01-06T17:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 173.49, + "datetime": "2023-01-06T18:00:00.000+01:00", + "datetime_utc": "2023-01-06T17:00:00Z", + "tz_time": "2023-01-06T17:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 173.49, + "datetime": "2023-01-06T18:00:00.000+01:00", + "datetime_utc": "2023-01-06T17:00:00Z", + "tz_time": "2023-01-06T17:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 186.17, + "datetime": "2023-01-06T19:00:00.000+01:00", + "datetime_utc": "2023-01-06T18:00:00Z", + "tz_time": "2023-01-06T18:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 186.17, + "datetime": "2023-01-06T19:00:00.000+01:00", + "datetime_utc": "2023-01-06T18:00:00Z", + "tz_time": "2023-01-06T18:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 186.17, + "datetime": "2023-01-06T19:00:00.000+01:00", + "datetime_utc": "2023-01-06T18:00:00Z", + "tz_time": "2023-01-06T18:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 186.17, + "datetime": "2023-01-06T19:00:00.000+01:00", + "datetime_utc": "2023-01-06T18:00:00Z", + "tz_time": "2023-01-06T18:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 186.17, + "datetime": "2023-01-06T19:00:00.000+01:00", + "datetime_utc": "2023-01-06T18:00:00Z", + "tz_time": "2023-01-06T18:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 186.11, + "datetime": "2023-01-06T20:00:00.000+01:00", + "datetime_utc": "2023-01-06T19:00:00Z", + "tz_time": "2023-01-06T19:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 186.11, + "datetime": "2023-01-06T20:00:00.000+01:00", + "datetime_utc": "2023-01-06T19:00:00Z", + "tz_time": "2023-01-06T19:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 186.11, + "datetime": "2023-01-06T20:00:00.000+01:00", + "datetime_utc": "2023-01-06T19:00:00Z", + "tz_time": "2023-01-06T19:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 186.11, + "datetime": "2023-01-06T20:00:00.000+01:00", + "datetime_utc": "2023-01-06T19:00:00Z", + "tz_time": "2023-01-06T19:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 186.11, + "datetime": "2023-01-06T20:00:00.000+01:00", + "datetime_utc": "2023-01-06T19:00:00Z", + "tz_time": "2023-01-06T19:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 178.45, + "datetime": "2023-01-06T21:00:00.000+01:00", + "datetime_utc": "2023-01-06T20:00:00Z", + "tz_time": "2023-01-06T20:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 178.45, + "datetime": "2023-01-06T21:00:00.000+01:00", + "datetime_utc": "2023-01-06T20:00:00Z", + "tz_time": "2023-01-06T20:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 178.45, + "datetime": "2023-01-06T21:00:00.000+01:00", + "datetime_utc": "2023-01-06T20:00:00Z", + "tz_time": "2023-01-06T20:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 178.45, + "datetime": "2023-01-06T21:00:00.000+01:00", + "datetime_utc": "2023-01-06T20:00:00Z", + "tz_time": "2023-01-06T20:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 178.45, + "datetime": "2023-01-06T21:00:00.000+01:00", + "datetime_utc": "2023-01-06T20:00:00Z", + "tz_time": "2023-01-06T20:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 139.37, + "datetime": "2023-01-06T22:00:00.000+01:00", + "datetime_utc": "2023-01-06T21:00:00Z", + "tz_time": "2023-01-06T21:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 139.37, + "datetime": "2023-01-06T22:00:00.000+01:00", + "datetime_utc": "2023-01-06T21:00:00Z", + "tz_time": "2023-01-06T21:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 139.37, + "datetime": "2023-01-06T22:00:00.000+01:00", + "datetime_utc": "2023-01-06T21:00:00Z", + "tz_time": "2023-01-06T21:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 139.37, + "datetime": "2023-01-06T22:00:00.000+01:00", + "datetime_utc": "2023-01-06T21:00:00Z", + "tz_time": "2023-01-06T21:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 139.37, + "datetime": "2023-01-06T22:00:00.000+01:00", + "datetime_utc": "2023-01-06T21:00:00Z", + "tz_time": "2023-01-06T21:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + }, + { + "value": 129.35, + "datetime": "2023-01-06T23:00:00.000+01:00", + "datetime_utc": "2023-01-06T22:00:00Z", + "tz_time": "2023-01-06T22:00:00.000Z", + "geo_id": 8741, + "geo_name": "Península" + }, + { + "value": 129.35, + "datetime": "2023-01-06T23:00:00.000+01:00", + "datetime_utc": "2023-01-06T22:00:00Z", + "tz_time": "2023-01-06T22:00:00.000Z", + "geo_id": 8742, + "geo_name": "Canarias" + }, + { + "value": 129.35, + "datetime": "2023-01-06T23:00:00.000+01:00", + "datetime_utc": "2023-01-06T22:00:00Z", + "tz_time": "2023-01-06T22:00:00.000Z", + "geo_id": 8743, + "geo_name": "Baleares" + }, + { + "value": 129.35, + "datetime": "2023-01-06T23:00:00.000+01:00", + "datetime_utc": "2023-01-06T22:00:00Z", + "tz_time": "2023-01-06T22:00:00.000Z", + "geo_id": 8744, + "geo_name": "Ceuta" + }, + { + "value": 129.35, + "datetime": "2023-01-06T23:00:00.000+01:00", + "datetime_utc": "2023-01-06T22:00:00Z", + "tz_time": "2023-01-06T22:00:00.000Z", + "geo_id": 8745, + "geo_name": "Melilla" + } + ] + } +} diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 6560c81ebbb..950aea8e32c 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -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