Add easyEnergy integration (#86266)

This commit is contained in:
Klaas Schoute 2023-02-03 19:56:17 +01:00 committed by GitHub
parent 4c1147e62b
commit 3723241937
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1346 additions and 0 deletions

View File

@ -286,6 +286,8 @@ build.json @home-assistant/supervisor
/tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k
/tests/components/eafm/ @Jc2k
/homeassistant/components/easyenergy/ @klaasnicolaas
/tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecobee/ @marthoc
/tests/components/ecobee/ @marthoc
/homeassistant/components/econet/ @vangorra @w1ll1am23

View File

@ -0,0 +1,35 @@
"""The easyEnergy 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 EasyEnergyDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up easyEnergy from a config entry."""
coordinator = EasyEnergyDataUpdateCoordinator(hass)
try:
await coordinator.async_config_entry_first_refresh()
except ConfigEntryNotReady:
await coordinator.easyenergy.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 easyEnergy 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 easyEnergy 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 EasyEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for easyEnergy 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="easyEnergy",
data={},
)

View File

@ -0,0 +1,17 @@
"""Constants for the easyEnergy integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final
DOMAIN: Final = "easyenergy"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=10)
THRESHOLD_HOUR: Final = 14
SERVICE_TYPE_DEVICE_NAMES = {
"today_energy_usage": "Energy market price - Usage",
"today_energy_return": "Energy market price - Return",
"today_gas": "Gas market price",
}

View File

@ -0,0 +1,82 @@
"""The Coordinator for easyEnergy."""
from __future__ import annotations
from datetime import timedelta
from typing import NamedTuple
from easyenergy import (
EasyEnergy,
EasyEnergyConnectionError,
EasyEnergyNoDataError,
Electricity,
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 EasyEnergyData(NamedTuple):
"""Class for defining data in dict."""
energy_today: Electricity
energy_tomorrow: Electricity | None
gas_today: Gas | None
class EasyEnergyDataUpdateCoordinator(DataUpdateCoordinator[EasyEnergyData]):
"""Class to manage fetching easyEnergy data from single endpoint."""
config_entry: ConfigEntry
def __init__(self, hass) -> None:
"""Initialize global easyEnergy data updater."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.easyenergy = EasyEnergy(session=async_get_clientsession(hass))
async def _async_update_data(self) -> EasyEnergyData:
"""Fetch data from easyEnergy."""
today = dt.now().date()
gas_today = None
energy_tomorrow = None
try:
energy_today = await self.easyenergy.energy_prices(
start_date=today, end_date=today
)
try:
gas_today = await self.easyenergy.gas_prices(
start_date=today, end_date=today
)
except EasyEnergyNoDataError:
LOGGER.debug("No data for gas prices for easyEnergy 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.easyenergy.energy_prices(
start_date=tomorrow, end_date=tomorrow
)
except EasyEnergyNoDataError:
LOGGER.debug(
"No electricity data for tomorrow for easyEnergy integration"
)
except EasyEnergyConnectionError as err:
raise UpdateFailed("Error communicating with easyEnergy API") from err
return EasyEnergyData(
energy_today=energy_today,
energy_tomorrow=energy_tomorrow,
gas_today=gas_today,
)

View File

