diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py index 0c9a7e9f337..a6cfd40c323 100644 --- a/homeassistant/components/nordpool/coordinator.py +++ b/homeassistant/components/nordpool/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from collections.abc import Callable from datetime import datetime, timedelta 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()) ) data = await self.api_call() - if data: + if data and data.entries: self.async_set_updated_data(data) async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None: @@ -90,18 +89,20 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]): self.config_entry.data[CONF_AREAS], ) except ( - NordPoolEmptyResponseError, NordPoolResponseError, NordPoolError, ) as 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) + 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 def merge_price_entries(self) -> list[DeliveryPeriodEntry]: diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json index 215494e10a0..b096d2bd506 100644 --- a/homeassistant/components/nordpool/manifest.json +++ b/homeassistant/components/nordpool/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["pynordpool"], "quality_scale": "platinum", - "requirements": ["pynordpool==0.2.3"], + "requirements": ["pynordpool==0.2.4"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 2988073f2a3..0c48fe1ab2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ pynetio==0.1.9.1 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.3 +pynordpool==0.2.4 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c13cad719ca..e092afbe528 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1720,7 +1720,7 @@ pynetgear==0.10.10 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.3 +pynordpool==0.2.4 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/tests/components/nordpool/conftest.py b/tests/components/nordpool/conftest.py index 1c26c7f84eb..ca1e2a05a0b 100644 --- a/tests/components/nordpool/conftest.py +++ b/tests/components/nordpool/conftest.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import AsyncGenerator import json from typing import Any -from unittest.mock import patch from pynordpool import API, NordPoolClient import pytest @@ -20,13 +19,6 @@ from tests.common import MockConfigEntry, load_fixture 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 async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry: """Set up the Nord Pool integration in Home Assistant.""" diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index 7647fe4bdfe..71c4644ea95 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -55,7 +55,7 @@ async def test_coordinator( freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) 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") assert state.state == STATE_UNAVAILABLE @@ -69,7 +69,7 @@ async def test_coordinator( freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) 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") assert state.state == STATE_UNAVAILABLE assert "Authentication error" in caplog.text @@ -84,7 +84,8 @@ async def test_coordinator( freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) 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") assert state.state == STATE_UNAVAILABLE assert "Empty response" in caplog.text @@ -99,7 +100,7 @@ async def test_coordinator( freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) 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") assert state.state == STATE_UNAVAILABLE assert "Response error" in caplog.text diff --git a/tests/components/nordpool/test_sensor.py b/tests/components/nordpool/test_sensor.py index a1a27b5feec..60be1ee3258 100644 --- a/tests/components/nordpool/test_sensor.py +++ b/tests/components/nordpool/test_sensor.py @@ -2,14 +2,22 @@ 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 from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant 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") @@ -59,3 +67,132 @@ async def test_sensor_no_previous_price( 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 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