mirror of
https://github.com/home-assistant/core.git
synced 2026-08-29 02:35:05 -05:00
346 lines
10 KiB
Python
346 lines
10 KiB
Python
"""Tests for the services provided by the EnergyZero integration."""
|
|
|
|
from datetime import date
|
|
import re
|
|
from unittest.mock import AsyncMock
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from energyzero import EnergyZeroNoDataError, PriceType
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.energyzero.const import DOMAIN
|
|
from homeassistant.components.energyzero.services import (
|
|
ATTR_CONFIG_ENTRY,
|
|
ENERGY_SERVICE_NAME,
|
|
GAS_SERVICE_NAME,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
pytestmark = pytest.mark.freeze_time("2026-04-10 20:32:59")
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize(
|
|
("service", "service_data"),
|
|
[
|
|
(
|
|
GAS_SERVICE_NAME,
|
|
{"incl_vat": False},
|
|
),
|
|
(
|
|
ENERGY_SERVICE_NAME,
|
|
{"incl_vat": True},
|
|
),
|
|
],
|
|
)
|
|
async def test_service(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
snapshot: SnapshotAssertion,
|
|
service: str,
|
|
service_data: dict[str, str | bool],
|
|
) -> None:
|
|
"""Test the EnergyZero Service."""
|
|
data = {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id} | service_data
|
|
|
|
assert snapshot == await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
data,
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize(
|
|
("service", "incl_vat", "expected_price_type"),
|
|
[
|
|
(GAS_SERVICE_NAME, True, PriceType.MARKET_WITH_VAT),
|
|
(GAS_SERVICE_NAME, False, PriceType.MARKET),
|
|
(ENERGY_SERVICE_NAME, True, PriceType.MARKET_WITH_VAT),
|
|
(ENERGY_SERVICE_NAME, False, PriceType.MARKET),
|
|
],
|
|
)
|
|
async def test_service_price_type_mapping(
|
|
hass: HomeAssistant,
|
|
mock_energyzero: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
incl_vat: bool,
|
|
expected_price_type: PriceType,
|
|
) -> None:
|
|
"""Test incl_vat maps to the expected EnergyZero price type."""
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
{
|
|
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
|
"incl_vat": incl_vat,
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
method = (
|
|
mock_energyzero.get_gas_prices
|
|
if service == GAS_SERVICE_NAME
|
|
else mock_energyzero.get_electricity_prices
|
|
)
|
|
assert method.await_args.kwargs["price_type"] is expected_price_type
|
|
assert method.await_args.kwargs["local_tz"] == ZoneInfo(hass.config.time_zone)
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME])
|
|
async def test_service_dates_normalized_to_hass_timezone(
|
|
hass: HomeAssistant,
|
|
mock_energyzero: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
) -> None:
|
|
"""Test service input datetimes are normalized to the HA timezone."""
|
|
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
|
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
{
|
|
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
|
"incl_vat": True,
|
|
"start": "2023-01-01 23:30:00-01:00",
|
|
"end": "2023-01-02 00:30:00-01:00",
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
method = (
|
|
mock_energyzero.get_gas_prices
|
|
if service == GAS_SERVICE_NAME
|
|
else mock_energyzero.get_electricity_prices
|
|
)
|
|
assert method.await_args.kwargs["start_date"] == date(2023, 1, 2)
|
|
assert method.await_args.kwargs["local_tz"] == ZoneInfo("Europe/Amsterdam")
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize(
|
|
("service", "expected_prices"),
|
|
[
|
|
(
|
|
GAS_SERVICE_NAME,
|
|
[
|
|
{
|
|
"price": 0.45193447944,
|
|
"timestamp": "2026-04-10 04:00:00+00:00",
|
|
}
|
|
],
|
|
),
|
|
(
|
|
ENERGY_SERVICE_NAME,
|
|
[
|
|
{"price": 0.12572, "timestamp": "2026-04-10 21:00:00+00:00"},
|
|
{"price": 0.125925, "timestamp": "2026-04-10 22:00:00+00:00"},
|
|
{"price": 0.1120525, "timestamp": "2026-04-10 23:00:00+00:00"},
|
|
],
|
|
),
|
|
],
|
|
)
|
|
async def test_service_filters_datetime_range(
|
|
hass: HomeAssistant,
|
|
mock_energyzero: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
expected_prices: list[dict[str, str | float]],
|
|
) -> None:
|
|
"""Test services request each day and filter to the datetime range."""
|
|
await hass.config.async_set_time_zone("Europe/Amsterdam")
|
|
mock_energyzero.reset_mock()
|
|
|
|
response = await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
{
|
|
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
|
"incl_vat": False,
|
|
"start": "2026-04-10 23:00:00+02:00",
|
|
"end": "2026-04-11 02:00:00+02:00",
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
assert response == {"prices": expected_prices}
|
|
|
|
method = (
|
|
mock_energyzero.get_gas_prices
|
|
if service == GAS_SERVICE_NAME
|
|
else mock_energyzero.get_electricity_prices
|
|
)
|
|
assert [item.kwargs["start_date"] for item in method.await_args_list] == [
|
|
date(2026, 4, 10),
|
|
date(2026, 4, 11),
|
|
]
|
|
assert all(
|
|
item.kwargs["end_date"] == item.kwargs["start_date"]
|
|
for item in method.await_args_list
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def config_entry_data(
|
|
mock_config_entry: MockConfigEntry, request: pytest.FixtureRequest
|
|
) -> dict[str, str]:
|
|
"""Fixture for the config entry."""
|
|
if "config_entry" in request.param and request.param["config_entry"] is True:
|
|
return {"config_entry": mock_config_entry.entry_id}
|
|
|
|
return request.param
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME])
|
|
@pytest.mark.parametrize(
|
|
("config_entry_data", "service_data", "error", "error_message"),
|
|
[
|
|
({}, {}, vol.error.Error, "required key not provided .+"),
|
|
(
|
|
{"config_entry": True},
|
|
{},
|
|
vol.error.Error,
|
|
"required key not provided .+",
|
|
),
|
|
(
|
|
{},
|
|
{"incl_vat": True},
|
|
vol.error.Error,
|
|
"required key not provided .+",
|
|
),
|
|
(
|
|
{"config_entry": True},
|
|
{"incl_vat": "incorrect vat"},
|
|
vol.error.Error,
|
|
"expected bool at .+",
|
|
),
|
|
(
|
|
{"config_entry": "incorrect entry"},
|
|
{"incl_vat": True},
|
|
ServiceValidationError,
|
|
".+ config entry with ID incorrect entry was not found",
|
|
),
|
|
(
|
|
{"config_entry": True},
|
|
{
|
|
"incl_vat": True,
|
|
"start": "incorrect date",
|
|
},
|
|
ServiceValidationError,
|
|
"Invalid date provided. Got incorrect date",
|
|
),
|
|
(
|
|
{"config_entry": True},
|
|
{
|
|
"incl_vat": True,
|
|
"end": "incorrect date",
|
|
},
|
|
ServiceValidationError,
|
|
"Invalid date provided. Got incorrect date",
|
|
),
|
|
(
|
|
{"config_entry": True},
|
|
{
|
|
"incl_vat": True,
|
|
"start": "2023-01-02",
|
|
"end": "2023-01-01",
|
|
},
|
|
ServiceValidationError,
|
|
"Invalid date range provided. End 2023-01-01 must be after start 2023-01-02",
|
|
),
|
|
],
|
|
indirect=["config_entry_data"],
|
|
)
|
|
async def test_service_validation(
|
|
hass: HomeAssistant,
|
|
service: str,
|
|
config_entry_data: dict[str, str],
|
|
service_data: dict[str, str],
|
|
error: type[Exception],
|
|
error_message: str,
|
|
) -> None:
|
|
"""Test the EnergyZero Service validation."""
|
|
|
|
with pytest.raises(error) as exc:
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
config_entry_data | service_data,
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
assert re.match(error_message, str(exc.value))
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME])
|
|
async def test_service_called_with_unloaded_entry(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
) -> None:
|
|
"""Test service calls with unloaded config entry."""
|
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
|
|
|
data = {"config_entry": mock_config_entry.entry_id, "incl_vat": True}
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError,
|
|
match=f"{mock_config_entry.title} for integration energyzero is not loaded",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
data,
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME])
|
|
async def test_service_no_data_returns_validation_error(
|
|
hass: HomeAssistant,
|
|
mock_energyzero: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
service: str,
|
|
) -> None:
|
|
"""Test backend no-data errors are surfaced as service validation errors."""
|
|
method = (
|
|
mock_energyzero.get_gas_prices
|
|
if service == GAS_SERVICE_NAME
|
|
else mock_energyzero.get_electricity_prices
|
|
)
|
|
method.side_effect = EnergyZeroNoDataError(
|
|
"not found: prices do not span the whole requested date"
|
|
)
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError,
|
|
match=r"No price data available for 2026-04-10\.?",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
service,
|
|
{
|
|
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
|
|
"incl_vat": True,
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|