Fix Nord Pool empty response (#134033)

* Fix Nord Pool empty response

* Mods

* reset validate prices
This commit is contained in:
G Johansson 2024-12-28 21:38:04 +01:00 committed by GitHub
parent b3aede611a
commit 645f2e44b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 155 additions and 24 deletions

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -73,7 +72,7 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow()) self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow())
) )
data = await self.api_call() data = await self.api_call()
if data: if data and data.entries:
self.async_set_updated_data(data) self.async_set_updated_data(data)
async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None: async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None:
@ -90,18 +89,20 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
self.config_entry.data[CONF_AREAS], self.config_entry.data[CONF_AREAS],
) )
except ( except (
NordPoolEmptyResponseError,
NordPoolResponseError, NordPoolResponseError,
NordPoolError, NordPoolError,
) as error: ) as error:
LOGGER.debug("Connection error: %s", error) LOGGER.debug("Connection error: %s", error)
if retry > 0:
next_run = (4 - retry) * 15
LOGGER.debug("Wait %d seconds for next try", next_run)
await asyncio.sleep(next_run)
return await self.api_call(retry - 1)
self.async_set_update_error(error) self.async_set_update_error(error)
if data:
current_day = dt_util.utcnow().strftime("%Y-%m-%d")
for entry in data.entries:
if entry.requested_date == current_day:
LOGGER.debug("Data for current day found")
return data
self.async_set_update_error(NordPoolEmptyResponseError("No current day data"))
return data return data
def merge_price_entries(self) -> list[DeliveryPeriodEntry]: def merge_price_entries(self) -> list[DeliveryPeriodEntry]:

View File

@ -8,6 +8,6 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pynordpool"], "loggers": ["pynordpool"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pynordpool==0.2.3"], "requirements": ["pynordpool==0.2.4"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -2118,7 +2118,7 @@ pynetio==0.1.9.1
pynobo==1.8.1 pynobo==1.8.1
# homeassistant.components.nordpool # homeassistant.components.nordpool
pynordpool==0.2.3 pynordpool==0.2.4
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.6.3 pynuki==1.6.3

View File

@ -1720,7 +1720,7 @@ pynetgear==0.10.10
pynobo==1.8.1 pynobo==1.8.1
# homeassistant.components.nordpool # homeassistant.components.nordpool
pynordpool==0.2.3 pynordpool==0.2.4
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.6.3 pynuki==1.6.3

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
import json import json
from typing import Any from typing import Any
from unittest.mock import patch
from pynordpool import API, NordPoolClient from pynordpool import API, NordPoolClient
import pytest import pytest
@ -20,13 +19,6 @@ from tests.common import MockConfigEntry, load_fixture
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.fixture(autouse=True)
async def no_sleep() -> AsyncGenerator[None]:
"""No sleeping."""
with patch("homeassistant.components.nordpool.coordinator.asyncio.sleep"):
yield
@pytest.fixture @pytest.fixture
async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry: async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry:
"""Set up the Nord Pool integration in Home Assistant.""" """Set up the Nord Pool integration in Home Assistant."""

View File

@ -55,7 +55,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1)) freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4 assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price") state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@ -69,7 +69,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1)) freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4 assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price") state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert "Authentication error" in caplog.text assert "Authentication error" in caplog.text
@ -84,7 +84,8 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1)) freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4 # Empty responses does not raise
assert mock_data.call_count == 3
state = hass.states.get("sensor.nord_pool_se3_current_price") state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert "Empty response" in caplog.text assert "Empty response" in caplog.text
@ -99,7 +100,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1)) freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4 assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price") state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert "Response error" in caplog.text assert "Response error" in caplog.text

View File

@ -2,14 +2,22 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from http import HTTPStatus
from typing import Any
from freezegun.api import FrozenDateTimeFactory
from pynordpool import API
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from tests.common import snapshot_platform from tests.common import async_fire_time_changed, snapshot_platform
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00")
@ -59,3 +67,132 @@ async def test_sensor_no_previous_price(
assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z
assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z
assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z
@pytest.mark.freeze_time("2024-11-05T11:00:01+01:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_empty_response(
hass: HomeAssistant,
load_int: ConfigEntry,
load_json: list[dict[str, Any]],
aioclient_mock: AiohttpClientMocker,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the Nord Pool sensor with empty response."""
responses = list(load_json)
current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.92737"
assert last_price.state == "1.03132"
assert next_price.state == "0.92505"
aioclient_mock.clear_requests()
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-04",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[1],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[0],
)
# Future date without data should return 204
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-06",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
status=HTTPStatus.NO_CONTENT,
)
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# All prices should be known as tomorrow is not loaded by sensors
current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.92505"
assert last_price.state == "0.92737"
assert next_price.state == "0.94949"
aioclient_mock.clear_requests()
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-04",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[1],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[0],
)
# Future date without data should return 204
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-06",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
status=HTTPStatus.NO_CONTENT,
)
freezer.move_to("2024-11-05T22:00:01+00:00")
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Current and last price should be known, next price should be unknown
# as api responds with empty data (204)
current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.28914"
assert last_price.state == "0.5223"
assert next_price.state == STATE_UNKNOWN