Load data for multiple days in Nord Pool (#133371)

* Load data for multiple days in Nord Pool

* Fix current day

* Fix tests

* Fix services

* Fix fixtures

* Mod get_data_current_day

* Mods

* simplify further
pull/133825/head^2
G Johansson 2024-12-22 21:10:12 +01:00 committed by GitHub
parent 26180486e7
commit 368e958457
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1582 additions and 462 deletions

View File

@ -10,6 +10,8 @@ from typing import TYPE_CHECKING
from pynordpool import (
Currency,
DeliveryPeriodData,
DeliveryPeriodEntry,
DeliveryPeriodsData,
NordPoolClient,
NordPoolEmptyResponseError,
NordPoolError,
@ -29,7 +31,7 @@ if TYPE_CHECKING:
from . import NordPoolConfigEntry
class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
"""A Nord Pool Data Update Coordinator."""
config_entry: NordPoolConfigEntry
@ -74,12 +76,16 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
if data:
self.async_set_updated_data(data)
async def api_call(self, retry: int = 3) -> DeliveryPeriodData | None:
async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None:
"""Make api call to retrieve data with retry if failure."""
data = None
try:
data = await self.client.async_get_delivery_period(
dt_util.now(),
data = await self.client.async_get_delivery_periods(
[
dt_util.now() - timedelta(days=1),
dt_util.now(),
dt_util.now() + timedelta(days=1),
],
Currency(self.config_entry.data[CONF_CURRENCY]),
self.config_entry.data[CONF_AREAS],
)
@ -97,3 +103,20 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
self.async_set_update_error(error)
return data
def merge_price_entries(self) -> list[DeliveryPeriodEntry]:
"""Return the merged price entries."""
merged_entries: list[DeliveryPeriodEntry] = []
for del_period in self.data.entries:
merged_entries.extend(del_period.entries)
return merged_entries
def get_data_current_day(self) -> DeliveryPeriodData:
"""Return the current day data."""
current_day = dt_util.utcnow().strftime("%Y-%m-%d")
delivery_period: DeliveryPeriodData = self.data.entries[0]
for del_period in self.data.entries:
if del_period.requested_date == current_day:
delivery_period = del_period
break
return delivery_period

View File

@ -6,8 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from pynordpool import DeliveryPeriodData
from homeassistant.components.sensor import (
EntityCategory,
SensorDeviceClass,
@ -29,34 +27,34 @@ PARALLEL_UPDATES = 0
def validate_prices(
func: Callable[
[DeliveryPeriodData], dict[str, tuple[float | None, float, float | None]]
[NordpoolPriceSensor], dict[str, tuple[float | None, float, float | None]]
],
data: DeliveryPeriodData,
entity: NordpoolPriceSensor,
area: str,
index: int,
) -> float | None:
"""Validate and return."""
if result := func(data)[area][index]:
if result := func(entity)[area][index]:
return result / 1000
return None
def get_prices(
data: DeliveryPeriodData,
entity: NordpoolPriceSensor,
) -> dict[str, tuple[float | None, float, float | None]]:
"""Return previous, current and next prices.
Output: {"SE3": (10.0, 10.5, 12.1)}
"""
data = entity.coordinator.merge_price_entries()
last_price_entries: dict[str, float] = {}
current_price_entries: dict[str, float] = {}
next_price_entries: dict[str, float] = {}
current_time = dt_util.utcnow()
previous_time = current_time - timedelta(hours=1)
next_time = current_time + timedelta(hours=1)
price_data = data.entries
LOGGER.debug("Price data: %s", price_data)
for entry in price_data:
LOGGER.debug("Price data: %s", data)
for entry in data:
if entry.start <= current_time <= entry.end:
current_price_entries = entry.entry
if entry.start <= previous_time <= entry.end:
@ -82,11 +80,12 @@ def get_prices(
def get_min_max_price(
data: DeliveryPeriodData,
area: str,
entity: NordpoolPriceSensor,
func: Callable[[float, float], float],
) -> tuple[float, datetime, datetime]:
"""Get the lowest price from the data."""
data = entity.coordinator.get_data_current_day()
area = entity.area
price_data = data.entries
price: float = price_data[0].entry[area]
start: datetime = price_data[0].start
@ -102,12 +101,13 @@ def get_min_max_price(
def get_blockprices(
data: DeliveryPeriodData,
entity: NordpoolBlockPriceSensor,
) -> dict[str, dict[str, tuple[datetime, datetime, float, float, float]]]:
"""Return average, min and max for block prices.
Output: {"SE3": {"Off-peak 1": (_datetime_, _datetime_, 9.3, 10.5, 12.1)}}
"""
data = entity.coordinator.get_data_current_day()
result: dict[str, dict[str, tuple[datetime, datetime, float, float, float]]] = {}
block_prices = data.block_prices
for entry in block_prices:
@ -130,15 +130,15 @@ def get_blockprices(
class NordpoolDefaultSensorEntityDescription(SensorEntityDescription):
"""Describes Nord Pool default sensor entity."""
value_fn: Callable[[DeliveryPeriodData], str | float | datetime | None]
value_fn: Callable[[NordpoolSensor], str | float | datetime | None]
@dataclass(frozen=True, kw_only=True)
class NordpoolPricesSensorEntityDescription(SensorEntityDescription):
"""Describes Nord Pool prices sensor entity."""
value_fn: Callable[[DeliveryPeriodData, str], float | None]
extra_fn: Callable[[DeliveryPeriodData, str], dict[str, str] | None]
value_fn: Callable[[NordpoolPriceSensor], float | None]
extra_fn: Callable[[NordpoolPriceSensor], dict[str, str] | None]
@dataclass(frozen=True, kw_only=True)
@ -155,19 +155,19 @@ DEFAULT_SENSOR_TYPES: tuple[NordpoolDefaultSensorEntityDescription, ...] = (
key="updated_at",
translation_key="updated_at",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.updated_at,
value_fn=lambda entity: entity.coordinator.get_data_current_day().updated_at,
entity_category=EntityCategory.DIAGNOSTIC,
),
NordpoolDefaultSensorEntityDescription(
key="currency",
translation_key="currency",
value_fn=lambda data: data.currency,
value_fn=lambda entity: entity.coordinator.get_data_current_day().currency,
entity_category=EntityCategory.DIAGNOSTIC,
),
NordpoolDefaultSensorEntityDescription(
key="exchange_rate",
translation_key="exchange_rate",
value_fn=lambda data: data.exchange_rate,
value_fn=lambda entity: entity.coordinator.get_data_current_day().exchange_rate,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
@ -177,42 +177,42 @@ PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
NordpoolPricesSensorEntityDescription(
key="current_price",
translation_key="current_price",
value_fn=lambda data, area: validate_prices(get_prices, data, area, 1),
extra_fn=lambda data, area: None,
value_fn=lambda entity: validate_prices(get_prices, entity, entity.area, 1),
extra_fn=lambda entity: None,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
NordpoolPricesSensorEntityDescription(
key="last_price",
translation_key="last_price",
value_fn=lambda data, area: validate_prices(get_prices, data, area, 0),
extra_fn=lambda data, area: None,
value_fn=lambda entity: validate_prices(get_prices, entity, entity.area, 0),
extra_fn=lambda entity: None,
suggested_display_precision=2,
),
NordpoolPricesSensorEntityDescription(
key="next_price",
translation_key="next_price",
value_fn=lambda data, area: validate_prices(get_prices, data, area, 2),
extra_fn=lambda data, area: None,
value_fn=lambda entity: validate_prices(get_prices, entity, entity.area, 2),
extra_fn=lambda entity: None,
suggested_display_precision=2,
),
NordpoolPricesSensorEntityDescription(
key="lowest_price",
translation_key="lowest_price",
value_fn=lambda data, area: get_min_max_price(data, area, min)[0] / 1000,
extra_fn=lambda data, area: {
"start": get_min_max_price(data, area, min)[1].isoformat(),
"end": get_min_max_price(data, area, min)[2].isoformat(),
value_fn=lambda entity: get_min_max_price(entity, min)[0] / 1000,
extra_fn=lambda entity: {
"start": get_min_max_price(entity, min)[1].isoformat(),
"end": get_min_max_price(entity, min)[2].isoformat(),
},
suggested_display_precision=2,
),
NordpoolPricesSensorEntityDescription(
key="highest_price",
translation_key="highest_price",
value_fn=lambda data, area: get_min_max_price(data, area, max)[0] / 1000,
extra_fn=lambda data, area: {
"start": get_min_max_price(data, area, max)[1].isoformat(),
"end": get_min_max_price(data, area, max)[2].isoformat(),
value_fn=lambda entity: get_min_max_price(entity, max)[0] / 1000,
extra_fn=lambda entity: {
"start": get_min_max_price(entity, max)[1].isoformat(),
"end": get_min_max_price(entity, max)[2].isoformat(),
},
suggested_display_precision=2,
),
@ -276,11 +276,12 @@ async def async_setup_entry(
"""Set up Nord Pool sensor platform."""
coordinator = entry.runtime_data
current_day_data = entry.runtime_data.get_data_current_day()
entities: list[NordpoolBaseEntity] = []
currency = entry.runtime_data.data.currency
currency = current_day_data.currency
for area in get_prices(entry.runtime_data.data):
for area in current_day_data.area_average:
LOGGER.debug("Setting up base sensors for area %s", area)
entities.extend(
NordpoolSensor(coordinator, description, area)
@ -297,16 +298,16 @@ async def async_setup_entry(
NordpoolDailyAveragePriceSensor(coordinator, description, area, currency)
for description in DAILY_AVERAGE_PRICES_SENSOR_TYPES
)
for block_name in get_blockprices(coordinator.data)[area]:
for block_prices in entry.runtime_data.get_data_current_day().block_prices:
LOGGER.debug(
"Setting up block price sensors for area %s with currency %s in block %s",
area,
currency,
block_name,
block_prices.name,
)
entities.extend(
NordpoolBlockPriceSensor(
coordinator, description, area, currency, block_name
coordinator, description, area, currency, block_prices.name
)
for description in BLOCK_PRICES_SENSOR_TYPES
)
@ -321,7 +322,7 @@ class NordpoolSensor(NordpoolBaseEntity, SensorEntity):
@property
def native_value(self) -> str | float | datetime | None:
"""Return value of sensor."""
return self.entity_description.value_fn(self.coordinator.data)
return self.entity_description.value_fn(self)
class NordpoolPriceSensor(NordpoolBaseEntity, SensorEntity):
@ -343,12 +344,12 @@ class NordpoolPriceSensor(NordpoolBaseEntity, SensorEntity):
@property
def native_value(self) -> float | None:
"""Return value of sensor."""
return self.entity_description.value_fn(self.coordinator.data, self.area)
return self.entity_description.value_fn(self)
@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the extra state attributes."""
return self.entity_description.extra_fn(self.coordinator.data, self.area)
return self.entity_description.extra_fn(self)
class NordpoolBlockPriceSensor(NordpoolBaseEntity, SensorEntity):
@ -376,7 +377,7 @@ class NordpoolBlockPriceSensor(NordpoolBaseEntity, SensorEntity):
def native_value(self) -> float | datetime | None:
"""Return value of sensor."""
return self.entity_description.value_fn(
get_blockprices(self.coordinator.data)[self.area][self.block_name]
get_blockprices(self)[self.area][self.block_name]
)
@ -399,4 +400,5 @@ class NordpoolDailyAveragePriceSensor(NordpoolBaseEntity, SensorEntity):
@property
def native_value(self) -> float | None:
"""Return value of sensor."""
return self.coordinator.data.area_average[self.area] / 1000
data = self.coordinator.get_data_current_day()
return data.area_average[self.area] / 1000

View File

@ -3,20 +3,16 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from datetime import datetime
import json
from typing import Any
from unittest.mock import patch
from pynordpool import NordPoolClient
from pynordpool.const import Currency
from pynordpool.model import DeliveryPeriodData
from pynordpool import API, NordPoolClient
import pytest
from homeassistant.components.nordpool.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from . import ENTRY_CONFIG
@ -32,9 +28,7 @@ async def no_sleep() -> AsyncGenerator[None]:
@pytest.fixture
async def load_int(
hass: HomeAssistant, get_data: DeliveryPeriodData
) -> MockConfigEntry:
async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry:
"""Set up the Nord Pool integration in Home Assistant."""
config_entry = MockConfigEntry(
domain=DOMAIN,
@ -44,40 +38,83 @@ async def load_int(
config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="get_data")
@pytest.fixture(name="get_client")
async def get_data_from_library(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, load_json: dict[str, Any]
) -> DeliveryPeriodData:
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
load_json: list[dict[str, Any]],
) -> AsyncGenerator[NordPoolClient]:
"""Retrieve data from Nord Pool library."""
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=load_json[0],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3",
"currency": "EUR",
},
json=load_json[0],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-04",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=load_json[1],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-06",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=load_json[2],
)
client = NordPoolClient(aioclient_mock.create_session(hass.loop))
with patch("pynordpool.NordPoolClient._get", return_value=load_json):
output = await client.async_get_delivery_period(
datetime(2024, 11, 5, 13, tzinfo=dt_util.UTC), Currency.SEK, ["SE3", "SE4"]
)
yield client
await client._session.close()
return output
@pytest.fixture(name="load_json")
def load_json_from_fixture(load_data: str) -> dict[str, Any]:
def load_json_from_fixture(load_data: list[str, str, str]) -> list[dict[str, Any]]:
"""Load fixture with json data and return."""
return json.loads(load_data)
return [
json.loads(load_data[0]),
json.loads(load_data[1]),
json.loads(load_data[2]),
]
@pytest.fixture(name="load_data", scope="package")
def load_data_from_fixture() -> str:
def load_data_from_fixture() -> list[str, str, str]:
"""Load fixture with fixture data and return."""
return load_fixture("delivery_period.json", DOMAIN)
return [
load_fixture("delivery_period_today.json", DOMAIN),
load_fixture("delivery_period_yesterday.json", DOMAIN),
load_fixture("delivery_period_tomorrow.json", DOMAIN),
]

View File

@ -0,0 +1,272 @@
{
"deliveryDateCET": "2024-11-06",
"version": 3,
"updatedAt": "2024-11-05T12:12:51.9853434Z",
"deliveryAreas": ["SE3", "SE4"],
"market": "DayAhead",
"multiAreaEntries": [
{
"deliveryStart": "2024-11-05T23:00:00Z",
"deliveryEnd": "2024-11-06T00:00:00Z",
"entryPerArea": {
"SE3": 126.66,
"SE4": 275.6
}
},
{
"deliveryStart": "2024-11-06T00:00:00Z",
"deliveryEnd": "2024-11-06T01:00:00Z",
"entryPerArea": {
"SE3": 74.06,
"SE4": 157.34
}
},
{
"deliveryStart": "2024-11-06T01:00:00Z",
"deliveryEnd": "2024-11-06T02:00:00Z",
"entryPerArea": {
"SE3": 78.38,
"SE4": 165.62
}
},
{
"deliveryStart": "2024-11-06T02:00:00Z",
"deliveryEnd": "2024-11-06T03:00:00Z",
"entryPerArea": {
"SE3": 92.37,
"SE4": 196.17
}
},
{
"deliveryStart": "2024-11-06T03:00:00Z",
"deliveryEnd": "2024-11-06T04:00:00Z",
"entryPerArea": {
"SE3": 99.14,
"SE4": 190.58
}
},
{
"deliveryStart": "2024-11-06T04:00:00Z",
"deliveryEnd": "2024-11-06T05:00:00Z",
"entryPerArea": {
"SE3": 447.51,
"SE4": 932.93
}
},
{
"deliveryStart": "2024-11-06T05:00:00Z",
"deliveryEnd": "2024-11-06T06:00:00Z",
"entryPerArea": {
"SE3": 641.47,
"SE4": 1284.69
}
},
{
"deliveryStart": "2024-11-06T06:00:00Z",
"deliveryEnd": "2024-11-06T07:00:00Z",
"entryPerArea": {
"SE3": 1820.5,
"SE4": 2449.96
}
},
{
"deliveryStart": "2024-11-06T07:00:00Z",
"deliveryEnd": "2024-11-06T08:00:00Z",
"entryPerArea": {
"SE3": 1723.0,
"SE4": 2244.22
}
},
{
"deliveryStart": "2024-11-06T08:00:00Z",
"deliveryEnd": "2024-11-06T09:00:00Z",
"entryPerArea": {
"SE3": 1298.57,
"SE4": 1643.45
}
},
{
"deliveryStart": "2024-11-06T09:00:00Z",
"deliveryEnd": "2024-11-06T10:00:00Z",
"entryPerArea": {
"SE3": 1099.25,
"SE4": 1507.23
}
},
{
"deliveryStart": "2024-11-06T10:00:00Z",
"deliveryEnd": "2024-11-06T11:00:00Z",
"entryPerArea": {
"SE3": 903.31,
"SE4": 1362.84
}
},
{
"deliveryStart": "2024-11-06T11:00:00Z",
"deliveryEnd": "2024-11-06T12:00:00Z",
"entryPerArea": {
"SE3": 959.99,
"SE4": 1376.13
}
},
{
"deliveryStart": "2024-11-06T12:00:00Z",
"deliveryEnd": "2024-11-06T13:00:00Z",
"entryPerArea": {
"SE3": 1186.61,
"SE4": 1449.96
}
},
{
"deliveryStart": "2024-11-06T13:00:00Z",
"deliveryEnd": "2024-11-06T14:00:00Z",
"entryPerArea": {
"SE3": 1307.67,
"SE4": 1608.35
}
},
{
"deliveryStart": "2024-11-06T14:00:00Z",
"deliveryEnd": "2024-11-06T15:00:00Z",
"entryPerArea": {
"SE3": 1385.46,
"SE4": 2110.8
}
},
{
"deliveryStart": "2024-11-06T15:00:00Z",
"deliveryEnd": "2024-11-06T16:00:00Z",
"entryPerArea": {
"SE3": 1366.8,
"SE4": 3031.25
}
},
{
"deliveryStart": "2024-11-06T16:00:00Z",
"deliveryEnd": "2024-11-06T17:00:00Z",
"entryPerArea": {
"SE3": 2366.57,
"SE4": 5511.77
}
},
{
"deliveryStart": "2024-11-06T17:00:00Z",
"deliveryEnd": "2024-11-06T18:00:00Z",
"entryPerArea": {
"SE3": 1481.92,
"SE4": 3351.64
}
},
{
"deliveryStart": "2024-11-06T18:00:00Z",
"deliveryEnd": "2024-11-06T19:00:00Z",
"entryPerArea": {
"SE3": 1082.69,
"SE4": 2484.95
}
},
{
"deliveryStart": "2024-11-06T19:00:00Z",
"deliveryEnd": "2024-11-06T20:00:00Z",
"entryPerArea": {
"SE3": 716.82,
"SE4": 1624.33
}
},
{
"deliveryStart": "2024-11-06T20:00:00Z",
"deliveryEnd": "2024-11-06T21:00:00Z",
"entryPerArea": {
"SE3": 583.16,
"SE4": 1306.27
}
},
{
"deliveryStart": "2024-11-06T21:00:00Z",
"deliveryEnd": "2024-11-06T22:00:00Z",
"entryPerArea": {
"SE3": 523.09,
"SE4": 1142.99
}
},
{
"deliveryStart": "2024-11-06T22:00:00Z",
"deliveryEnd": "2024-11-06T23:00:00Z",
"entryPerArea": {
"SE3": 250.64,
"SE4": 539.42
}
}
],
"blockPriceAggregates": [
{
"blockName": "Off-peak 1",
"deliveryStart": "2024-11-05T23:00:00Z",
"deliveryEnd": "2024-11-06T07:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 422.51,
"min": 74.06,
"max": 1820.5
},
"SE4": {
"average": 706.61,
"min": 157.34,
"max": 2449.96
}
}
},
{
"blockName": "Peak",
"deliveryStart": "2024-11-06T07:00:00Z",
"deliveryEnd": "2024-11-06T19:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 1346.82,
"min": 903.31,
"max": 2366.57
},
"SE4": {
"average": 2306.88,
"min": 1362.84,
"max": 5511.77
}
}
},
{
"blockName": "Off-peak 2",
"deliveryStart": "2024-11-06T19:00:00Z",
"deliveryEnd": "2024-11-06T23:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 518.43,
"min": 250.64,
"max": 716.82
},
"SE4": {
"average": 1153.25,
"min": 539.42,
"max": 1624.33
}
}
}
],
"currency": "SEK",
"exchangeRate": 11.66314,
"areaStates": [
{
"state": "Final",
"areas": ["SE3", "SE4"]
}
],
"areaAverages": [
{
"areaCode": "SE3",
"price": 900.65
},
{
"areaCode": "SE4",
"price": 1581.19
}
]
}

View File

@ -0,0 +1,272 @@
{
"deliveryDateCET": "2024-11-04",
"version": 3,
"updatedAt": "2024-11-04T08:09:11.1931991Z",
"deliveryAreas": ["SE3", "SE4"],
"market": "DayAhead",
"multiAreaEntries": [
{
"deliveryStart": "2024-11-03T23:00:00Z",
"deliveryEnd": "2024-11-04T00:00:00Z",
"entryPerArea": {
"SE3": 66.13,
"SE4": 78.59
}
},
{
"deliveryStart": "2024-11-04T00:00:00Z",
"deliveryEnd": "2024-11-04T01:00:00Z",
"entryPerArea": {
"SE3": 72.54,
"SE4": 86.51
}
},
{
"deliveryStart": "2024-11-04T01:00:00Z",
"deliveryEnd": "2024-11-04T02:00:00Z",
"entryPerArea": {
"SE3": 73.12,
"SE4": 84.88
}
},
{
"deliveryStart": "2024-11-04T02:00:00Z",
"deliveryEnd": "2024-11-04T03:00:00Z",
"entryPerArea": {
"SE3": 171.97,
"SE4": 217.26
}
},
{
"deliveryStart": "2024-11-04T03:00:00Z",
"deliveryEnd": "2024-11-04T04:00:00Z",
"entryPerArea": {
"SE3": 181.05,
"SE4": 227.74
}
},
{
"deliveryStart": "2024-11-04T04:00:00Z",
"deliveryEnd": "2024-11-04T05:00:00Z",
"entryPerArea": {
"SE3": 360.71,
"SE4": 414.61
}
},
{
"deliveryStart": "2024-11-04T05:00:00Z",
"deliveryEnd": "2024-11-04T06:00:00Z",
"entryPerArea": {
"SE3": 917.83,
"SE4": 1439.33
}
},
{
"deliveryStart": "2024-11-04T06:00:00Z",
"deliveryEnd": "2024-11-04T07:00:00Z",
"entryPerArea": {
"SE3": 1426.17,
"SE4": 1695.95
}
},
{
"deliveryStart": "2024-11-04T07:00:00Z",
"deliveryEnd": "2024-11-04T08:00:00Z",
"entryPerArea": {
"SE3": 1350.96,
"SE4": 1605.13
}
},
{
"deliveryStart": "2024-11-04T08:00:00Z",
"deliveryEnd": "2024-11-04T09:00:00Z",
"entryPerArea": {
"SE3": 1195.06,
"SE4": 1393.46
}
},
{
"deliveryStart": "2024-11-04T09:00:00Z",
"deliveryEnd": "2024-11-04T10:00:00Z",
"entryPerArea": {
"SE3": 992.35,
"SE4": 1126.71
}
},
{
"deliveryStart": "2024-11-04T10:00:00Z",
"deliveryEnd": "2024-11-04T11:00:00Z",
"entryPerArea": {
"SE3": 976.63,
"SE4": 1107.97
}
},
{
"deliveryStart": "2024-11-04T11:00:00Z",
"deliveryEnd": "2024-11-04T12:00:00Z",
"entryPerArea": {
"SE3": 952.76,
"SE4": 1085.73
}
},
{
"deliveryStart": "2024-11-04T12:00:00Z",
"deliveryEnd": "2024-11-04T13:00:00Z",
"entryPerArea": {
"SE3": 1029.37,
"SE4": 1177.71
}
},
{
"deliveryStart": "2024-11-04T13:00:00Z",
"deliveryEnd": "2024-11-04T14:00:00Z",
"entryPerArea": {
"SE3": 1043.35,
"SE4": 1194.59
}
},
{
"deliveryStart": "2024-11-04T14:00:00Z",
"deliveryEnd": "2024-11-04T15:00:00Z",
"entryPerArea": {
"SE3": 1359.57,
"SE4": 1561.12
}
},
{
"deliveryStart": "2024-11-04T15:00:00Z",
"deliveryEnd": "2024-11-04T16:00:00Z",
"entryPerArea": {
"SE3": 1848.35,
"SE4": 2145.84
}
},
{
"deliveryStart": "2024-11-04T16:00:00Z",
"deliveryEnd": "2024-11-04T17:00:00Z",
"entryPerArea": {
"SE3": 2812.53,
"SE4": 3313.53
}
},
{
"deliveryStart": "2024-11-04T17:00:00Z",
"deliveryEnd": "2024-11-04T18:00:00Z",
"entryPerArea": {
"SE3": 2351.69,
"SE4": 2751.87
}
},
{
"deliveryStart": "2024-11-04T18:00:00Z",
"deliveryEnd": "2024-11-04T19:00:00Z",
"entryPerArea": {
"SE3": 1553.08,
"SE4": 1842.77
}
},
{
"deliveryStart": "2024-11-04T19:00:00Z",
"deliveryEnd": "2024-11-04T20:00:00Z",
"entryPerArea": {
"SE3": 1165.02,
"SE4": 1398.35
}
},
{
"deliveryStart": "2024-11-04T20:00:00Z",
"deliveryEnd": "2024-11-04T21:00:00Z",
"entryPerArea": {
"SE3": 1007.48,
"SE4": 1172.35
}
},
{
"deliveryStart": "2024-11-04T21:00:00Z",
"deliveryEnd": "2024-11-04T22:00:00Z",
"entryPerArea": {
"SE3": 792.09,
"SE4": 920.28
}
},
{
"deliveryStart": "2024-11-04T22:00:00Z",
"deliveryEnd": "2024-11-04T23:00:00Z",
"entryPerArea": {
"SE3": 465.38,
"SE4": 528.83
}
}
],
"blockPriceAggregates": [
{
"blockName": "Off-peak 1",
"deliveryStart": "2024-11-03T23:00:00Z",
"deliveryEnd": "2024-11-04T07:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 408.69,
"min": 66.13,
"max": 1426.17
},
"SE4": {
"average": 530.61,
"min": 78.59,
"max": 1695.95
}
}
},
{
"blockName": "Peak",
"deliveryStart": "2024-11-04T07:00:00Z",
"deliveryEnd": "2024-11-04T19:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 1455.48,
"min": 952.76,
"max": 2812.53
},
"SE4": {
"average": 1692.2,
"min": 1085.73,
"max": 3313.53
}
}
},
{
"blockName": "Off-peak 2",
"deliveryStart": "2024-11-04T19:00:00Z",
"deliveryEnd": "2024-11-04T23:00:00Z",
"averagePricePerArea": {
"SE3": {
"average": 857.49,
"min": 465.38,
"max": 1165.02
},
"SE4": {
"average": 1004.95,
"min": 528.83,
"max": 1398.35
}
}
}
],
"currency": "SEK",
"exchangeRate": 11.64318,
"areaStates": [
{
"state": "Final",
"areas": ["SE3", "SE4"]
}
],
"areaAverages": [
{
"areaCode": "SE3",
"price": 1006.88
},
{
"areaCode": "SE4",
"price": 1190.46
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,11 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from pynordpool import (
DeliveryPeriodData,
NordPoolClient,
NordPoolConnectionError,
NordPoolEmptyResponseError,
NordPoolError,
@ -22,10 +23,11 @@ from homeassistant.data_entry_flow import FlowResultType
from . import ENTRY_CONFIG
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00")
async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None:
async def test_form(hass: HomeAssistant, get_client: NordPoolClient) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
@ -34,17 +36,11 @@ async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None:
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
ENTRY_CONFIG,
)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
ENTRY_CONFIG,
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["version"] == 1
@ -54,7 +50,7 @@ async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None:
@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00")
async def test_single_config_entry(
hass: HomeAssistant, load_int: None, get_data: DeliveryPeriodData
hass: HomeAssistant, load_int: None, get_client: NordPoolClient
) -> None:
"""Test abort for single config entry."""
@ -77,7 +73,7 @@ async def test_single_config_entry(
)
async def test_cannot_connect(
hass: HomeAssistant,
get_data: DeliveryPeriodData,
get_client: NordPoolClient,
error_message: Exception,
p_error: str,
) -> None:
@ -101,14 +97,10 @@ async def test_cannot_connect(
assert result["errors"] == {"base": p_error}
with patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=ENTRY_CONFIG,
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=ENTRY_CONFIG,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Nord Pool"
@ -119,25 +111,18 @@ async def test_cannot_connect(
async def test_reconfigure(
hass: HomeAssistant,
load_int: MockConfigEntry,
get_data: DeliveryPeriodData,
) -> None:
"""Test reconfiguration."""
result = await load_int.start_reconfigure_flow(hass)
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_AREAS: ["SE3"],
CONF_CURRENCY: "EUR",
},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_AREAS: ["SE3"],
CONF_CURRENCY: "EUR",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
@ -162,7 +147,8 @@ async def test_reconfigure(
async def test_reconfigure_cannot_connect(
hass: HomeAssistant,
load_int: MockConfigEntry,
get_data: DeliveryPeriodData,
aioclient_mock: AiohttpClientMocker,
load_json: list[dict[str, Any]],
error_message: Exception,
p_error: str,
) -> None:
@ -184,17 +170,13 @@ async def test_reconfigure_cannot_connect(
assert result["errors"] == {"base": p_error}
with patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_AREAS: ["SE3"],
CONF_CURRENCY: "EUR",
},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_AREAS: ["SE3"],
CONF_CURRENCY: "EUR",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"

View File

@ -7,8 +7,8 @@ from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from pynordpool import (
DeliveryPeriodData,
NordPoolAuthenticationError,
NordPoolClient,
NordPoolEmptyResponseError,
NordPoolError,
NordPoolResponseError,
@ -28,7 +28,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00")
async def test_coordinator(
hass: HomeAssistant,
get_data: DeliveryPeriodData,
get_client: NordPoolClient,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
@ -41,30 +41,31 @@ async def test_coordinator(
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == "0.92737"
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
side_effect=NordPoolError("error"),
) as mock_data,
):
mock_data.return_value = get_data
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
mock_data.assert_called_once()
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == "0.92737"
mock_data.reset_mock()
mock_data.side_effect = NordPoolError("error")
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
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
mock_data.reset_mock()
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
side_effect=NordPoolAuthenticationError("Authentication error"),
) as mock_data,
):
assert "Authentication error" not in caplog.text
mock_data.side_effect = NordPoolAuthenticationError("Authentication error")
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@ -72,10 +73,14 @@ async def test_coordinator(
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Authentication error" in caplog.text
mock_data.reset_mock()
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
side_effect=NordPoolEmptyResponseError("Empty response"),
) as mock_data,
):
assert "Empty response" not in caplog.text
mock_data.side_effect = NordPoolEmptyResponseError("Empty response")
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@ -83,10 +88,14 @@ async def test_coordinator(
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Empty response" in caplog.text
mock_data.reset_mock()
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
side_effect=NordPoolResponseError("Response error"),
) as mock_data,
):
assert "Response error" not in caplog.text
mock_data.side_effect = NordPoolResponseError("Response error")
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
@ -94,13 +103,9 @@ async def test_coordinator(
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Response error" in caplog.text
mock_data.reset_mock()
mock_data.return_value = get_data
mock_data.side_effect = None
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_data.assert_called_once()
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == "1.81645"
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == "1.81645"

View File

@ -2,19 +2,21 @@
from __future__ import annotations
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00")
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
load_int: ConfigEntry,
load_int: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test generating diagnostics for a config entry."""

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from unittest.mock import patch
from pynordpool import (
DeliveryPeriodData,
NordPoolClient,
NordPoolConnectionError,
NordPoolEmptyResponseError,
NordPoolError,
@ -22,7 +22,8 @@ from . import ENTRY_CONFIG
from tests.common import MockConfigEntry
async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None:
@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00")
async def test_unload_entry(hass: HomeAssistant, get_client: NordPoolClient) -> None:
"""Test load and unload an entry."""
entry = MockConfigEntry(
domain=DOMAIN,
@ -31,13 +32,7 @@ async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) -
)
entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
assert entry.state is ConfigEntryState.LOADED
@ -56,7 +51,7 @@ async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) -
],
)
async def test_initial_startup_fails(
hass: HomeAssistant, get_data: DeliveryPeriodData, error: Exception
hass: HomeAssistant, get_client: NordPoolClient, error: Exception
) -> None:
"""Test load and unload an entry."""
entry = MockConfigEntry(

View File

@ -6,7 +6,6 @@ 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
@ -38,12 +37,12 @@ async def test_sensor_no_next_price(hass: HomeAssistant, load_int: ConfigEntry)
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.28914"
assert next_price.state == STATE_UNKNOWN
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-05T00:00:00+01:00")
@pytest.mark.freeze_time("2024-11-06T00:00:00+01:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_no_previous_price(
hass: HomeAssistant, load_int: ConfigEntry
@ -57,6 +56,6 @@ async def test_sensor_no_previous_price(
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.25073"
assert last_price.state == STATE_UNKNOWN
assert next_price.state == "0.07636"
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

View File

@ -3,7 +3,6 @@
from unittest.mock import patch
from pynordpool import (
DeliveryPeriodData,
NordPoolAuthenticationError,
NordPoolEmptyResponseError,
NordPoolError,
@ -28,7 +27,7 @@ TEST_SERVICE_DATA = {
ATTR_CONFIG_ENTRY: "to_replace",
ATTR_DATE: "2024-11-05",
ATTR_AREAS: "SE3",
ATTR_CURRENCY: "SEK",
ATTR_CURRENCY: "EUR",
}
TEST_SERVICE_DATA_USE_DEFAULTS = {
ATTR_CONFIG_ENTRY: "to_replace",
@ -40,45 +39,32 @@ TEST_SERVICE_DATA_USE_DEFAULTS = {
async def test_service_call(
hass: HomeAssistant,
load_int: MockConfigEntry,
get_data: DeliveryPeriodData,
snapshot: SnapshotAssertion,
) -> None:
"""Test get_prices_for_date service call."""
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
service_data = TEST_SERVICE_DATA.copy()
service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,
service_data,
blocking=True,
return_response=True,
)
service_data = TEST_SERVICE_DATA.copy()
service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,
service_data,
blocking=True,
return_response=True,
)
assert response == snapshot
price_value = response["SE3"][0]["price"]
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
):
service_data = TEST_SERVICE_DATA_USE_DEFAULTS.copy()
service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,
service_data,
blocking=True,
return_response=True,
)
service_data = TEST_SERVICE_DATA_USE_DEFAULTS.copy()
service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,
service_data,
blocking=True,
return_response=True,
)
assert "SE3" in response
assert response["SE3"][0]["price"] == price_value
@ -124,17 +110,10 @@ async def test_service_call_failures(
async def test_service_call_config_entry_bad_state(
hass: HomeAssistant,
load_int: MockConfigEntry,
get_data: DeliveryPeriodData,
) -> None:
"""Test get_prices_for_date service call when config entry bad state."""
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
pytest.raises(ServiceValidationError) as err,
):
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,
@ -149,13 +128,7 @@ async def test_service_call_config_entry_bad_state(
await hass.config_entries.async_unload(load_int.entry_id)
await hass.async_block_till_done()
with (
patch(
"homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period",
return_value=get_data,
),
pytest.raises(ServiceValidationError) as err,
):
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_GET_PRICES_FOR_DATE,