Add EnergyZero integration (#83886)

This commit is contained in:
Klaas Schoute
2023-01-03 22:28:16 +01:00
committed by GitHub
parent 38f183a683
commit 7d54620f34
20 changed files with 1029 additions and 0 deletions

View File

@ -313,6 +313,8 @@ build.json @home-assistant/supervisor
/tests/components/emulated_kasa/ @kbickar
/homeassistant/components/energy/ @home-assistant/core
/tests/components/energy/ @home-assistant/core
/homeassistant/components/energyzero/ @klaasnicolaas
/tests/components/energyzero/ @klaasnicolaas
/homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer

View File

@ -0,0 +1,35 @@
"""The EnergyZero integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import EnergyZeroDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up EnergyZero from a config entry."""
coordinator = EnergyZeroDataUpdateCoordinator(hass)
try:
await coordinator.async_config_entry_first_refresh()
except ConfigEntryNotReady:
await coordinator.energyzero.close()
raise
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload EnergyZero config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,31 @@
"""Config flow for EnergyZero integration."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigFlow
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
class EnergyZeroFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for EnergyZero integration."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
if user_input is None:
return self.async_show_form(step_id="user")
return self.async_create_entry(
title="EnergyZero",
data={},
)

View File

@ -0,0 +1,16 @@
"""Constants for the EnergyZero integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final
DOMAIN: Final = "energyzero"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=10)
THRESHOLD_HOUR: Final = 14
SERVICE_TYPE_DEVICE_NAMES = {
"today_energy": "Energy market price",
"today_gas": "Gas market price",
}

View File

