mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 15:31:52 -05:00
Add all-in electricity price sensors to EnergyZero (#181602)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user