mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Expose full price validity range in EnergyZero service response (#181851)
This commit is contained in:
@@ -86,6 +86,8 @@ def __serialize_prices(
|
||||
{
|
||||
"price": price,
|
||||
"timestamp": str(time_range.start_including),
|
||||
"start": str(time_range.start_including),
|
||||
"end": str(time_range.end_excluding),
|
||||
}
|
||||
for price_data in prices
|
||||
for time_range, price in price_data.prices.items()
|
||||
@@ -111,12 +113,20 @@ async def __get_prices(
|
||||
local_tz = ZoneInfo(call.hass.config.time_zone)
|
||||
start_date, start_datetime = __get_date(call.data.get(ATTR_START), local_tz)
|
||||
end_date, end_datetime = __get_date(call.data.get(ATTR_END), local_tz)
|
||||
|
||||
filter_start = start_datetime or dt_util.as_utc(
|
||||
dt_util.start_of_local_day(start_date)
|
||||
)
|
||||
filter_end = end_datetime or dt_util.as_utc(
|
||||
dt_util.start_of_local_day(end_date + timedelta(days=1))
|
||||
)
|
||||
end_start = end_datetime or dt_util.as_utc(dt_util.start_of_local_day(end_date))
|
||||
|
||||
if filter_start == end_start:
|
||||
day = filter_start.astimezone(local_tz).date()
|
||||
filter_start = dt_util.as_utc(dt_util.start_of_local_day(day))
|
||||
filter_end = dt_util.as_utc(dt_util.start_of_local_day(day + timedelta(days=1)))
|
||||
else:
|
||||
filter_end = end_datetime or dt_util.as_utc(
|
||||
dt_util.start_of_local_day(end_date + timedelta(days=1))
|
||||
)
|
||||
|
||||
if filter_end <= filter_start:
|
||||
raise ServiceValidationError(
|
||||
|
||||
@@ -3,99 +3,147 @@
|
||||
dict({
|
||||
'prices': list([
|
||||
dict({
|
||||
'end': '2026-04-10 08:00:00+00:00',
|
||||
'price': 0.158361775,
|
||||
'start': '2026-04-10 07:00:00+00:00',
|
||||
'timestamp': '2026-04-10 07:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 09:00:00+00:00',
|
||||
'price': 0.15199415,
|
||||
'start': '2026-04-10 08:00:00+00:00',
|
||||
'timestamp': '2026-04-10 08:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 10:00:00+00:00',
|
||||
'price': 0.129712,
|
||||
'start': '2026-04-10 09:00:00+00:00',
|
||||
'timestamp': '2026-04-10 09:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 11:00:00+00:00',
|
||||
'price': 0.12542255,
|
||||
'start': '2026-04-10 10:00:00+00:00',
|
||||
'timestamp': '2026-04-10 10:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 12:00:00+00:00',
|
||||
'price': 0.1105819,
|
||||
'start': '2026-04-10 11:00:00+00:00',
|
||||
'timestamp': '2026-04-10 11:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 13:00:00+00:00',
|
||||
'price': 0.11546425,
|
||||
'start': '2026-04-10 12:00:00+00:00',
|
||||
'timestamp': '2026-04-10 12:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 14:00:00+00:00',
|
||||
'price': 0.10231155,
|
||||
'start': '2026-04-10 13:00:00+00:00',
|
||||
'timestamp': '2026-04-10 13:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 15:00:00+00:00',
|
||||
'price': 0.122527625,
|
||||
'start': '2026-04-10 14:00:00+00:00',
|
||||
'timestamp': '2026-04-10 14:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 16:00:00+00:00',
|
||||
'price': 0.146068175,
|
||||
'start': '2026-04-10 15:00:00+00:00',
|
||||
'timestamp': '2026-04-10 15:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 17:00:00+00:00',
|
||||
'price': 0.181984,
|
||||
'start': '2026-04-10 16:00:00+00:00',
|
||||
'timestamp': '2026-04-10 16:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 18:00:00+00:00',
|
||||
'price': 0.272253025,
|
||||
'start': '2026-04-10 17:00:00+00:00',
|
||||
'timestamp': '2026-04-10 17:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 19:00:00+00:00',
|
||||
'price': 0.288152425,
|
||||
'start': '2026-04-10 18:00:00+00:00',
|
||||
'timestamp': '2026-04-10 18:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 20:00:00+00:00',
|
||||
'price': 0.2064623,
|
||||
'start': '2026-04-10 19:00:00+00:00',
|
||||
'timestamp': '2026-04-10 19:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 21:00:00+00:00',
|
||||
'price': 0.17191075,
|
||||
'start': '2026-04-10 20:00:00+00:00',
|
||||
'timestamp': '2026-04-10 20:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 22:00:00+00:00',
|
||||
'price': 0.1521212,
|
||||
'start': '2026-04-10 21:00:00+00:00',
|
||||
'timestamp': '2026-04-10 21:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-10 23:00:00+00:00',
|
||||
'price': 0.15236925,
|
||||
'start': '2026-04-10 22:00:00+00:00',
|
||||
'timestamp': '2026-04-10 22:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 00:00:00+00:00',
|
||||
'price': 0.135583525,
|
||||
'start': '2026-04-10 23:00:00+00:00',
|
||||
'timestamp': '2026-04-10 23:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 01:00:00+00:00',
|
||||
'price': 0.12652365,
|
||||
'start': '2026-04-11 00:00:00+00:00',
|
||||
'timestamp': '2026-04-11 00:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 02:00:00+00:00',
|
||||
'price': 0.122267475,
|
||||
'start': '2026-04-11 01:00:00+00:00',
|
||||
'timestamp': '2026-04-11 01:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 03:00:00+00:00',
|
||||
'price': 0.120640025,
|
||||
'start': '2026-04-11 02:00:00+00:00',
|
||||
'timestamp': '2026-04-11 02:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 04:00:00+00:00',
|
||||
'price': 0.117239925,
|
||||
'start': '2026-04-11 03:00:00+00:00',
|
||||
'timestamp': '2026-04-11 03:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 05:00:00+00:00',
|
||||
'price': 0.116616775,
|
||||
'start': '2026-04-11 04:00:00+00:00',
|
||||
'timestamp': '2026-04-11 04:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 06:00:00+00:00',
|
||||
'price': 0.10214215,
|
||||
'start': '2026-04-11 05:00:00+00:00',
|
||||
'timestamp': '2026-04-11 05:00:00+00:00',
|
||||
}),
|
||||
dict({
|
||||
'end': '2026-04-11 07:00:00+00:00',
|
||||
'price': 0.077503525,
|
||||
'start': '2026-04-11 06:00:00+00:00',
|
||||
'timestamp': '2026-04-11 06:00:00+00:00',
|
||||
}),
|
||||
]),
|
||||
@@ -105,7 +153,9 @@
|
||||
dict({
|
||||
'prices': list([
|
||||
dict({
|
||||
'end': '2026-04-12 04:00:00+00:00',
|
||||
'price': 0.430304930424,
|
||||
'start': '2026-04-11 04:00:00+00:00',
|
||||
'timestamp': '2026-04-11 04:00:00+00:00',
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Tests for the services provided by the EnergyZero integration."""
|
||||
|
||||
from datetime import date
|
||||
from datetime import UTC, date, datetime
|
||||
import re
|
||||
from unittest.mock import AsyncMock
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from energyzero import EnergyZeroNoDataError, PriceType
|
||||
from energyzero import EnergyPrices, EnergyZeroNoDataError, PriceType, TimeRange
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
import voluptuous as vol
|
||||
@@ -139,15 +139,32 @@ async def test_service_dates_normalized_to_hass_timezone(
|
||||
{
|
||||
"price": 0.45193447944,
|
||||
"timestamp": "2026-04-10 04:00:00+00:00",
|
||||
"start": "2026-04-10 04:00:00+00:00",
|
||||
"end": "2026-04-11 04:00:00+00:00",
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
ENERGY_SERVICE_NAME,
|
||||
[
|
||||
{"price": 0.12572, "timestamp": "2026-04-10 21:00:00+00:00"},
|
||||
{"price": 0.125925, "timestamp": "2026-04-10 22:00:00+00:00"},
|
||||
{"price": 0.1120525, "timestamp": "2026-04-10 23:00:00+00:00"},
|
||||
{
|
||||
"price": 0.12572,
|
||||
"timestamp": "2026-04-10 21:00:00+00:00",
|
||||
"start": "2026-04-10 21:00:00+00:00",
|
||||
"end": "2026-04-10 22:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"price": 0.125925,
|
||||
"timestamp": "2026-04-10 22:00:00+00:00",
|
||||
"start": "2026-04-10 22:00:00+00:00",
|
||||
"end": "2026-04-10 23:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"price": 0.1120525,
|
||||
"timestamp": "2026-04-10 23:00:00+00:00",
|
||||
"start": "2026-04-10 23:00:00+00:00",
|
||||
"end": "2026-04-11 00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -177,6 +194,8 @@ async def test_service_filters_datetime_range(
|
||||
)
|
||||
|
||||
assert response == {"prices": expected_prices}
|
||||
assert response["prices"]
|
||||
assert all(item["timestamp"] == item["start"] for item in response["prices"])
|
||||
|
||||
method = (
|
||||
mock_energyzero.get_gas_prices
|
||||
@@ -193,6 +212,178 @@ async def test_service_filters_datetime_range(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
"equal_time", ["00:00:00", "14:30:00"], ids=["midnight", "non-midnight"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected_count", "expected_first", "expected_last"),
|
||||
[
|
||||
(
|
||||
GAS_SERVICE_NAME,
|
||||
1,
|
||||
{
|
||||
"price": 0.430304930424,
|
||||
"timestamp": "2026-04-11 04:00:00+00:00",
|
||||
"start": "2026-04-11 04:00:00+00:00",
|
||||
"end": "2026-04-12 04:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"price": 0.430304930424,
|
||||
"timestamp": "2026-04-11 04:00:00+00:00",
|
||||
"start": "2026-04-11 04:00:00+00:00",
|
||||
"end": "2026-04-12 04:00:00+00:00",
|
||||
},
|
||||
),
|
||||
(
|
||||
ENERGY_SERVICE_NAME,
|
||||
24,
|
||||
{
|
||||
"price": 0.125925,
|
||||
"timestamp": "2026-04-10 22:00:00+00:00",
|
||||
"start": "2026-04-10 22:00:00+00:00",
|
||||
"end": "2026-04-10 23:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"price": 0.082385,
|
||||
"timestamp": "2026-04-11 21:00:00+00:00",
|
||||
"start": "2026-04-11 21:00:00+00:00",
|
||||
"end": "2026-04-11 22:00:00+00:00",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_service_equal_start_end_selects_full_local_day(
|
||||
hass: HomeAssistant,
|
||||
mock_energyzero: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
equal_time: str,
|
||||
expected_count: int,
|
||||
expected_first: dict[str, str | float],
|
||||
expected_last: dict[str, str | float],
|
||||
) -> None:
|
||||
"""Equal start/end datetimes select the full local calendar day."""
|
||||
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||
mock_energyzero.reset_mock()
|
||||
equal_datetime = f"2026-04-11 {equal_time}"
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
||||
"incl_vat": False,
|
||||
"start": equal_datetime,
|
||||
"end": equal_datetime,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert len(response["prices"]) == expected_count
|
||||
assert response["prices"][0] == expected_first
|
||||
assert response["prices"][-1] == expected_last
|
||||
assert all(item["timestamp"] == item["start"] for item in response["prices"])
|
||||
|
||||
method = (
|
||||
mock_energyzero.get_gas_prices
|
||||
if service == GAS_SERVICE_NAME
|
||||
else mock_energyzero.get_electricity_prices
|
||||
)
|
||||
assert len(method.await_args_list) == 1
|
||||
assert method.await_args.kwargs["start_date"] == date(2026, 4, 11)
|
||||
assert method.await_args.kwargs["end_date"] == date(2026, 4, 11)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_service_equal_start_end_full_day_respects_dst(
|
||||
hass: HomeAssistant,
|
||||
mock_energyzero: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Equal start/end selects a local calendar day, not a fixed 24h window."""
|
||||
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||
|
||||
in_range = TimeRange(
|
||||
datetime(2026, 3, 28, 23, 0, tzinfo=UTC),
|
||||
datetime(2026, 3, 29, 22, 0, tzinfo=UTC),
|
||||
)
|
||||
out_of_range = TimeRange(
|
||||
datetime(2026, 3, 29, 22, 0, tzinfo=UTC),
|
||||
datetime(2026, 3, 29, 23, 0, tzinfo=UTC),
|
||||
)
|
||||
mock_energyzero.get_electricity_prices.side_effect = None
|
||||
mock_energyzero.get_electricity_prices.return_value = EnergyPrices(
|
||||
prices={in_range: 0.1, out_of_range: 0.2}, average_price=None
|
||||
)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
ENERGY_SERVICE_NAME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
||||
"incl_vat": False,
|
||||
"start": "2026-03-29 00:00:00",
|
||||
"end": "2026-03-29 00:00:00",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert response == {
|
||||
"prices": [
|
||||
{
|
||||
"price": 0.1,
|
||||
"timestamp": "2026-03-28 23:00:00+00:00",
|
||||
"start": "2026-03-28 23:00:00+00:00",
|
||||
"end": "2026-03-29 22:00:00+00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert mock_energyzero.get_electricity_prices.await_args.kwargs[
|
||||
"start_date"
|
||||
] == date(2026, 3, 29)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_service_date_and_equal_datetime_selects_full_local_day(
|
||||
hass: HomeAssistant,
|
||||
mock_energyzero: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""A date-only start and a same-day midnight datetime end are equal."""
|
||||
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
||||
mock_energyzero.reset_mock()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
GAS_SERVICE_NAME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
||||
"incl_vat": False,
|
||||
"start": "2026-04-11",
|
||||
"end": "2026-04-11 00:00:00",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert response == {
|
||||
"prices": [
|
||||
{
|
||||
"price": 0.430304930424,
|
||||
"timestamp": "2026-04-11 04:00:00+00:00",
|
||||
"start": "2026-04-11 04:00:00+00:00",
|
||||
"end": "2026-04-12 04:00:00+00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
assert mock_energyzero.get_gas_prices.await_args.kwargs["start_date"] == date(
|
||||
2026, 4, 11
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry_data(
|
||||
mock_config_entry: MockConfigEntry, request: pytest.FixtureRequest
|
||||
@@ -262,6 +453,17 @@ def config_entry_data(
|
||||
ServiceValidationError,
|
||||
"Invalid date range provided. End 2023-01-01 must be after start 2023-01-02",
|
||||
),
|
||||
(
|
||||
{"config_entry": True},
|
||||
{
|
||||
"incl_vat": True,
|
||||
"start": "2026-04-11 12:00:00",
|
||||
"end": "2026-04-11 08:00:00",
|
||||
},
|
||||
ServiceValidationError,
|
||||
"Invalid date range provided. End 2026-04-11 08:00:00 must be "
|
||||
"after start 2026-04-11 12:00:00",
|
||||
),
|
||||
],
|
||||
indirect=["config_entry_data"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user