From 60a00b3b2cd711b68b19aaedc90d899c595c24ca Mon Sep 17 00:00:00 2001 From: Roeland Date: Tue, 10 Sep 2024 18:25:28 +0200 Subject: [PATCH] Add service to fetch prices --- custom_components/entsoe/__init__.py | 20 ++-- custom_components/entsoe/const.py | 1 - custom_components/entsoe/coordinator.py | 21 ++-- custom_components/entsoe/sensor.py | 4 +- custom_components/entsoe/services.py | 133 ++++++++++++++++++++++++ custom_components/entsoe/services.yaml | 17 +++ 6 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 custom_components/entsoe/services.py create mode 100644 custom_components/entsoe/services.yaml diff --git a/custom_components/entsoe/__init__.py b/custom_components/entsoe/__init__.py index b149cc1..79b4b87 100644 --- a/custom_components/entsoe/__init__.py +++ b/custom_components/entsoe/__init__.py @@ -4,14 +4,23 @@ import logging from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import ConfigType from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import CONF_COORDINATOR, CONF_VAT_VALUE, DOMAIN, CONF_API_KEY, CONF_AREA, CONF_MODIFYER, DEFAULT_MODIFYER, CALCULATION_MODE, CONF_CALCULATION_MODE + +from .const import CONF_VAT_VALUE, DOMAIN, CONF_API_KEY, CONF_AREA, CONF_MODIFYER, DEFAULT_MODIFYER, CALCULATION_MODE, CONF_CALCULATION_MODE from .coordinator import EntsoeCoordinator +from .services import async_setup_services _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up ENTSO-e services.""" + + async_setup_services(hass) + + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the ENTSO-e prices component from a config entry.""" @@ -24,10 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: calculation_mode = entry.options.get(CONF_CALCULATION_MODE, CALCULATION_MODE["default"]) entsoe_coordinator = EntsoeCoordinator(hass, api_key=api_key, area = area, modifyer = modifyer, calculation_mode=calculation_mode, VAT=vat) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - CONF_COORDINATOR: entsoe_coordinator, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entsoe_coordinator # Fetch initial data, so we have data when entities subscribe and set up the platform await entsoe_coordinator.async_config_entry_first_refresh() @@ -39,10 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + 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/custom_components/entsoe/const.py b/custom_components/entsoe/const.py index eeff5fa..c2db406 100644 --- a/custom_components/entsoe/const.py +++ b/custom_components/entsoe/const.py @@ -8,7 +8,6 @@ CONF_API_KEY = "api_key" CONF_ENTITY_NAME = "name" CONF_AREA = "area" -CONF_COORDINATOR = "coordinator" CONF_MODIFYER = "modifyer" CONF_CURRENCY = "currency" CONF_ADVANCED_OPTIONS = "advanced_options" diff --git a/custom_components/entsoe/coordinator.py b/custom_components/entsoe/coordinator.py index 2db3aac..9341a8d 100644 --- a/custom_components/entsoe/coordinator.py +++ b/custom_components/entsoe/coordinator.py @@ -97,7 +97,7 @@ async def _async_update_data(self) -> dict: self.logger.debug(f"received data = {data}") if data is not None: - parsed_data = self.parse_hourprices(dict(list(data.items())[-48:])) + parsed_data = self.parse_hourprices(data) self.logger.debug(f"received pricing data from entso-e for {len(data)} hours") self.filtered_hourprices = self._filter_calculated_hourprices(parsed_data) return parsed_data @@ -137,19 +137,25 @@ def api_update(self, start_date, end_date, api_key): return client.query_day_ahead_prices( country_code=self.area, start=start_date, end=end_date ) + + async def get_energy_prices(self, start_date, end_date): + #check if we have the data already + if len(self.get_data(start_date)) == 24 and len(self.get_data(end_date)) == 24: + self.logger.debug(f'return prices from coordinator cache.') + return {k: v for k, v in self.data.items() if k.date() >= start_date.date() and k.date() <= end_date.date()} + return await self.fetch_prices(start_date, end_date) def today_data_available(self): return len(self.get_data_today()) == 24 def _filter_calculated_hourprices(self, data): - hourprices = data if self.calculation_mode == CALCULATION_MODE["rotation"]: - return { hour: price for hour, price in hourprices.items() if hour >= self.today and hour < self.today + timedelta(days=1) } + return { hour: price for hour, price in data.items() if hour >= self.today and hour < self.today + timedelta(days=1) } elif self.calculation_mode == CALCULATION_MODE["sliding"]: now = dt.now().replace(minute=0, second=0, microsecond=0) - return { hour: price for hour, price in hourprices.items() if hour >= now } + return { hour: price for hour, price in data.items() if hour >= now } elif self.calculation_mode == CALCULATION_MODE["publish"]: - return hourprices + return dict(list(data.items())[-48:]) def get_prices_today(self): return self.get_timestamped_prices(self.get_data_today()) @@ -158,8 +164,11 @@ def get_prices_tomorrow(self): return self.get_timestamped_prices(self.get_data_tomorrow()) def get_prices(self): - return self.get_timestamped_prices(self.data) + return self.get_timestamped_prices(dict(list(self.data.items())[-48:])) + def get_data(self, date): + return {k: v for k, v in self.data.items() if k.date() == date.date()} + def get_data_today(self): return {k: v for k, v in self.data.items() if k.date() == self.today.date()} diff --git a/custom_components/entsoe/sensor.py b/custom_components/entsoe/sensor.py index e424e28..5d85239 100644 --- a/custom_components/entsoe/sensor.py +++ b/custom_components/entsoe/sensor.py @@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import utcnow -from .const import ATTRIBUTION, CONF_COORDINATOR, CONF_ENTITY_NAME, DOMAIN, DEFAULT_CURRENCY, CONF_CURRENCY +from .const import ATTRIBUTION, CONF_ENTITY_NAME, DOMAIN, DEFAULT_CURRENCY, CONF_CURRENCY from .coordinator import EntsoeCoordinator _LOGGER = logging.getLogger(__name__) @@ -113,7 +113,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ENTSO-e price sensor entries.""" - entsoe_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] + entsoe_coordinator = hass.data[DOMAIN][config_entry.entry_id] entities = [] entity = {} diff --git a/custom_components/entsoe/services.py b/custom_components/entsoe/services.py new file mode 100644 index 0000000..6c1714d --- /dev/null +++ b/custom_components/entsoe/services.py @@ -0,0 +1,133 @@ +"""The Entso-e services.""" + +from __future__ import annotations + +from datetime import date, datetime +from functools import partial +from typing import Final + +import voluptuous as vol +import logging + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import selector +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .coordinator import EntsoeCoordinator + +_LOGGER = logging.getLogger(__name__) + +ATTR_CONFIG_ENTRY: Final = "config_entry" +ATTR_START: Final = "start" +ATTR_END: Final = "end" + +ENERGY_SERVICE_NAME: Final = "get_energy_prices" +SERVICE_SCHEMA: Final = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Optional(ATTR_START): str, + vol.Optional(ATTR_END): str, + } +) + + +def __get_date(date_input: str | None) -> date | datetime: + """Get date.""" + if not date_input: + return dt_util.now().date() + + if value := dt_util.parse_datetime(date_input): + return value + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_date", + translation_placeholders={ + "date": date_input, + }, + ) + + +def __serialize_prices(prices) -> ServiceResponse: + """Serialize prices.""" + return { + "prices": [ + { + "timestamp": dt.isoformat(), + "price": price + } + for dt, price in prices.items() + ] + } + + +def __get_coordinator( + hass: HomeAssistant, call: ServiceCall +) -> EntsoeCoordinator: + """Get the coordinator from the entry.""" + entry_id: str = call.data[ATTR_CONFIG_ENTRY] + entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id) + + if not entry: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + translation_placeholders={ + "config_entry": entry_id, + }, + ) + if entry.state != ConfigEntryState.LOADED: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="unloaded_config_entry", + translation_placeholders={ + "config_entry": entry.title, + }, + ) + + coordinator: EntsoeCoordinator = hass.data[DOMAIN][entry_id] + return coordinator + + +async def __get_prices( + call: ServiceCall, + *, + hass: HomeAssistant, +) -> ServiceResponse: + coordinator = __get_coordinator(hass, call) + + start = __get_date(call.data.get(ATTR_START)) + end = __get_date(call.data.get(ATTR_END)) + + data = await coordinator.get_energy_prices( + start_date=start, + end_date=end, + ) + + return __serialize_prices(data) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Set up Entso-e services.""" + + hass.services.async_register( + DOMAIN, + ENERGY_SERVICE_NAME, + partial(__get_prices, hass=hass), + schema=SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) \ No newline at end of file diff --git a/custom_components/entsoe/services.yaml b/custom_components/entsoe/services.yaml new file mode 100644 index 0000000..fe99abc --- /dev/null +++ b/custom_components/entsoe/services.yaml @@ -0,0 +1,17 @@ +get_energy_prices: + fields: + config_entry: + required: true + selector: + config_entry: + integration: entsoe + start: + required: false + example: "2023-01-01 00:00:00" + selector: + datetime: + end: + required: false + example: "2023-01-01 00:00:00" + selector: + datetime: \ No newline at end of file