@ -0,0 +1,70 @@
"""Diagnostics support for easyEnergy."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import EasyEnergyDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import EasyEnergyData
def get_gas_price(data: EasyEnergyData, hours: int) -> float | None:
"""Get the gas price for a given hour.
Args:
data: The data object.
hours: The number of hours to add to the current time.
Returns:
The gas market price value.
"""
if not data.gas_today:
return None
return data.gas_today.price_at_time(
data.gas_today.utcnow() + timedelta(hours=hours)
)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: EasyEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return {
"entry": {
"title": entry.title,
},
"energy_usage": {
"current_hour_price": coordinator.data.energy_today.current_usage_price,
"next_hour_price": coordinator.data.energy_today.price_at_time(
coordinator.data.energy_today.utcnow() + timedelta(hours=1)
),
"average_price": coordinator.data.energy_today.average_usage_price,
"max_price": coordinator.data.energy_today.extreme_usage_prices[1],
"min_price": coordinator.data.energy_today.extreme_usage_prices[0],
"highest_price_time": coordinator.data.energy_today.highest_usage_price_time,
"lowest_price_time": coordinator.data.energy_today.lowest_usage_price_time,
"percentage_of_max": coordinator.data.energy_today.pct_of_max_usage,
},
"energy_return": {
"current_hour_price": coordinator.data.energy_today.current_return_price,
"next_hour_price": coordinator.data.energy_today.price_at_time(
coordinator.data.energy_today.utcnow() + timedelta(hours=1), "return"
),
"average_price": coordinator.data.energy_today.average_return_price,
"max_price": coordinator.data.energy_today.extreme_return_prices[1],
"min_price": coordinator.data.energy_today.extreme_return_prices[0],
"highest_price_time": coordinator.data.energy_today.highest_return_price_time,
"lowest_price_time": coordinator.data.energy_today.lowest_return_price_time,
"percentage_of_max": coordinator.data.energy_today.pct_of_max_return,
},
"gas": {
"current_hour_price": get_gas_price(coordinator.data, 0),
"next_hour_price": get_gas_price(coordinator.data, 1),
},
}

View File

@ -0,0 +1,10 @@
{
"domain": "easyenergy",
"name": "easyEnergy",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
"requirements": ["easyenergy==0.1.2"],
"codeowners": ["@klaasnicolaas"],
"iot_class": "cloud_polling",
"quality_scale": "platinum"
}

View File

@ -0,0 +1,247 @@
"""Support for easyEnergy 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 EasyEnergyData, EasyEnergyDataUpdateCoordinator
@dataclass
class EasyEnergySensorEntityDescriptionMixin:
"""Mixin for required keys."""
value_fn: Callable[[EasyEnergyData], float | datetime | None]
service_type: str
@dataclass
class EasyEnergySensorEntityDescription(
SensorEntityDescription, EasyEnergySensorEntityDescriptionMixin
):
"""Describes easyEnergy sensor entity."""
SENSORS: tuple[EasyEnergySensorEntityDescription, ...] = (
EasyEnergySensorEntityDescription(
key="current_hour_price",
name="Current hour",
service_type="today_gas",
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,
),
EasyEnergySensorEntityDescription(
key="next_hour_price",
name="Next hour",
service_type="today_gas",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}",
value_fn=lambda data: get_gas_price(data, 1),
),
EasyEnergySensorEntityDescription(
key="current_hour_price",
name="Current hour",
service_type="today_energy_usage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.current_usage_price,
),
EasyEnergySensorEntityDescription(
key="next_hour_price",
name="Next hour",
service_type="today_energy_usage",
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)
),
),
EasyEnergySensorEntityDescription(
key="average_price",
name="Average - today",
service_type="today_energy_usage",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.average_usage_price,
),
EasyEnergySensorEntityDescription(
key="max_price",
name="Highest price - today",
service_type="today_energy_usage",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_usage_prices[1],
),
EasyEnergySensorEntityDescription(
key="min_price",
name="Lowest price - today",
service_type="today_energy_usage",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_usage_prices[0],
),
EasyEnergySensorEntityDescription(
key="highest_price_time",
name="Time of highest price - today",
service_type="today_energy_usage",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.highest_usage_price_time,
),
EasyEnergySensorEntityDescription(
key="lowest_price_time",
name="Time of lowest price - today",
service_type="today_energy_usage",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.lowest_usage_price_time,
),
EasyEnergySensorEntityDescription(
key="percentage_of_max",
name="Current percentage of highest price - today",
service_type="today_energy_usage",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
value_fn=lambda data: data.energy_today.pct_of_max_usage,
),
EasyEnergySensorEntityDescription(
key="current_hour_price",
name="Current hour",
service_type="today_energy_return",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.current_return_price,
),
EasyEnergySensorEntityDescription(
key="next_hour_price",
name="Next hour",
service_type="today_energy_return",
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), "return"
),
),
EasyEnergySensorEntityDescription(
key="average_price",
name="Average - today",
service_type="today_energy_return",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.average_return_price,
),
EasyEnergySensorEntityDescription(
key="max_price",
name="Highest price - today",
service_type="today_energy_return",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_return_prices[1],
),
EasyEnergySensorEntityDescription(
key="min_price",
name="Lowest price - today",
service_type="today_energy_return",
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
value_fn=lambda data: data.energy_today.extreme_return_prices[0],
),
EasyEnergySensorEntityDescription(
key="highest_price_time",
name="Time of highest price - today",
service_type="today_energy_return",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.highest_return_price_time,
),
EasyEnergySensorEntityDescription(
key="lowest_price_time",
name="Time of lowest price - today",
service_type="today_energy_return",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.energy_today.lowest_return_price_time,
),
EasyEnergySensorEntityDescription(
key="percentage_of_max",
name="Current percentage of highest price - today",
service_type="today_energy_return",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
value_fn=lambda data: data.energy_today.pct_of_max_return,
),
)
def get_gas_price(data: EasyEnergyData, 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 easyEnergy sensors based on a config entry."""
coordinator: EasyEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
EasyEnergySensorEntity(coordinator=coordinator, description=description)
for description in SENSORS
)
class EasyEnergySensorEntity(
CoordinatorEntity[EasyEnergyDataUpdateCoordinator], SensorEntity
):
"""Defines a easyEnergy sensor."""
_attr_has_entity_name = True
_attr_attribution = "Data provided by easyEnergy"
entity_description: EasyEnergySensorEntityDescription
def __init__(
self,
*,
coordinator: EasyEnergyDataUpdateCoordinator,
description: EasyEnergySensorEntityDescription,
) -> None:
"""Initialize easyEnergy 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}",
)
},
configuration_url="https://www.easyenergy.com",
manufacturer="easyEnergy",
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 setup?"
}
}
}
}

