diff --git a/homeassistant/components/nordpool/coordinator.py b/homeassistant/components/nordpool/coordinator.py index f2f41322aff2..506e4a2ac3f1 100644 --- a/homeassistant/components/nordpool/coordinator.py +++ b/homeassistant/components/nordpool/coordinator.py @@ -49,6 +49,12 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]): def get_next_data_interval(self, now: datetime) -> datetime: """Compute next time an update should occur.""" + if self.is_prices_final(): + # Prices are final for the current day, update next day + LOGGER.debug("Prices are final for the current day") + if tomorrow_starts_at := self.tomorrow_starts_at(): + LOGGER.debug("Next data update at %s", tomorrow_starts_at) + return tomorrow_starts_at next_hour = dt_util.utcnow() + timedelta(hours=1) next_run = datetime( next_hour.year, @@ -92,17 +98,22 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]): async def fetch_data(self, now: datetime, initial: bool = False) -> None: """Fetch data from Nord Pool.""" - self.data_unsub = async_track_point_in_utc_time( - self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow()) - ) if self.config_entry.pref_disable_polling and not initial: + self.data_unsub = async_track_point_in_utc_time( + self.hass, + self.fetch_data, + self.get_next_data_interval(dt_util.utcnow()), + ) return try: data = await self.handle_data(initial) except UpdateFailed as err: self.async_set_update_error(err) - return - self.async_set_updated_data(data) + else: + self.async_set_updated_data(data) + self.data_unsub = async_track_point_in_utc_time( + self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow()) + ) async def handle_data(self, initial: bool = False) -> DeliveryPeriodsData: """Fetch data from Nord Pool.""" @@ -171,3 +182,25 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]): delivery_period = del_period break return delivery_period + + def is_prices_final(self) -> bool: + """Return True if prices for the current day are final.""" + current_day = dt_util.utcnow().strftime("%Y-%m-%d") + if not self.data: + return False + for del_period in self.data.entries: + if del_period.requested_date == current_day and del_period.prices_final: + return True + return False + + def tomorrow_starts_at(self) -> datetime | None: + """Return tomorrow's starting time.""" + # What if tomorrow file not exist? + + tomorrow = dt_util.utcnow() + timedelta(days=1) + tomorrow_day = tomorrow.strftime("%Y-%m-%d") + for del_period in self.data.entries: + if del_period.requested_date == tomorrow_day: + sorted_tomorrow = sorted(del_period.entries, key=lambda x: x.start) + return sorted_tomorrow[0].start + return None diff --git a/homeassistant/components/nordpool/sensor.py b/homeassistant/components/nordpool/sensor.py index 4295691f8f43..712450d9a617 100644 --- a/homeassistant/components/nordpool/sensor.py +++ b/homeassistant/components/nordpool/sensor.py @@ -54,7 +54,7 @@ def get_prices( current_price_entries: dict[str, float] = {} next_price_entries: dict[str, float] = {} current_time = dt_util.utcnow() - LOGGER.debug("Price data: %s", data) + # LOGGER.debug("Price data: %s", data) for entry in data: resolution = entry.end - entry.start previous_time = current_time - resolution diff --git a/tests/components/nordpool/fixtures/delivery_period_today.json b/tests/components/nordpool/fixtures/delivery_period_today.json index ecd7b3868021..aa8f104658f0 100644 --- a/tests/components/nordpool/fixtures/delivery_period_today.json +++ b/tests/components/nordpool/fixtures/delivery_period_today.json @@ -831,7 +831,7 @@ "exchangeRate": 11.05186, "areaStates": [ { - "state": "Final", + "state": "Preliminary", "areas": ["SE3", "SE4"] } ], diff --git a/tests/components/nordpool/fixtures/delivery_period_tomorrow.json b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json index 0e64088d33bf..eeaade71b8d3 100644 --- a/tests/components/nordpool/fixtures/delivery_period_tomorrow.json +++ b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json @@ -831,7 +831,7 @@ "exchangeRate": 11.03362, "areaStates": [ { - "state": "Final", + "state": "Preliminary", "areas": ["SE3", "SE4"] } ], diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr index a4434a1246a2..4dd3bb290354 100644 --- a/tests/components/nordpool/snapshots/test_diagnostics.ambr +++ b/tests/components/nordpool/snapshots/test_diagnostics.ambr @@ -297,7 +297,7 @@ 'SE3', 'SE4', ]), - 'state': 'Final', + 'state': 'Preliminary', }), ]), 'blockPriceAggregates': list([ @@ -1151,7 +1151,7 @@ 'SE3', 'SE4', ]), - 'state': 'Final', + 'state': 'Preliminary', }), ]), 'blockPriceAggregates': list([ diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index 94d66a789cc5..8a17da0e91da 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -3,11 +3,13 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from unittest.mock import patch import aiohttp from freezegun.api import FrozenDateTimeFactory from pynordpool import ( + API, NordPoolAuthenticationError, NordPoolClient, NordPoolEmptyResponseError, @@ -21,14 +23,16 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.nordpool.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") @@ -223,3 +227,117 @@ async def test_coordinator( state = hass.states.get("sensor.nord_pool_se3_current_price") assert state.state == STATE_UNAVAILABLE assert "Data for current day is missing" in caplog.text + + +@pytest.mark.freeze_time("2025-10-01T10:00:00+00:00") +async def test_coordinator_updating_for_final_prices( + hass: HomeAssistant, + load_int: ConfigEntry, + load_json: list[dict[str, Any]], + get_client: NordPoolClient, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the Nord Pool coordinator with errors.""" + responses = list(load_json) + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.67405" + + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + wraps=get_client.async_get_delivery_period, + ) as mock_data, + ): + 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 == 3 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.63736" + + 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 == 6 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.62233" + + todays_response = responses[0] + todays_response["areaStates"][0]["state"] = "Final" + + aioclient_mock.clear_requests() + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2025-09-30", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[1], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2025-10-01", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[0], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2025-10-02", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[2], + ) + + 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 == 9 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.6294" + + 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 == 9 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.61526" + + 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 == 9 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.98052" + + freezer.move_to(dt_util.utcnow().replace(hour=22)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert mock_data.call_count == 12 + + 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 == 15 + + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.83513"