From 372324193767f397370f0efd06b014237fd15ed0 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 3 Feb 2023 19:56:17 +0100 Subject: [PATCH] Add easyEnergy integration (#86266) --- CODEOWNERS | 2 + .../components/easyenergy/__init__.py | 35 +++ .../components/easyenergy/config_flow.py | 31 ++ homeassistant/components/easyenergy/const.py | 17 ++ .../components/easyenergy/coordinator.py | 82 ++++++ .../components/easyenergy/diagnostics.py | 70 +++++ .../components/easyenergy/manifest.json | 10 + homeassistant/components/easyenergy/sensor.py | 247 ++++++++++++++++ .../components/easyenergy/strings.json | 12 + .../easyenergy/translations/en.json | 12 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/easyenergy/__init__.py | 1 + tests/components/easyenergy/conftest.py | 61 ++++ .../easyenergy/fixtures/today_energy.json | 146 +++++++++ .../easyenergy/fixtures/today_gas.json | 146 +++++++++ .../components/easyenergy/test_config_flow.py | 32 ++ .../components/easyenergy/test_diagnostics.py | 106 +++++++ tests/components/easyenergy/test_init.py | 45 +++ tests/components/easyenergy/test_sensor.py | 278 ++++++++++++++++++ 22 files changed, 1346 insertions(+) create mode 100644 homeassistant/components/easyenergy/__init__.py create mode 100644 homeassistant/components/easyenergy/config_flow.py create mode 100644 homeassistant/components/easyenergy/const.py create mode 100644 homeassistant/components/easyenergy/coordinator.py create mode 100644 homeassistant/components/easyenergy/diagnostics.py create mode 100644 homeassistant/components/easyenergy/manifest.json create mode 100644 homeassistant/components/easyenergy/sensor.py create mode 100644 homeassistant/components/easyenergy/strings.json create mode 100644 homeassistant/components/easyenergy/translations/en.json create mode 100644 tests/components/easyenergy/__init__.py create mode 100644 tests/components/easyenergy/conftest.py create mode 100644 tests/components/easyenergy/fixtures/today_energy.json create mode 100644 tests/components/easyenergy/fixtures/today_gas.json create mode 100644 tests/components/easyenergy/test_config_flow.py create mode 100644 tests/components/easyenergy/test_diagnostics.py create mode 100644 tests/components/easyenergy/test_init.py create mode 100644 tests/components/easyenergy/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 26f7476fafc..dafd89c2f56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/easyenergy/__init__.py b/homeassistant/components/easyenergy/__init__.py new file mode 100644 index 00000000000..498a355f0ab --- /dev/null +++ b/homeassistant/components/easyenergy/__init__.py @@ -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 diff --git a/homeassistant/components/easyenergy/config_flow.py b/homeassistant/components/easyenergy/config_flow.py new file mode 100644 index 00000000000..a8c23f2c6e2 --- /dev/null +++ b/homeassistant/components/easyenergy/config_flow.py @@ -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={}, + ) diff --git a/homeassistant/components/easyenergy/const.py b/homeassistant/components/easyenergy/const.py new file mode 100644 index 00000000000..1de7ac0bd58 --- /dev/null +++ b/homeassistant/components/easyenergy/const.py @@ -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", +} diff --git a/homeassistant/components/easyenergy/coordinator.py b/homeassistant/components/easyenergy/coordinator.py new file mode 100644 index 00000000000..b709d929237 --- /dev/null +++ b/homeassistant/components/easyenergy/coordinator.py @@ -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, + ) diff --git a/homeassistant/components/easyenergy/diagnostics.py b/homeassistant/components/easyenergy/diagnostics.py new file mode 100644 index 00000000000..6bc5ed3803a --- /dev/null +++ b/homeassistant/components/easyenergy/diagnostics.py @@ -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), + }, + } diff --git a/homeassistant/components/easyenergy/manifest.json b/homeassistant/components/easyenergy/manifest.json new file mode 100644 index 00000000000..64475c643c7 --- /dev/null +++ b/homeassistant/components/easyenergy/manifest.json @@ -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" +} diff --git a/homeassistant/components/easyenergy/sensor.py b/homeassistant/components/easyenergy/sensor.py new file mode 100644 index 00000000000..0df5b9bd8c2 --- /dev/null +++ b/homeassistant/components/easyenergy/sensor.py @@ -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) diff --git a/homeassistant/components/easyenergy/strings.json b/homeassistant/components/easyenergy/strings.json new file mode 100644 index 00000000000..ed89e0068d4 --- /dev/null +++ b/homeassistant/components/easyenergy/strings.json @@ -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%]" + } + } +} diff --git a/homeassistant/components/easyenergy/translations/en.json b/homeassistant/components/easyenergy/translations/en.json new file mode 100644 index 00000000000..9384d0b5f96 --- /dev/null +++ b/homeassistant/components/easyenergy/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "step": { + "user": { + "description": "Do you want to start setup?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 22fd40c5101..c3b3d814dee 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -102,6 +102,7 @@ FLOWS = { "dunehd", "dynalite", "eafm", + "easyenergy", "ecobee", "econet", "ecowitt", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9a8058d3a58..826d03bbb17 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -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", diff --git a/requirements_all.txt b/requirements_all.txt index 1ad7d192cc5..936697dd769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed5af3edf58..582782b66ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/easyenergy/__init__.py b/tests/components/easyenergy/__init__.py new file mode 100644 index 00000000000..bcac2da2967 --- /dev/null +++ b/tests/components/easyenergy/__init__.py @@ -0,0 +1 @@ +"""Tests for the easyEnergy integration.""" diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py new file mode 100644 index 00000000000..442221c0147 --- /dev/null +++ b/tests/components/easyenergy/conftest.py @@ -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 diff --git a/tests/components/easyenergy/fixtures/today_energy.json b/tests/components/easyenergy/fixtures/today_energy.json new file mode 100644 index 00000000000..8e91a6244ac --- /dev/null +++ b/tests/components/easyenergy/fixtures/today_energy.json @@ -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 + } +] diff --git a/tests/components/easyenergy/fixtures/today_gas.json b/tests/components/easyenergy/fixtures/today_gas.json new file mode 100644 index 00000000000..ed3e0106b06 --- /dev/null +++ b/tests/components/easyenergy/fixtures/today_gas.json @@ -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 + } +] diff --git a/tests/components/easyenergy/test_config_flow.py b/tests/components/easyenergy/test_config_flow.py new file mode 100644 index 00000000000..2ad9b762e83 --- /dev/null +++ b/tests/components/easyenergy/test_config_flow.py @@ -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 diff --git a/tests/components/easyenergy/test_diagnostics.py b/tests/components/easyenergy/test_diagnostics.py new file mode 100644 index 00000000000..90120ff6baf --- /dev/null +++ b/tests/components/easyenergy/test_diagnostics.py @@ -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, + }, + } diff --git a/tests/components/easyenergy/test_init.py b/tests/components/easyenergy/test_init.py new file mode 100644 index 00000000000..ed12d805e7a --- /dev/null +++ b/tests/components/easyenergy/test_init.py @@ -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 diff --git a/tests/components/easyenergy/test_sensor.py b/tests/components/easyenergy/test_sensor.py new file mode 100644 index 00000000000..edbf5e23ae4 --- /dev/null +++ b/tests/components/easyenergy/test_sensor.py @@ -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