Add all-in electricity price sensors to EnergyZero (#181602)

This commit is contained in:
Klaas Schoute
2026-09-08 22:14:07 +02:00
committed by GitHub
parent 368adbe0fb
commit f134bdc394
14 changed files with 890 additions and 118 deletions
+1 -1
View File
@@ -16,6 +16,6 @@ SCAN_INTERVAL = timedelta(minutes=10)
THRESHOLD_HOUR: Final = 14
SERVICE_TYPE_DEVICE_NAMES = {
"today_energy": "Energy market price",
"today_energy": "Electricity price",
"today_gas": "Gas market price",
}
@@ -1,6 +1,6 @@
"""The Coordinator for EnergyZero."""
from datetime import timedelta
from datetime import date, timedelta
from typing import NamedTuple, override
from zoneinfo import ZoneInfo
@@ -34,17 +34,22 @@ type EnergyZeroConfigEntry = ConfigEntry[EnergyZeroDataUpdateCoordinator]
class EnergyZeroData(NamedTuple):
"""Class for defining data in dict."""
energy_today: EnergyPrices
energy_tomorrow: EnergyPrices | None
electricity_market_today: EnergyPrices
electricity_market_tomorrow: EnergyPrices | None
electricity_all_in_today: EnergyPrices
electricity_all_in_tomorrow: EnergyPrices | None
gas_today: EnergyPrices | None
electricity_price_step: timedelta
@property
def next_energy_price(self) -> float | None:
"""Return the electricity price one market period from now."""
return self.energy_today.price_at_time(
self.energy_today.utcnow() + self.electricity_price_step
)
def next_price(
self, prices: EnergyPrices, tomorrow: EnergyPrices | None
) -> float | None:
"""Return the next period's price, including across midnight."""
moment = prices.utcnow() + self.electricity_price_step
price = prices.price_at_time(moment)
if price is not None:
return price
return tomorrow.price_at_time(moment) if tomorrow is not None else None
class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]):
@@ -71,21 +76,29 @@ class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]):
)
self.energyzero = EnergyZero(session=async_get_clientsession(hass))
async def _async_get_electricity_prices(
self, day: date, local_tz: ZoneInfo
) -> dict[PriceType, EnergyPrices]:
"""Fetch both electricity price streams in a single request."""
return await self.energyzero.get_electricity_prices(
start_date=day,
end_date=day,
interval=self.electricity_interval,
price_type=(PriceType.MARKET_WITH_VAT, PriceType.ALL_IN),
local_tz=local_tz,
)
@override
async def _async_update_data(self) -> EnergyZeroData:
"""Fetch data from EnergyZero."""
today = dt_util.now().date()
gas_today = None
energy_tomorrow = None
electricity_tomorrow: dict[PriceType, EnergyPrices] = {}
local_tz = ZoneInfo(self.hass.config.time_zone)
try:
energy_today = await self.energyzero.get_electricity_prices(
start_date=today,
end_date=today,
interval=self.electricity_interval,
price_type=PriceType.MARKET_WITH_VAT,
local_tz=local_tz,
electricity_today = await self._async_get_electricity_prices(
today, local_tz
)
try:
gas_today = await self.energyzero.get_gas_prices(
@@ -98,24 +111,22 @@ class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]):
LOGGER.debug("No data for gas prices for EnergyZero integration")
# Energy for tomorrow only after 14:00 UTC
if dt_util.utcnow().hour >= THRESHOLD_HOUR:
tomorrow = today + timedelta(days=1)
try:
energy_tomorrow = await self.energyzero.get_electricity_prices(
start_date=tomorrow,
end_date=tomorrow,
interval=self.electricity_interval,
price_type=PriceType.MARKET_WITH_VAT,
local_tz=local_tz,
electricity_tomorrow = await self._async_get_electricity_prices(
today + timedelta(days=1), local_tz
)
except EnergyZeroNoDataError:
LOGGER.debug("No data for tomorrow for EnergyZero integration")
LOGGER.debug("No electricity prices for tomorrow")
except EnergyZeroConnectionError as err:
raise UpdateFailed("Error communicating with EnergyZero API") from err
return EnergyZeroData(
energy_today=energy_today,
energy_tomorrow=energy_tomorrow,
electricity_market_today=electricity_today[PriceType.MARKET_WITH_VAT],
electricity_market_tomorrow=electricity_tomorrow.get(
PriceType.MARKET_WITH_VAT
),
electricity_all_in_today=electricity_today[PriceType.ALL_IN],
electricity_all_in_tomorrow=electricity_tomorrow.get(PriceType.ALL_IN),
gas_today=gas_today,
electricity_price_step=self.electricity_price_step,
)
@@ -31,19 +31,35 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator_data = entry.runtime_data.data
energy_today = coordinator_data.energy_today
energy_today = coordinator_data.electricity_market_today
all_in_today = coordinator_data.electricity_all_in_today
return {
"energy": {
"electricity_market": {
"current_price": energy_today.current_price,
"next_price": coordinator_data.next_energy_price,
"next_price": coordinator_data.next_price(
energy_today, coordinator_data.electricity_market_tomorrow
),
"average_price": energy_today.average_price,
"max_price": energy_today.extreme_prices[1],
"min_price": energy_today.extreme_prices[0],
"highest_price_time": energy_today.highest_price_time_range.start_including,
"lowest_price_time": energy_today.lowest_price_time_range.start_including,
"percentage_of_max": energy_today.pct_of_max_price,
"hours_priced_equal_or_lower": energy_today.time_ranges_priced_equal_or_lower,
"periods_priced_equal_or_lower": energy_today.time_ranges_priced_equal_or_lower,
},
"electricity_all_in": {
"current_price": all_in_today.current_price,
"next_price": coordinator_data.next_price(
all_in_today, coordinator_data.electricity_all_in_tomorrow
),
"average_price": all_in_today.average_price,
"max_price": all_in_today.extreme_prices[1],
"min_price": all_in_today.extreme_prices[0],
"highest_price_time": all_in_today.highest_price_time_range.start_including,
"lowest_price_time": all_in_today.lowest_price_time_range.start_including,
"percentage_of_max": all_in_today.pct_of_max_price,
"periods_priced_equal_or_lower": all_in_today.time_ranges_priced_equal_or_lower,
},
"gas": {
"current_hour_price": get_gas_price(coordinator_data, 0),
@@ -1,9 +1,39 @@
{
"entity": {
"sensor": {
"all_in_average_price": {
"default": "mdi:cash-multiple"
},
"all_in_current_price": {
"default": "mdi:cash"
},
"all_in_max_price": {
"default": "mdi:cash-plus"
},
"all_in_min_price": {
"default": "mdi:cash-minus"
},
"all_in_next_price": {
"default": "mdi:cash"
},
"average_price": {
"default": "mdi:cash-multiple"
},
"current_price": {
"default": "mdi:cash"
},
"hours_priced_equal_or_lower": {
"default": "mdi:clock"
},
"max_price": {
"default": "mdi:cash-plus"
},
"min_price": {
"default": "mdi:cash-minus"
},
"next_price": {
"default": "mdi:cash"
},
"percentage_of_max": {
"default": "mdi:percent"
}
+58 -9
View File
@@ -59,7 +59,7 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.energy_today.current_price,
value_fn=lambda data: data.electricity_market_today.current_price,
),
EnergyZeroSensorEntityDescription(
key="next_hour_price",
@@ -67,7 +67,9 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.next_energy_price,
value_fn=lambda data: data.next_price(
data.electricity_market_today, data.electricity_market_tomorrow
),
),
EnergyZeroSensorEntityDescription(
key="average_price",
@@ -75,7 +77,7 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.energy_today.average_price,
value_fn=lambda data: data.electricity_market_today.average_price,
),
EnergyZeroSensorEntityDescription(
key="max_price",
@@ -83,7 +85,7 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.energy_today.extreme_prices[1],
value_fn=lambda data: data.electricity_market_today.extreme_prices[1],
),
EnergyZeroSensorEntityDescription(
key="min_price",
@@ -91,7 +93,7 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.energy_today.extreme_prices[0],
value_fn=lambda data: data.electricity_market_today.extreme_prices[0],
),
EnergyZeroSensorEntityDescription(
key="highest_price_time",
@@ -99,7 +101,7 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
service_type="today_energy",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: (
data.energy_today.highest_price_time_range.start_including
data.electricity_market_today.highest_price_time_range.start_including
),
),
EnergyZeroSensorEntityDescription(
@@ -107,20 +109,67 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
translation_key="lowest_price_time",
service_type="today_energy",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.lowest_price_time_range.start_including,
value_fn=lambda data: (
data.electricity_market_today.lowest_price_time_range.start_including
),
),
EnergyZeroSensorEntityDescription(
key="percentage_of_max",
translation_key="percentage_of_max",
service_type="today_energy",
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: data.energy_today.pct_of_max_price,
value_fn=lambda data: data.electricity_market_today.pct_of_max_price,
),
EnergyZeroSensorEntityDescription(
key="hours_priced_equal_or_lower",
translation_key="hours_priced_equal_or_lower",
service_type="today_energy",
value_fn=lambda data: data.energy_today.time_ranges_priced_equal_or_lower,
value_fn=lambda data: (
data.electricity_market_today.time_ranges_priced_equal_or_lower
),
),
EnergyZeroSensorEntityDescription(
key="all_in_current_price",
translation_key="all_in_current_price",
service_type="today_energy",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.electricity_all_in_today.current_price,
),
EnergyZeroSensorEntityDescription(
key="all_in_next_price",
translation_key="all_in_next_price",
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.next_price(
data.electricity_all_in_today, data.electricity_all_in_tomorrow
),
),
EnergyZeroSensorEntityDescription(
key="all_in_average_price",
translation_key="all_in_average_price",
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.electricity_all_in_today.average_price,
),
EnergyZeroSensorEntityDescription(
key="all_in_max_price",
translation_key="all_in_max_price",
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.electricity_all_in_today.extreme_prices[1],
),
EnergyZeroSensorEntityDescription(
key="all_in_min_price",
translation_key="all_in_min_price",
service_type="today_energy",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
suggested_display_precision=3,
value_fn=lambda data: data.electricity_all_in_today.extreme_prices[0],
),
)
@@ -11,38 +11,53 @@
},
"entity": {
"sensor": {
"all_in_average_price": {
"name": "Average all-in price"
},
"all_in_current_price": {
"name": "Current all-in price"
},
"all_in_max_price": {
"name": "Maximum all-in price"
},
"all_in_min_price": {
"name": "Minimum all-in price"
},
"all_in_next_price": {
"name": "Next all-in price"
},
"average_price": {
"name": "Average - today"
"name": "Average market price"
},
"current_hour_price": {
"name": "Current hour"
},
"current_price": {
"name": "Current price"
"name": "Current market price"
},
"highest_price_time": {
"name": "Time of highest price - today"
"name": "Highest market price time"
},
"hours_priced_equal_or_lower": {
"name": "Periods priced equal or lower"
"name": "Market periods priced equal or lower"
},
"lowest_price_time": {
"name": "Time of lowest price - today"
"name": "Lowest market price time"
},
"max_price": {
"name": "Highest price - today"
"name": "Maximum market price"
},
"min_price": {
"name": "Lowest price - today"
"name": "Minimum market price"
},
"next_hour_price": {
"name": "Next hour"
},
"next_price": {
"name": "Next price"
"name": "Next market price"
},
"percentage_of_max": {
"name": "Current percentage of highest price - today"
"name": "Market percentage of maximum"
}
}
},
+35 -21
View File
@@ -1,17 +1,17 @@
"""Fixtures for EnergyZero integration tests."""
from collections.abc import AsyncGenerator, Generator
from datetime import UTC, datetime
from datetime import UTC, date, datetime, tzinfo
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from zoneinfo import ZoneInfo
from energyzero import EnergyPrices, PriceType
from energyzero import EnergyPrices, Interval, PriceType
from energyzero.models import REST_PRICE_STREAMS
import pytest
from homeassistant.components.energyzero.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_load_json_object_fixture
@@ -49,24 +49,38 @@ async def mock_energyzero(hass: HomeAssistant) -> AsyncGenerator[MagicMock]:
)
gas_data = await async_load_json_object_fixture(hass, "today_gas.json", DOMAIN)
def _get_prices(data: dict, *args, **kwargs) -> EnergyPrices:
price_type = kwargs.get("price_type", args[0] if args else PriceType.ALL_IN)
filter_date = kwargs.get("start_date", dt_util.now().date())
local_tz = kwargs.get("local_tz", ZoneInfo(hass.config.time_zone))
stream = REST_PRICE_STREAMS[price_type]
filtered_data = {
**data,
stream: [
item
for item in data[stream]
if datetime.strptime(item["start"], "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=UTC)
.astimezone(local_tz)
.date()
== filter_date
],
}
return EnergyPrices.from_rest_dict(filtered_data, price_type)
def _get_prices(
data: dict[str, Any],
*,
start_date: date,
end_date: date | None = None,
interval: Interval = Interval.QUARTER,
price_type: PriceType | tuple[PriceType, ...] = PriceType.ALL_IN,
local_tz: tzinfo | None = None,
) -> EnergyPrices | dict[PriceType, EnergyPrices]:
local_tz = local_tz or ZoneInfo(hass.config.time_zone)
requested_types = (
(price_type,) if isinstance(price_type, PriceType) else price_type
)
results = {}
for requested_type in requested_types:
stream = REST_PRICE_STREAMS[requested_type]
filtered_data = {
**data,
stream: [
item
for item in data[stream]
if datetime.strptime(item["start"], "%Y-%m-%dT%H:%M:%SZ")
.replace(tzinfo=UTC)
.astimezone(local_tz)
.date()
== start_date
],
}
results[requested_type] = EnergyPrices.from_rest_dict(
filtered_data, requested_type
)
return results[price_type] if isinstance(price_type, PriceType) else results
client.get_electricity_prices.side_effect = lambda *a, **kw: _get_prices(
energy_data, *a, **kw
@@ -1,16 +1,27 @@
# serializer version: 1
# name: test_diagnostics_no_gas_today
dict({
'energy': dict({
'electricity_all_in': dict({
'average_price': 0.25694034895833334,
'current_price': 0.28275885,
'highest_price_time': '2026-04-10T18:00:00+00:00',
'lowest_price_time': '2026-04-11T06:00:00+00:00',
'max_price': 0.399000525,
'min_price': 0.188351625,
'next_price': 0.2629693,
'percentage_of_max': 70.87,
'periods_priced_equal_or_lower': 20,
}),
'electricity_market': dict({
'average_price': 0.14609224895833334,
'current_price': 0.17191075,
'highest_price_time': '2026-04-10T18:00:00+00:00',
'hours_priced_equal_or_lower': 20,
'lowest_price_time': '2026-04-11T06:00:00+00:00',
'max_price': 0.288152425,
'min_price': 0.077503525,
'next_price': 0.1521212,
'percentage_of_max': 59.66,
'periods_priced_equal_or_lower': 20,
}),
'gas': dict({
'current_hour_price': None,
@@ -20,16 +31,27 @@
# ---
# name: test_entry_diagnostics
dict({
'energy': dict({
'electricity_all_in': dict({
'average_price': 0.25694034895833334,
'current_price': 0.28275885,
'highest_price_time': '2026-04-10T18:00:00+00:00',
'lowest_price_time': '2026-04-11T06:00:00+00:00',
'max_price': 0.399000525,
'min_price': 0.188351625,
'next_price': 0.2629693,
'percentage_of_max': 70.87,
'periods_priced_equal_or_lower': 20,
}),
'electricity_market': dict({
'average_price': 0.14609224895833334,
'current_price': 0.17191075,
'highest_price_time': '2026-04-10T18:00:00+00:00',
'hours_priced_equal_or_lower': 20,
'lowest_price_time': '2026-04-11T06:00:00+00:00',
'max_price': 0.288152425,
'min_price': 0.077503525,
'next_price': 0.1521212,
'percentage_of_max': 59.66,
'periods_priced_equal_or_lower': 20,
}),
'gas': dict({
'current_hour_price': None,
@@ -1,4 +1,282 @@
# serializer version: 1
# name: test_sensor[sensor.energyzero_today_energy_all_in_average_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energyzero_today_energy_all_in_average_price',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Average all-in price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_all_in_average_price',
'supported_features': 0,
'translation_key': 'all_in_average_price',
'unique_id': '12345_today_energy_all_in_average_price',
'unit_of_measurement': '€/kWh',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_average_price-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Average all-in price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_all_in_average_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.252829121875',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_current_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energyzero_today_energy_all_in_current_price',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current all-in price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_all_in_current_price',
'supported_features': 0,
'translation_key': 'all_in_current_price',
'unique_id': '12345_today_energy_all_in_current_price',
'unit_of_measurement': '€/kWh',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_current_price-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Current all-in price',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_all_in_current_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.28275885',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_max_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energyzero_today_energy_all_in_max_price',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Maximum all-in price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_all_in_max_price',
'supported_features': 0,
'translation_key': 'all_in_max_price',
'unique_id': '12345_today_energy_all_in_max_price',
'unit_of_measurement': '€/kWh',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_max_price-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Maximum all-in price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_all_in_max_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.399000525',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_min_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energyzero_today_energy_all_in_min_price',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Minimum all-in price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_all_in_min_price',
'supported_features': 0,
'translation_key': 'all_in_min_price',
'unique_id': '12345_today_energy_all_in_min_price',
'unit_of_measurement': '€/kWh',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_min_price-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Minimum all-in price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_all_in_min_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.19798625',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_next_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.energyzero_today_energy_all_in_next_price',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Next all-in price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_all_in_next_price',
'supported_features': 0,
'translation_key': 'all_in_next_price',
'unique_id': '12345_today_energy_all_in_next_price',
'unit_of_measurement': '€/kWh',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_all_in_next_price-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Next all-in price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_all_in_next_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.2629693',
})
# ---
# name: test_sensor[sensor.energyzero_today_energy_average_price-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
@@ -29,7 +307,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Average - today',
'original_name': 'Average market price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_average_price',
@@ -43,7 +321,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Average - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Average market price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
@@ -86,7 +364,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current price',
'original_name': 'Current market price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_current_hour_price',
@@ -100,7 +378,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Current price',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Current market price',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
@@ -139,7 +417,7 @@
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Time of highest price - today',
'original_name': 'Highest market price time',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_highest_price_time',
@@ -154,7 +432,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Time of highest price - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Highest market price time',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_highest_price_time',
@@ -191,7 +469,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Periods priced equal or lower',
'original_name': 'Market periods priced equal or lower',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_hours_priced_equal_or_lower',
@@ -205,7 +483,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Periods priced equal or lower',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Market periods priced equal or lower',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_hours_priced_equal_or_lower',
@@ -242,7 +520,7 @@
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Time of lowest price - today',
'original_name': 'Lowest market price time',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_lowest_price_time',
@@ -257,7 +535,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Time of lowest price - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Lowest market price time',
}),
'context': <ANY>,
'entity_id': 'sensor.energyzero_today_energy_lowest_price_time',
@@ -297,7 +575,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Highest price - today',
'original_name': 'Maximum market price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_max_price',
@@ -311,7 +589,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Highest price - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Maximum market price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
@@ -352,7 +630,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Lowest price - today',
'original_name': 'Minimum market price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_min_price',
@@ -366,7 +644,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Lowest price - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Minimum market price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
@@ -407,7 +685,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Next price',
'original_name': 'Next market price',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_next_hour_price',
@@ -421,7 +699,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Next price',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Next market price',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '€/kWh',
}),
'context': <ANY>,
@@ -459,7 +737,7 @@
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Current percentage of highest price - today',
'original_name': 'Market percentage of maximum',
'platform': 'energyzero',
'previous_unique_id': None,
'suggested_object_id': 'energyzero_today_energy_percentage_of_max',
@@ -473,7 +751,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by EnergyZero',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Energy market price Current percentage of highest price - today',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Electricity price Market percentage of maximum',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
}),
'context': <ANY>,
@@ -103,7 +103,6 @@ async def test_options_reload(
selected: str,
) -> None:
"""Apply changed options to both requests without recreating entities."""
original_coordinator = init_integration.runtime_data
original_entities = set(hass.states.async_entity_ids("sensor"))
mock_energyzero.get_electricity_prices.reset_mock()
result = await hass.config_entries.options.async_init(init_integration.entry_id)
@@ -111,7 +110,6 @@ async def test_options_reload(
result["flow_id"], user_input={CONF_ELECTRICITY_PRICE_INTERVAL: selected}
)
await hass.async_block_till_done()
assert init_integration.runtime_data is not original_coordinator
assert set(hass.states.async_entity_ids("sensor")) == original_entities
assert mock_energyzero.get_electricity_prices.await_count == 2
assert all(
@@ -49,7 +49,7 @@ async def test_diagnostics_no_gas_today(
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
assert (
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
+145 -6
View File
@@ -5,13 +5,23 @@ from unittest.mock import MagicMock, call, patch
from zoneinfo import ZoneInfo
from energyzero import EnergyZeroConnectionError, Interval, PriceType
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.energyzero.const import CONF_ELECTRICITY_PRICE_INTERVAL
from homeassistant.components.energyzero.const import (
CONF_ELECTRICITY_PRICE_INTERVAL,
DOMAIN,
ELECTRICITY_INTERVALS,
SCAN_INTERVAL,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_load_json_object_fixture,
)
@pytest.mark.parametrize(
@@ -29,14 +39,14 @@ from tests.common import MockConfigEntry
],
)
@pytest.mark.freeze_time("2026-04-10 20:32:59")
async def test_coordinator_requests_market_prices_with_vat(
async def test_coordinator_requests_both_prices_with_vat(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_energyzero: MagicMock,
options: dict[str, str],
interval: Interval,
) -> None:
"""Test the coordinator requests the backwards-compatible price stream."""
"""Test both VAT-inclusive streams share a request and the configured interval."""
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(mock_config_entry, options=options)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
@@ -51,18 +61,19 @@ async def test_coordinator_requests_market_prices_with_vat(
start_date=today,
end_date=today,
interval=interval,
price_type=PriceType.MARKET_WITH_VAT,
price_type=(PriceType.MARKET_WITH_VAT, PriceType.ALL_IN),
local_tz=local_tz,
),
call(
start_date=tomorrow,
end_date=tomorrow,
interval=interval,
price_type=PriceType.MARKET_WITH_VAT,
price_type=(PriceType.MARKET_WITH_VAT, PriceType.ALL_IN),
local_tz=local_tz,
),
]
)
assert mock_energyzero.get_electricity_prices.await_count == 2
mock_energyzero.get_gas_prices.assert_awaited_once_with(
start_date=today,
end_date=today,
@@ -101,3 +112,131 @@ async def test_config_flow_entry_not_ready(
assert mock_request.call_count == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.freeze_time("2026-04-10 20:32:59")
@pytest.mark.parametrize("selected", ["hourly", "quarter_hourly"])
@pytest.mark.parametrize(
"missing_streams",
[
pytest.param(("base_with_vat",), id="market"),
pytest.param(("all_in_with_vat",), id="all_in"),
pytest.param(("base_with_vat", "all_in_with_vat"), id="both"),
pytest.param(
("base", "base_with_vat", "all_in", "all_in_with_vat"),
id="unpublished",
),
],
)
async def test_missing_tomorrow_prices_do_not_retry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
selected: str,
missing_streams: tuple[str, ...],
freezer: FrozenDateTimeFactory,
) -> None:
"""Missing tomorrow streams do not cause extra requests or fail today's sensors."""
await hass.config.async_set_time_zone("Europe/Amsterdam")
electricity = await async_load_json_object_fixture(
hass, "today_energy.json", DOMAIN
)
gas = await async_load_json_object_fixture(hass, "today_gas.json", DOMAIN)
tomorrow = {**electricity, **{stream: [] for stream in missing_streams}}
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, options={CONF_ELECTRICITY_PRICE_INTERVAL: selected}
)
with patch(
"energyzero.api.rest.RESTClient._request",
side_effect=[electricity, gas, tomorrow] * 2,
) as request:
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert request.await_count == 3
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert request.await_count == 6
assert (
request.await_args_list[2]
== request.await_args_list[5]
== call(
"public/v1/prices",
params={
"energyType": "ENERGY_TYPE_ELECTRICITY",
"date": "11-04-2026",
"interval": ELECTRICITY_INTERVALS[selected].value,
},
)
)
assert mock_config_entry.state is ConfigEntryState.LOADED
assert (
state := hass.states.get("sensor.energyzero_today_energy_current_hour_price")
)
assert state.state == "0.17191075"
assert (
state := hass.states.get("sensor.energyzero_today_energy_all_in_current_price")
)
assert state.state == "0.28275885"
@pytest.mark.freeze_time("2026-04-10 20:32:59")
@pytest.mark.parametrize("selected", ["hourly", "quarter_hourly"])
async def test_rest_requests_share_price_streams(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
selected: str,
) -> None:
"""The actual library extracts both streams with one REST request per day."""
await hass.config.async_set_time_zone("Europe/Amsterdam")
electricity = await async_load_json_object_fixture(
hass, "today_energy.json", DOMAIN
)
gas = await async_load_json_object_fixture(hass, "today_gas.json", DOMAIN)
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, options={CONF_ELECTRICITY_PRICE_INTERVAL: selected}
)
with patch(
"energyzero.api.rest.RESTClient._request",
side_effect=[electricity, gas, electricity],
) as request:
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.LOADED
assert request.await_args_list == [
call(
"public/v1/prices",
params={
"energyType": "ENERGY_TYPE_ELECTRICITY",
"date": "10-04-2026",
"interval": ELECTRICITY_INTERVALS[selected].value,
},
),
call(
"public/v1/prices",
params={
"energyType": "ENERGY_TYPE_GAS",
"date": "10-04-2026",
"interval": "INTERVAL_DAY",
},
),
call(
"public/v1/prices",
params={
"energyType": "ENERGY_TYPE_ELECTRICITY",
"date": "11-04-2026",
"interval": ELECTRICITY_INTERVALS[selected].value,
},
),
]
assert (
state := hass.states.get("sensor.energyzero_today_energy_current_hour_price")
)
assert state.state == "0.17191075"
assert (
state := hass.states.get("sensor.energyzero_today_energy_all_in_current_price")
)
assert state.state == "0.28275885"
+156 -14
View File
@@ -4,16 +4,18 @@ from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
from zoneinfo import ZoneInfo
from energyzero import EnergyPrices, EnergyZeroNoDataError, Interval
from energyzero import EnergyPrices, EnergyZeroNoDataError, Interval, PriceType
from energyzero.models import TimeRange
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.energyzero.const import CONF_ELECTRICITY_PRICE_INTERVAL
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@@ -66,9 +68,17 @@ async def test_electricity_interval(
},
average_price=(hours * 60 // minutes + 1) / 2,
)
all_in_prices = EnergyPrices(
prices={period: price + 100 for period, price in prices.prices.items()},
average_price=prices.average_price + 100,
)
electricity = {
PriceType.MARKET_WITH_VAT: prices,
PriceType.ALL_IN: all_in_prices,
}
mock_energyzero.get_electricity_prices.side_effect = [
prices,
EnergyZeroNoDataError() if missing_tomorrow else prices,
electricity,
EnergyZeroNoDataError() if missing_tomorrow else electricity,
]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
@@ -81,29 +91,161 @@ async def test_electricity_interval(
assert expected != prices.current_price
assert (state := hass.states.get("sensor.energyzero_today_energy_next_hour_price"))
assert state.state == str(expected)
data = mock_config_entry.runtime_data.data
assert (data.energy_tomorrow is not None) == (
requests_tomorrow and not missing_tomorrow
assert (
all_in_state := hass.states.get(
"sensor.energyzero_today_energy_all_in_next_price"
)
)
assert len(data.energy_today.prices) == hours * 60 // minutes
assert all_in_state.state == str(expected + 100)
for suffix, market_value, all_in_value in (
("current_hour_price", prices.current_price, all_in_prices.current_price),
("average_price", prices.average_price, all_in_prices.average_price),
("min_price", prices.extreme_prices[0], all_in_prices.extreme_prices[0]),
("max_price", prices.extreme_prices[1], all_in_prices.extreme_prices[1]),
):
assert (state := hass.states.get(f"sensor.energyzero_today_energy_{suffix}"))
assert state.state == str(market_value)
all_in_suffix = suffix.replace("current_hour", "current")
assert (
state := hass.states.get(
f"sensor.energyzero_today_energy_all_in_{all_in_suffix}"
)
)
assert state.state == str(all_in_value)
diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert diagnostics["energy"]["next_price"] == expected
assert diagnostics["energy"]["current_price"] == prices.current_price
assert diagnostics["energy"]["average_price"] == prices.average_price
assert diagnostics["electricity_market"]["next_price"] == expected
assert diagnostics["electricity_market"]["current_price"] == prices.current_price
assert diagnostics["electricity_market"]["average_price"] == prices.average_price
assert (
diagnostics["energy"]["hours_priced_equal_or_lower"]
diagnostics["electricity_market"]["periods_priced_equal_or_lower"]
== prices.time_ranges_priced_equal_or_lower
)
assert diagnostics["electricity_all_in"]["next_price"] == expected + 100
assert (
mock_energyzero.get_electricity_prices.call_args.kwargs["interval"] == interval
diagnostics["electricity_all_in"]["current_price"]
== all_in_prices.current_price
)
assert (
diagnostics["electricity_all_in"]["average_price"]
== all_in_prices.average_price
)
assert (
diagnostics["electricity_all_in"]["min_price"]
== all_in_prices.extreme_prices[0]
)
assert (
diagnostics["electricity_all_in"]["max_price"]
== all_in_prices.extreme_prices[1]
)
assert all(
request.kwargs["interval"] == interval
for request in mock_energyzero.get_electricity_prices.await_args_list
)
assert mock_energyzero.get_electricity_prices.await_count == 1 + requests_tomorrow
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entries) == 11
assert len(entries) == 16
assert all(
entry.unique_id == f"12345_{entry.entity_id.removeprefix('sensor.energyzero_')}"
for entry in entries
)
@pytest.mark.freeze_time("2026-04-10 21:42:00")
@pytest.mark.parametrize(
("selected", "minutes"), [("hourly", 60), ("quarter_hourly", 15)]
)
@pytest.mark.parametrize("missing_tomorrow", [False, True])
async def test_next_price_across_midnight(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
mock_energyzero: MagicMock,
freezer: FrozenDateTimeFactory,
selected: str,
minutes: int,
missing_tomorrow: bool,
) -> None:
"""Use tomorrow prices until the first scheduled refresh after midnight."""
await hass.config.async_set_time_zone("Europe/Amsterdam")
start = dt_util.start_of_local_day().astimezone(UTC)
step = timedelta(minutes=minutes)
today_prices = EnergyPrices(
prices={TimeRange(start, start + timedelta(days=1)): -0.1},
average_price=-0.1,
)
tomorrow_start = start + timedelta(days=1)
tomorrow_prices = EnergyPrices(
prices={
TimeRange(
tomorrow_start + index * step, tomorrow_start + (index + 1) * step
): index / 100
for index in range(24 * 60 // minutes)
},
average_price=0.1,
)
all_in_tomorrow = EnergyPrices(
prices={
period: price + 0.11 for period, price in tomorrow_prices.prices.items()
},
average_price=0.21,
)
today = {PriceType.MARKET_WITH_VAT: today_prices, PriceType.ALL_IN: today_prices}
tomorrow = {
PriceType.MARKET_WITH_VAT: tomorrow_prices,
PriceType.ALL_IN: all_in_tomorrow,
}
tomorrow_result = EnergyZeroNoDataError() if missing_tomorrow else tomorrow
mock_energyzero.get_electricity_prices.side_effect = [
today,
tomorrow_result,
today,
tomorrow_result,
tomorrow,
EnergyZeroNoDataError(),
]
mock_config_entry.add_to_hass(hass)
hass.config_entries.async_update_entry(
mock_config_entry, options={CONF_ELECTRICITY_PRICE_INTERVAL: selected}
)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
freezer.move_to("2026-04-10 21:52:00")
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_energyzero.get_electricity_prices.await_count == 4
for entity_id, value in (
("sensor.energyzero_today_energy_next_hour_price", 0.0),
("sensor.energyzero_today_energy_all_in_next_price", 0.11),
):
assert (state := hass.states.get(entity_id))
assert state.state == (STATE_UNKNOWN if missing_tomorrow else str(value))
freezer.move_to("2026-04-10 22:00:00")
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
diagnostics = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert diagnostics["electricity_market"]["next_price"] == (
None if missing_tomorrow else 0.01
)
assert diagnostics["electricity_all_in"]["next_price"] == (
None if missing_tomorrow else 0.12
)
assert mock_energyzero.get_electricity_prices.await_count == 4
freezer.move_to("2026-04-10 22:02:00")
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
for entity_id, value in (
("sensor.energyzero_today_energy_next_hour_price", 0.01),
("sensor.energyzero_today_energy_all_in_next_price", 0.12),
):
assert (state := hass.states.get(entity_id))
assert state.state == str(value)
assert mock_energyzero.get_electricity_prices.await_count == 6
+60 -2
View File
@@ -8,7 +8,10 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.energyzero.const import SCAN_INTERVAL
from homeassistant.components.energyzero.const import (
CONF_ELECTRICITY_PRICE_INTERVAL,
SCAN_INTERVAL,
)
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -95,7 +98,62 @@ async def test_no_data(
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
await hass.async_block_till_done(wait_background_tasks=True)
assert (state := hass.states.get(entity_id))
assert state.state == expected_state
@pytest.mark.usefixtures("mock_energyzero")
@pytest.mark.parametrize("disabled_by", [None, er.RegistryEntryDisabler.USER])
async def test_existing_market_registry_entries(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
disabled_by: er.RegistryEntryDisabler | None,
) -> None:
"""Keep existing identities and user customizations through setup and reload."""
mock_config_entry.add_to_hass(hass)
entries = [
entity_registry.async_get_or_create(
"sensor",
"energyzero",
f"12345_today_energy_{key}",
suggested_object_id=f"custom_{key}",
config_entry=mock_config_entry,
disabled_by=disabled_by,
)
for key in (
"current_hour_price",
"next_hour_price",
"average_price",
"min_price",
"max_price",
"highest_price_time",
"lowest_price_time",
"percentage_of_max",
"hours_priced_equal_or_lower",
)
]
entries = [
entity_registry.async_update_entity(
entry.entity_id, name=f"Custom {entry.entity_id}"
)
for entry in entries
]
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
hass.config_entries.async_update_entry(
mock_config_entry, options={CONF_ELECTRICITY_PRICE_INTERVAL: "quarter_hourly"}
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
for original in entries:
assert (entry := entity_registry.async_get(original.entity_id))
assert entry.id == original.id
assert entry.entity_id == original.entity_id
assert entry.unique_id == original.unique_id
assert entry.name == original.name
assert entry.disabled_by == original.disabled_by
assert len(er.async_entries_for_config_entry(entity_registry, "12345")) == 16