@ -0,0 +1,80 @@
"""The Coordinator for EnergyZero."""
from __future__ import annotations
from datetime import timedelta
from typing import NamedTuple
from energyzero import (
Electricity,
EnergyZero,
EnergyZeroConnectionError,
EnergyZeroNoDataError,
Gas,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt
from .const import DOMAIN, LOGGER, SCAN_INTERVAL, THRESHOLD_HOUR
class EnergyZeroData(NamedTuple):
"""Class for defining data in dict."""
energy_today: Electricity
energy_tomorrow: Electricity | None
gas_today: Gas | None
class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]):
"""Class to manage fetching EnergyZero data from single endpoint."""
config_entry: ConfigEntry
def __init__(self, hass) -> None:
"""Initialize global EnergyZero data updater."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.energyzero = EnergyZero(session=async_get_clientsession(hass))
async def _async_update_data(self) -> EnergyZeroData:
"""Fetch data from EnergyZero."""
today = dt.now().date()
gas_today = None
energy_tomorrow = None
try:
energy_today = await self.energyzero.energy_prices(
start_date=today, end_date=today
)
try:
gas_today = await self.energyzero.gas_prices(
start_date=today, end_date=today
)
except EnergyZeroNoDataError:
LOGGER.debug("No data for gas prices for EnergyZero integration")
# Energy for tomorrow only after 14:00 UTC
if dt.utcnow().hour >= THRESHOLD_HOUR:
tomorrow = today + timedelta(days=1)
try:
energy_tomorrow = await self.energyzero.energy_prices(
start_date=tomorrow, end_date=tomorrow
)
except EnergyZeroNoDataError:
LOGGER.debug("No data for tomorrow for EnergyZero integration")
except EnergyZeroConnectionError as err:
raise UpdateFailed("Error communicating with EnergyZero API") from err
return EnergyZeroData(
energy_today=energy_today,
energy_tomorrow=energy_tomorrow,
gas_today=gas_today,
)

View File

@ -0,0 +1,9 @@
{
"domain": "energyzero",
"name": "EnergyZero",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/energyzero",
"requirements": ["energyzero==0.3.1"],
"codeowners": ["@klaasnicolaas"],
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,196 @@
"""Support for EnergyZero sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CURRENCY_EURO, PERCENTAGE, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES
from .coordinator import EnergyZeroData, EnergyZeroDataUpdateCoordinator
@dataclass
class EnergyZeroSensorEntityDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable[[EnergyZeroData], float | datetime | None]
service_type: str
@dataclass
class EnergyZeroSensorEntityDescription(
SensorEntityDescription, EnergyZeroSensorEntityDescriptionMixin
):
"""Describes a Pure Energie sensor entity."""
SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = (
EnergyZeroSensorEntityDescription(
key="current_hour_price",
name="Current hour",
service_type="today_gas",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}",
value_fn=lambda data: data.gas_today.current_price if data.gas_today else None,
),
EnergyZeroSensorEntityDescription(
key="next_hour_price",
name="Next hour",
service_type="today_gas",
device_class=SensorDeviceClass.MONETARY,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}",
value_fn=lambda data: get_gas_price(data, 1),
),
EnergyZeroSensorEntityDescription(
key="current_hour_price",
name="Current hour",
service_type="today_energy",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.current_price,
),
EnergyZeroSensorEntityDescription(
key="next_hour_price",
name="Next hour",
service_type="today_energy",
device_class=SensorDeviceClass.MONETARY,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.price_at_time(
data.energy_today.utcnow() + timedelta(hours=1)
),
),
EnergyZeroSensorEntityDescription(
key="average_price",
name="Average - today",
service_type="today_energy",
device_class=SensorDeviceClass.MONETARY,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.average_price,
),
EnergyZeroSensorEntityDescription(
key="max_price",
name="Highest price - today",
service_type="today_energy",
device_class=SensorDeviceClass.MONETARY,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_prices[1],
),
EnergyZeroSensorEntityDescription(
key="min_price",
name="Lowest price - today",
service_type="today_energy",
device_class=SensorDeviceClass.MONETARY,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_prices[0],
),
EnergyZeroSensorEntityDescription(
key="highest_price_time",
name="Time of highest price - today",
service_type="today_energy",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.highest_price_time,
),
EnergyZeroSensorEntityDescription(
key="lowest_price_time",
name="Time of lowest price - today",
service_type="today_energy",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.lowest_price_time,
),
EnergyZeroSensorEntityDescription(
key="percentage_of_max",
name="Current percentage of highest price - today",
service_type="today_energy",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
value_fn=lambda data: data.energy_today.pct_of_max_price,
),
)
def get_gas_price(data: EnergyZeroData, hours: int) -> float | None:
"""Return the gas value.
Args:
data: The data object.
hours: The number of hours to add to the current time.
Returns:
The gas market price value.
"""
if data.gas_today is None:
return None
return data.gas_today.price_at_time(
data.gas_today.utcnow() + timedelta(hours=hours)
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up EnergyZero Sensors based on a config entry."""
coordinator: EnergyZeroDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
EnergyZeroSensorEntity(
coordinator=coordinator,
description=description,
)
for description in SENSORS
)
class EnergyZeroSensorEntity(
CoordinatorEntity[EnergyZeroDataUpdateCoordinator], SensorEntity
):
"""Defines a EnergyZero sensor."""
_attr_has_entity_name = True
_attr_attribution = "Data provided by EnergyZero"
entity_description: EnergyZeroSensorEntityDescription
def __init__(
self,
*,
coordinator: EnergyZeroDataUpdateCoordinator,
description: EnergyZeroSensorEntityDescription,
) -> None:
"""Initialize EnergyZero sensor."""
super().__init__(coordinator=coordinator)
self.entity_description = description
self.entity_id = (
f"{SENSOR_DOMAIN}.{DOMAIN}_{description.service_type}_{description.key}"
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.service_type}_{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={
(
DOMAIN,
f"{coordinator.config_entry.entry_id}_{description.service_type}",
)
},
manufacturer="EnergyZero",
name=SERVICE_TYPE_DEVICE_NAMES[self.entity_description.service_type],
)
@property
def native_value(self) -> float | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -0,0 +1,12 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"step": {
"user": {
"description": "Do you want to start set up?"
}
}
}
}

View File

@ -112,6 +112,7 @@ FLOWS = {
"elmax",
"emonitor",
"emulated_roku",
"energyzero",
"enocean",
"enphase_envoy",
"environment_canada",

View File

@ -1363,6 +1363,12 @@
"config_flow": true,
"iot_class": "local_push"
},
"energyzero": {
"name": "EnergyZero",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"enigma2": {
"name": "Enigma2 (OpenWebif)",
"integration_type": "hub",

View File

@ -647,6 +647,9 @@ emulated_roku==0.2.1
# homeassistant.components.huisbaasje
energyflip-client==0.2.2
# homeassistant.components.energyzero
energyzero==0.3.1
# homeassistant.components.enocean
enocean==0.50

View File

@ -500,6 +500,9 @@ emulated_roku==0.2.1
# homeassistant.components.huisbaasje
energyflip-client==0.2.2
# homeassistant.components.energyzero
energyzero==0.3.1
# homeassistant.components.enocean
enocean==0.50

View File

@ -0,0 +1 @@
"""Tests for the EnergyZero integration."""

View File

@ -0,0 +1,61 @@
"""Fixtures for EnergyZero integration tests."""
from collections.abc import Generator
import json
from unittest.mock import AsyncMock, MagicMock, patch
from energyzero import Electricity, Gas
import pytest
from homeassistant.components.energyzero.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Mock setting up a config entry."""
with patch(
"homeassistant.components.energyzero.async_setup_entry", return_value=True
) as mock_setup:
yield mock_setup
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="energy",
domain=DOMAIN,
data={},
unique_id="unique_thingy",
)
@pytest.fixture
def mock_energyzero() -> Generator[MagicMock, None, None]:
"""Return a mocked EnergyZero client."""
with patch(
"homeassistant.components.energyzero.coordinator.EnergyZero", autospec=True
) as energyzero_mock:
client = energyzero_mock.return_value
client.energy_prices.return_value = Electricity.from_dict(
json.loads(load_fixture("today_energy.json", DOMAIN))
)
client.gas_prices.return_value = Gas.from_dict(
json.loads(load_fixture("today_gas.json", DOMAIN))
)
yield client
@pytest.fixture
async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_energyzero: MagicMock
) -> MockConfigEntry:
"""Set up the EnergyZero integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@ -0,0 +1,104 @@
{
"Prices": [
{
"price": 0.35,
"readingDate": "2022-12-06T23:00:00Z"
},
{
"price": 0.32,
"readingDate": "2022-12-07T00:00:00Z"
},
{
"price": 0.28,
"readingDate": "2022-12-07T01:00:00Z"
},
{
"price": 0.26,
"readingDate": "2022-12-07T02:00:00Z"
},
{
"price": 0.27,
"readingDate": "2022-12-07T03:00:00Z"
},
{
"price": 0.28,
"readingDate": "2022-12-07T04:00:00Z"
},
{
"price": 0.28,
"readingDate": "2022-12-07T05:00:00Z"
},
{
"price": 0.38,
"readingDate": "2022-12-07T06:00:00Z"
},
{
"price": 0.41,
"readingDate": "2022-12-07T07:00:00Z"
},
{
"price": 0.46,
"readingDate": "2022-12-07T08:00:00Z"
},
{
"price": 0.44,
"readingDate": "2022-12-07T09:00:00Z"
},
{
"price": 0.39,
"readingDate": "2022-12-07T10:00:00Z"
},
{
"price": 0.33,
"readingDate": "2022-12-07T11:00:00Z"
},
{
"price": 0.37,
"readingDate": "2022-12-07T12:00:00Z"
},
{
"price": 0.44,
"readingDate": "2022-12-07T13:00:00Z"
},
{
"price": 0.48,
"readingDate": "2022-12-07T14:00:00Z"
},
{
"price": 0.49,
"readingDate": "2022-12-07T15:00:00Z"
},
{
"price": 0.55,
"readingDate": "2022-12-07T16:00:00Z"
},
{
"price": 0.37,
"readingDate": "2022-12-07T17:00:00Z"
},
{
"price": 0.4,
"readingDate": "2022-12-07T18:00:00Z"
},
{
"price": 0.4,
"readingDate": "2022-12-07T19:00:00Z"
},
{
"price": 0.32,
"readingDate": "2022-12-07T20:00:00Z"
},
{
"price": 0.33,
"readingDate": "2022-12-07T21:00:00Z"
},
{
"price": 0.31,
"readingDate": "2022-12-07T22:00:00Z"
}
],
"intervalType": 4,
"average": 0.37,
"fromDate": "2022-12-06T23:00:00Z",
"tillDate": "2022-12-07T22:59:59.999Z"
}

View File

@ -0,0 +1,200 @@
{
"Prices": [
{
"price": 1.43,
"readingDate": "2022-12-05T23:00:00Z"
},
{
"price": 1.43,
"readingDate": "2022-12-06T00:00:00Z"
},
{
"price": 1.43,
"readingDate": "2022-12-06T01:00:00Z"
},
{
"price": 1.43,
"readingDate": "2022-12-06T02:00:00Z"
},
{
"price": 1.43,
"readingDate": "2022-12-06T03:00:00Z"
},
{
"price": 1.43,
"readingDate": "2022-12-06T04:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T05:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T06:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T07:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T08:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T09:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T10:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T11:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T12:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T13:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T14:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T15:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T16:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T17:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T18:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T19:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T20:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T21:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T22:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-06T23:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-07T00:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-07T01:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-07T02:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-07T03:00:00Z"
},
{
"price": 1.45,
"readingDate": "2022-12-07T04:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T05:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T06:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T07:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T08:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T09:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T10:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T11:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T12:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T13:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T14:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T15:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T16:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T17:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T18:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T19:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T20:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T21:00:00Z"
},
{
"price": 1.47,
"readingDate": "2022-12-07T22:00:00Z"
}
],
"intervalType": 4,
"average": 1.46,
"fromDate": "2022-12-06T23:00:00Z",
"tillDate": "2022-12-07T22:59:59.999Z"
}

View File

@ -0,0 +1,32 @@
"""Test the EnergyZero config flow."""
from unittest.mock import MagicMock
from homeassistant.components.energyzero.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
async def test_full_user_flow(
hass: HomeAssistant,
mock_setup_entry: MagicMock,
) -> None:
"""Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result.get("type") == FlowResultType.FORM
assert result.get("step_id") == SOURCE_USER
assert "flow_id" in result
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={},
)
assert result2.get("type") == FlowResultType.CREATE_ENTRY
assert result2.get("title") == "EnergyZero"
assert result2.get("data") == {}
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,45 @@
"""Tests for the EnergyZero integration."""
from unittest.mock import MagicMock, patch
from energyzero import EnergyZeroConnectionError
from homeassistant.components.energyzero.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_energyzero: MagicMock
) -> None:
"""Test the EnergyZero configuration entry loading/unloading."""
mock_config_entry.add_to_hass(hass)
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
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.data.get(DOMAIN)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@patch(
"homeassistant.components.energyzero.coordinator.EnergyZero._request",
side_effect=EnergyZeroConnectionError,
)
async def test_config_flow_entry_not_ready(
mock_request: MagicMock,
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the EnergyZero configuration entry not ready."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_request.call_count == 1
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@ -0,0 +1,180 @@
"""Tests for the sensors provided by the EnergyZero integration."""
from unittest.mock import MagicMock
from energyzero import EnergyZeroNoDataError
import pytest
from homeassistant.components.energyzero.const import DOMAIN
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
CURRENCY_EURO,
ENERGY_KILO_WATT_HOUR,
STATE_UNKNOWN,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
@pytest.mark.freeze_time("2022-12-07 15:00:00")
async def test_energy_today(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the EnergyZero - Energy sensors."""
entry_id = init_integration.entry_id
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
# Current energy price sensor
state = hass.states.get("sensor.energyzero_today_energy_current_hour_price")
entry = entity_registry.async_get(
"sensor.energyzero_today_energy_current_hour_price"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_current_hour_price"
assert state.state == "0.49"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price Current hour"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY
assert ATTR_ICON not in state.attributes
# Average price sensor
state = hass.states.get("sensor.energyzero_today_energy_average_price")
entry = entity_registry.async_get("sensor.energyzero_today_energy_average_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_average_price"
assert state.state == "0.37"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price Average - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY
assert ATTR_ICON not in state.attributes
# Highest price sensor
state = hass.states.get("sensor.energyzero_today_energy_max_price")
entry = entity_registry.async_get("sensor.energyzero_today_energy_max_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_max_price"
assert state.state == "0.55"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price Highest price - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}"
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY
assert ATTR_ICON not in state.attributes
# Highest price time sensor
state = hass.states.get("sensor.energyzero_today_energy_highest_price_time")
entry = entity_registry.async_get(
"sensor.energyzero_today_energy_highest_price_time"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_highest_price_time"
assert state.state == "2022-12-07T16:00:00+00:00"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price Time of highest price - today"
)
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
assert ATTR_ICON not in state.attributes
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_today_energy")}
assert device_entry.manufacturer == "EnergyZero"
assert device_entry.name == "Energy market price"
assert device_entry.entry_type is dr.DeviceEntryType.SERVICE
assert not device_entry.model
assert not device_entry.sw_version
@pytest.mark.freeze_time("2022-12-07 15:00:00")
async def test_gas_today(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the EnergyZero - Gas sensors."""
entry_id = init_integration.entry_id
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
# Current gas price sensor
state = hass.states.get("sensor.energyzero_today_gas_current_hour_price")
entry = entity_registry.async_get("sensor.energyzero_today_gas_current_hour_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_gas_current_hour_price"
assert state.state == "1.47"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}"
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.MONETARY
assert ATTR_ICON not in state.attributes
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_today_gas")}
assert device_entry.manufacturer == "EnergyZero"
assert device_entry.name == "Gas market price"
assert device_entry.entry_type is dr.DeviceEntryType.SERVICE
assert not device_entry.model
assert not device_entry.sw_version
@pytest.mark.freeze_time("2022-12-07 15:00:00")
async def test_no_gas_today(
hass: HomeAssistant, mock_energyzero: MagicMock, init_integration: MockConfigEntry
) -> None:
"""Test the EnergyZero - No gas sensors available."""
await async_setup_component(hass, "homeassistant", {})
mock_energyzero.gas_prices.side_effect = EnergyZeroNoDataError
await hass.services.async_call(
"homeassistant",
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["sensor.energyzero_today_gas_current_hour_price"]},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.energyzero_today_gas_current_hour_price")
assert state
assert state.state == STATE_UNKNOWN