Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into Supporting-PT15M
Browse files Browse the repository at this point in the history
  • Loading branch information
Pluimvee committed Oct 10, 2024
2 parents ad529d3 + e604e67 commit b09b230
Showing 1 changed file with 82 additions and 27 deletions.
109 changes: 82 additions & 27 deletions custom_components/entsoe/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import logging
from datetime import datetime, timedelta
from datetime import timedelta

import homeassistant.helpers.config_validation as cv
from homeassistant.core import HomeAssistant
Expand All @@ -18,6 +18,9 @@
MIN_HOURS = 20


# This class contains actually two main tasks
# 1. ENTSO: Refresh data from ENTSO on interval basis triggered by HASS every 60 minutes
# 2. ANALYSIS: Implement some analysis on this data, like min(), max(), avg(), perc(). Updated analysis is triggered by an explicit call from a sensor
class EntsoeCoordinator(DataUpdateCoordinator):
"""Get the latest data and update the states."""

Expand Down Expand Up @@ -62,7 +65,7 @@ def __init__(
update_interval=timedelta(minutes=60),
)

# calculate the price using the given template
# ENTSO: recalculate the price using the given template
def calc_price(self, value, fake_dt=None, no_template=False) -> float:
"""Calculate price based on the users settings."""
# Used to inject the current hour.
Expand Down Expand Up @@ -104,7 +107,7 @@ async def _async_update_data(self) -> dict:
now = dt.now()
self.today = now.replace(hour=0, minute=0, second=0, microsecond=0)
if self.check_update_needed(now) is False:
self.logger.debug(f"Skipping api fetch. All data is already available")
self.logger.debug("Skipping api fetch. All data is already available")
return self.data

yesterday = self.today - timedelta(days=1)
Expand Down Expand Up @@ -167,43 +170,86 @@ def api_update(self, start_date, end_date, api_key):
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)) > MIN_HOURS
and len(self.get_data(end_date)) > MIN_HOURS
):
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 self.parse_hourprices(await self.fetch_prices(start_date, end_date))
# ENTSO: Return the data for the given date
def get_data(self, date):
return {k: v for k, v in self.data.items() if k.date() == date.date()}

# ENTSO: Return the data for today
def get_data_today(self):
return self.get_data(self.today)

# ENTSO: Return the data for tomorrow
def get_data_tomorrow(self):
return self.get_data(self.today + timedelta(days=1))

# ENTSO: Return the data for yesterday
def get_data_yesterday(self):
return self.get_data(self.today - timedelta(days=1))

# SENSOR: Do we have data available for today
def today_data_available(self):
return len(self.get_data_today()) > MIN_HOURS

# this method is called by each sensor, each complete hour, and ensures the date and filtered hourprices are in line with the current time
# SENSOR: Get the current price
def get_current_hourprice(self) -> int:
return self.data[dt.now().replace(minute=0, second=0, microsecond=0)]

# SENSOR: Get the next hour price
def get_next_hourprice(self) -> int:
return self.data[
dt.now().replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
]

# SENSOR: Get timestamped prices of today as attribute for Average Sensor
def get_prices_today(self):
return self.get_timestamped_prices(self.get_data_today())

# SENSOR: Get timestamped prices of tomorrow as attribute for Average Sensor
def get_prices_tomorrow(self):
return self.get_timestamped_prices(self.get_data_tomorrow())

# SENSOR: Get timestamped prices of today & tomorrow or yesterday & today as attribute for Average Sensor
def get_prices(self):
if len(self.data) > 48:
return self.get_timestamped_prices(
{hour: price for hour, price in self.data.items() if hour >= self.today}
)
return self.get_timestamped_prices(
{
hour: price
for hour, price in self.data.items()
if hour >= self.today - timedelta(days=1)
}
)

# SENSOR: Timestamp the prices
def get_timestamped_prices(self, hourprices):
list = []
for hour, price in hourprices.items():
str_hour = str(hour)
list.append({"time": str_hour, "price": price})
return list

# --------------------------------------------------------------------------------------------------------------------------------
# ANALYSIS: this method is called by each sensor, each complete hour, and ensures the date and filtered hourprices are in line with the current time
# we could still optimize as not every calculator mode needs hourly updates
def sync_calculator(self):
now = dt.now()
if (
self.calculator_last_sync is None
or self.calculator_last_sync.hour != now.hour
):
self.logger.debug(
f"The calculator needs to be synced with the current time"
)
self.logger.debug("The calculator needs to be synced with the current time")
if self.today.date() != now.date():
self.logger.debug(
f"new day detected: update today and filtered hourprices"
"new day detected: update today and filtered hourprices"
)
self.today = now.replace(hour=0, minute=0, second=0, microsecond=0)
self.filtered_hourprices = self._filter_calculated_hourprices(self.data)

self.calculator_last_sync = now

# ANALYSIS: filter the hourprices on which to apply the calculations based on the calculation_mode
def _filter_calculated_hourprices(self, data):
# rotation = calculations made upon 24hrs today
if self.calculation_mode == CALCULATION_MODE["rotation"]:
Expand Down Expand Up @@ -295,9 +341,18 @@ def get_percentage_of_range(self):
current = self.get_current_hourprice() - min
return round(current / spread * 100, 1)

def get_timestamped_prices(self, hourprices):
list = []
for hour, price in hourprices.items():
str_hour = str(hour)
list.append({"time": str_hour, "price": price})
return list
# --------------------------------------------------------------------------------------------------------------------------------
# SERVICES: returns data from the coordinator cache, or directly from ENTSO when not availble
async def get_energy_prices(self, start_date, end_date):
# check if we have the data already
if (
len(self.get_data(start_date)) > MIN_HOURS
and len(self.get_data(end_date)) > MIN_HOURS
):
self.logger.debug("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 self.parse_hourprices(await self.fetch_prices(start_date, end_date))

0 comments on commit b09b230

Please sign in to comment.