mirror of
https://github.com/home-assistant/core.git
synced 2025-12-03 14:38:06 +00:00
Compare commits
1 Commits
knx-data-s
...
gj-2025110
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaa19701b2 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -831,7 +831,7 @@
|
||||
"exchangeRate": 11.05186,
|
||||
"areaStates": [
|
||||
{
|
||||
"state": "Final",
|
||||
"state": "Preliminary",
|
||||
"areas": ["SE3", "SE4"]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -831,7 +831,7 @@
|
||||
"exchangeRate": 11.03362,
|
||||
"areaStates": [
|
||||
{
|
||||
"state": "Final",
|
||||
"state": "Preliminary",
|
||||
"areas": ["SE3", "SE4"]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
'SE3',
|
||||
'SE4',
|
||||
]),
|
||||
'state': 'Final',
|
||||
'state': 'Preliminary',
|
||||
}),
|
||||
]),
|
||||
'blockPriceAggregates': list([
|
||||
@@ -1151,7 +1151,7 @@
|
||||
'SE3',
|
||||
'SE4',
|
||||
]),
|
||||
'state': 'Final',
|
||||
'state': 'Preliminary',
|
||||
}),
|
||||
]),
|
||||
'blockPriceAggregates': list([
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user