View File

@ -102,6 +102,7 @@ FLOWS = {
"dunehd",
"dynalite",
"eafm",
"easyenergy",
"ecobee",
"econet",
"ecowitt",

View File

@ -1203,6 +1203,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"easyenergy": {
"name": "easyEnergy",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"ebox": {
"name": "EBox",
"integration_type": "hub",

View File

@ -624,6 +624,9 @@ dynalite_devices==0.1.47
# homeassistant.components.rainforest_eagle
eagle100==0.1.1
# homeassistant.components.easyenergy
easyenergy==0.1.2
# homeassistant.components.ebusd
ebusdpy==0.0.17

View File

@ -489,6 +489,9 @@ dynalite_devices==0.1.47
# homeassistant.components.rainforest_eagle
eagle100==0.1.1
# homeassistant.components.easyenergy
easyenergy==0.1.2
# homeassistant.components.elgato
elgato==3.0.0

View File

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

View File

@ -0,0 +1,61 @@
"""Fixtures for easyEnergy integration tests."""
from collections.abc import Generator
import json
from unittest.mock import AsyncMock, MagicMock, patch
from easyenergy import Electricity, Gas
import pytest
from homeassistant.components.easyenergy.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.easyenergy.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_easyenergy() -> Generator[MagicMock, None, None]:
"""Return a mocked easyEnergy client."""
with patch(
"homeassistant.components.easyenergy.coordinator.EasyEnergy", autospec=True
) as easyenergy_mock:
client = easyenergy_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_easyenergy: MagicMock
) -> MockConfigEntry:
"""Set up the easyEnergy 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,146 @@
[
{
"Timestamp": "2023-01-18T23:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1349513,
"TariffReturn": 0.11153
},
{
"Timestamp": "2023-01-19T00:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1294458,
"TariffReturn": 0.10698
},
{
"Timestamp": "2023-01-19T01:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1270137,
"TariffReturn": 0.10497
},
{
"Timestamp": "2023-01-19T02:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1230812,
"TariffReturn": 0.10172
},
{
"Timestamp": "2023-01-19T03:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1297483,
"TariffReturn": 0.10723
},
{
"Timestamp": "2023-01-19T04:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1386902,
"TariffReturn": 0.11462
},
{
"Timestamp": "2023-01-19T05:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1439174,
"TariffReturn": 0.11894
},
{
"Timestamp": "2023-01-19T06:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.193479,
"TariffReturn": 0.1599
},
{
"Timestamp": "2023-01-19T07:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.19844,
"TariffReturn": 0.164
},
{
"Timestamp": "2023-01-19T08:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2077449,
"TariffReturn": 0.17169
},
{
"Timestamp": "2023-01-19T09:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.16819,
"TariffReturn": 0.139
},
{
"Timestamp": "2023-01-19T10:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1649835,
"TariffReturn": 0.13635
},
{
"Timestamp": "2023-01-19T11:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.156816,
"TariffReturn": 0.1296
},
{
"Timestamp": "2023-01-19T12:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1873927,
"TariffReturn": 0.15487
},
{
"Timestamp": "2023-01-19T13:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1941929,
"TariffReturn": 0.16049
},
{
"Timestamp": "2023-01-19T14:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2129116,
"TariffReturn": 0.17596
},
{
"Timestamp": "2023-01-19T15:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2254109,
"TariffReturn": 0.18629
},
{
"Timestamp": "2023-01-19T16:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2467674,
"TariffReturn": 0.20394
},
{
"Timestamp": "2023-01-19T17:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2390597,
"TariffReturn": 0.19757
},
{
"Timestamp": "2023-01-19T18:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.2074303,
"TariffReturn": 0.17143
},
{
"Timestamp": "2023-01-19T19:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1815,
"TariffReturn": 0.15
},
{
"Timestamp": "2023-01-19T20:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1795761,
"TariffReturn": 0.14841
},
{
"Timestamp": "2023-01-19T21:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.1807014,
"TariffReturn": 0.14934
},
{
"Timestamp": "2023-01-19T22:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.16819,
"TariffReturn": 0.139
}
]

View File

@ -0,0 +1,146 @@
[
{
"Timestamp": "2023-01-19T05:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T06:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T07:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T08:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T09:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T10:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T11:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T12:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T13:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T14:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T15:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T16:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T17:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T18:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T19:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T20:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T21:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T22:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-19T23:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-20T00:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-20T01:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-20T02:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-20T03:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
},
{
"Timestamp": "2023-01-20T04:00:00+00:00",
"SupplierId": 0,
"TariffUsage": 0.7252982,
"TariffReturn": 0.7252982
}
]

View File

@ -0,0 +1,32 @@
"""Test the easyEnergy config flow."""
from unittest.mock import MagicMock
from homeassistant.components.easyenergy.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") == "easyEnergy"
assert result2.get("data") == {}
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,106 @@
"""Tests for the diagnostics data provided by the easyEnergy integration."""
from unittest.mock import MagicMock
from aiohttp import ClientSession
from easyenergy import EasyEnergyNoDataError
import pytest
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
@pytest.mark.freeze_time("2023-01-19 15:00:00")
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSession,
init_integration: MockConfigEntry,
) -> None:
"""Test diagnostics."""
assert await get_diagnostics_for_config_entry(
hass, hass_client, init_integration
) == {
"entry": {
"title": "energy",
},
"energy_usage": {
"current_hour_price": 0.22541,
"next_hour_price": 0.24677,
"average_price": 0.17665,
"max_price": 0.24677,
"min_price": 0.12308,
"highest_price_time": "2023-01-19T16:00:00+00:00",
"lowest_price_time": "2023-01-19T02:00:00+00:00",
"percentage_of_max": 91.34,
},
"energy_return": {
"current_hour_price": 0.18629,
"next_hour_price": 0.20394,
"average_price": 0.14599,
"max_price": 0.20394,
"min_price": 0.10172,
"highest_price_time": "2023-01-19T16:00:00+00:00",
"lowest_price_time": "2023-01-19T02:00:00+00:00",
"percentage_of_max": 91.35,
},
"gas": {
"current_hour_price": 0.7253,
"next_hour_price": 0.7253,
},
}
@pytest.mark.freeze_time("2023-01-19 15:00:00")
async def test_diagnostics_no_gas_today(
hass: HomeAssistant,
hass_client: ClientSession,
mock_easyenergy: MagicMock,
init_integration: MockConfigEntry,
) -> None:
"""Test diagnostics, no gas sensors available."""
await async_setup_component(hass, "homeassistant", {})
mock_easyenergy.gas_prices.side_effect = EasyEnergyNoDataError
await hass.services.async_call(
"homeassistant",
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: ["sensor.easyenergy_today_gas_current_hour_price"]},
blocking=True,
)
await hass.async_block_till_done()
assert await get_diagnostics_for_config_entry(
hass, hass_client, init_integration
) == {
"entry": {
"title": "energy",
},
"energy_usage": {
"current_hour_price": 0.22541,
"next_hour_price": 0.24677,
"average_price": 0.17665,
"max_price": 0.24677,
"min_price": 0.12308,
"highest_price_time": "2023-01-19T16:00:00+00:00",
"lowest_price_time": "2023-01-19T02:00:00+00:00",
"percentage_of_max": 91.34,
},
"energy_return": {
"current_hour_price": 0.18629,
"next_hour_price": 0.20394,
"average_price": 0.14599,
"max_price": 0.20394,
"min_price": 0.10172,
"highest_price_time": "2023-01-19T16:00:00+00:00",
"lowest_price_time": "2023-01-19T02:00:00+00:00",
"percentage_of_max": 91.35,
},
"gas": {
"current_hour_price": None,
"next_hour_price": None,
},
}

View File

@ -0,0 +1,45 @@
"""Tests for the easyEnergy integration."""
from unittest.mock import MagicMock, patch
from easyenergy import EasyEnergyConnectionError
from homeassistant.components.easyenergy.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_easyenergy: MagicMock
) -> None:
"""Test the easyEnergy 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.easyenergy.coordinator.EasyEnergy._request",
side_effect=EasyEnergyConnectionError,
)
async def test_config_flow_entry_not_ready(
mock_request: MagicMock,
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the easyEnergy 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,278 @@
"""Tests for the sensors provided by the easyEnergy integration."""
from unittest.mock import MagicMock
from easyenergy import EasyEnergyNoDataError
import pytest
from homeassistant.components.easyenergy.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,
STATE_UNKNOWN,
UnitOfEnergy,
UnitOfVolume,
)
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("2023-01-19 15:00:00")
async def test_energy_usage_today(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the easyEnergy - Energy usage sensors."""
entry_id = init_integration.entry_id
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
# Current usage energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_usage_current_hour_price")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_usage_current_hour_price"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_usage_current_hour_price"
assert state.state == "0.22541"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Usage Current hour"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Average usage energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_usage_average_price")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_usage_average_price"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_usage_average_price"
assert state.state == "0.17665"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Usage Average - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Highest usage energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_usage_max_price")
entry = entity_registry.async_get("sensor.easyenergy_today_energy_usage_max_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_usage_max_price"
assert state.state == "0.24677"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Usage Highest price - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Highest usage price time sensor
state = hass.states.get("sensor.easyenergy_today_energy_usage_highest_price_time")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_usage_highest_price_time"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_usage_highest_price_time"
assert state.state == "2023-01-19T16:00:00+00:00"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Usage 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_usage")}
assert device_entry.manufacturer == "easyEnergy"
assert device_entry.name == "Energy market price - Usage"
assert device_entry.entry_type is dr.DeviceEntryType.SERVICE
assert not device_entry.model
assert not device_entry.sw_version
@pytest.mark.freeze_time("2023-01-19 15:00:00")
async def test_energy_return_today(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the easyEnergy - Energy return sensors."""
entry_id = init_integration.entry_id
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
# Current return energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_return_current_hour_price")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_return_current_hour_price"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_return_current_hour_price"
assert state.state == "0.18629"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Return Current hour"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Average return energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_return_average_price")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_return_average_price"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_return_average_price"
assert state.state == "0.14599"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Return Average - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Highest return energy price sensor
state = hass.states.get("sensor.easyenergy_today_energy_return_max_price")
entry = entity_registry.async_get("sensor.easyenergy_today_energy_return_max_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_return_max_price"
assert state.state == "0.20394"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Return Highest price - today"
)
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}"
)
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
# Highest return price time sensor
state = hass.states.get("sensor.easyenergy_today_energy_return_highest_price_time")
entry = entity_registry.async_get(
"sensor.easyenergy_today_energy_return_highest_price_time"
)
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_energy_return_highest_price_time"
assert state.state == "2023-01-19T16:00:00+00:00"
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Energy market price - Return 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_return")}
assert device_entry.manufacturer == "easyEnergy"
assert device_entry.name == "Energy market price - Return"
assert device_entry.entry_type is dr.DeviceEntryType.SERVICE
assert not device_entry.model
assert not device_entry.sw_version
@pytest.mark.freeze_time("2023-01-19 10:00:00")
async def test_gas_today(
hass: HomeAssistant, init_integration: MockConfigEntry
) -> None:
"""Test the easyEnergy - 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.easyenergy_today_gas_current_hour_price")
entry = entity_registry.async_get("sensor.easyenergy_today_gas_current_hour_price")
assert entry
assert state
assert entry.unique_id == f"{entry_id}_today_gas_current_hour_price"
assert state.state == "0.7253"
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}"
)
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes
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 == "easyEnergy"
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("2023-01-19 15:00:00")
async def test_no_gas_today(
hass: HomeAssistant, mock_easyenergy: MagicMock, init_integration: MockConfigEntry
) -> None:
"""Test the easyEnergy - No gas data available."""
await async_setup_component(hass, "homeassistant", {})
mock_easyenergy.gas_prices.side_effect = EasyEnergyNoDataError
await hass.services.async_call(
"homeassistant",
SERVICE_UPDATE_ENTITY,
{ATTR_ENTITY_ID: "sensor.easyenergy_today_gas_current_hour_price"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.easyenergy_today_gas_current_hour_price")
assert state
assert state.state == STATE_UNKNOWN