Skip to content

Commit 88d4c9a

Browse files
committed
Enhance price minimum handling to consider end-of-day rates
Refactor methods to return a tuple with next price minimum timestamp and a flag for rechecking due to incomplete next-day rates. Update inverter charge controller logic to adjust waiting times and energy usage estimates accordingly.
1 parent c3752ec commit 88d4c9a

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

source/inverter_charge_controller.py

+33-8
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,22 @@ def start(self) -> None:
4747
while True:
4848
try:
4949
if first_iteration:
50-
next_price_minimum = self.tibber_api_handler.get_timestamp_of_next_price_minimum(first_iteration)
50+
next_price_minimum, minimum_has_to_be_rechecked = (
51+
self.tibber_api_handler.get_timestamp_of_next_price_minimum(first_iteration)
52+
)
5153
first_iteration = False
5254
else:
53-
next_price_minimum = self._do_iteration()
55+
next_price_minimum, minimum_has_to_be_rechecked = self._do_iteration()
56+
57+
time_to_sleep_to = next_price_minimum
58+
if minimum_has_to_be_rechecked:
59+
time_to_sleep_to = time_to_sleep_to.replace(hour=14, minute=0, second=0, microsecond=0)
60+
self.log.info(f"The price minimum has to re-checked at {time_to_sleep_to}. Waiting until then...")
61+
else:
62+
self.log.info(f"The next price minimum is at {next_price_minimum}. Waiting until then...")
5463

55-
self.log.info(f"The next price minimum is at {next_price_minimum}. Waiting until then...")
5664
self._write_newlines_to_log_file()
57-
pause.until(next_price_minimum)
65+
pause.until(time_to_sleep_to)
5866

5967
except (ClientError, RequestException) as e:
6068
self.log.exception(f"An exception occurred while trying to fetch data from a different system: {e}")
@@ -66,7 +74,7 @@ def start(self) -> None:
6674
self.log.critical("Exiting now...")
6775
exit(1)
6876

69-
def _do_iteration(self) -> datetime: # FIXME: Find better name
77+
def _do_iteration(self) -> tuple[datetime, bool]: # FIXME: Find better name
7078
"""
7179
Computes the optimal charging strategy for an inverter until the next price minimum.
7280
@@ -80,16 +88,23 @@ def _do_iteration(self) -> datetime: # FIXME: Find better name
8088
8189
Returns:
8290
datetime: The timestamp of the next price minimum.
91+
minimum_has_to_be_rechecked: Whether the price minimum has to be re-checked since not all the prices were
92+
available yet.
8393
"""
8494
self.log.info(
8595
"Waiting is over, now is the a price minimum. Checking what has to be done to reach the next minimum..."
8696
)
8797

8898
timestamp_now = datetime.now(tz=self.timezone)
8999

90-
next_price_minimum = self.tibber_api_handler.get_timestamp_of_next_price_minimum()
100+
next_price_minimum, minimum_has_to_be_rechecked = self.tibber_api_handler.get_timestamp_of_next_price_minimum()
91101
self.log.info(f"The next price minimum is at {next_price_minimum}")
92102

103+
if minimum_has_to_be_rechecked:
104+
self.log.info(
105+
"The price minimum has to be re-checked since it is at the end of a day and the price rates for tomorrow are unavailable"
106+
)
107+
93108
expected_power_harvested_till_next_minimum = self.sun_forecast_handler.get_solar_output_in_timeframe(
94109
timestamp_now, next_price_minimum
95110
)
@@ -111,6 +126,15 @@ def _do_iteration(self) -> datetime: # FIXME: Find better name
111126
expected_energy_usage_till_next_minimum = self.sems_portal_api_handler.estimate_energy_usage_in_timeframe(
112127
timestamp_now, next_price_minimum
113128
)
129+
130+
if minimum_has_to_be_rechecked:
131+
power_usage_factor = 1.15
132+
self.log.info(
133+
"Since the real price minimum is unknown at the moment the expected power usage "
134+
+ f"({expected_energy_usage_till_next_minimum}) is increased by {(power_usage_factor - 1 ) * 100} %"
135+
)
136+
expected_energy_usage_till_next_minimum = expected_energy_usage_till_next_minimum * power_usage_factor
137+
114138
self.log.info(
115139
f"The total expected energy usage till the next price minimum is {expected_energy_usage_till_next_minimum}"
116140
)
@@ -140,6 +164,7 @@ def _do_iteration(self) -> datetime: # FIXME: Find better name
140164
"current energy in battery": current_energy_in_battery,
141165
"target min state of charge": target_min_state_of_charge,
142166
"energy to be in battery when reaching next minimum": energy_to_be_in_battery_when_reaching_next_minimum,
167+
"minimum_has_to_be_rechecked": minimum_has_to_be_rechecked,
143168
}
144169
self.log.debug(f"Summary of energy values: {summary_of_energy_vales}")
145170

@@ -151,7 +176,7 @@ def _do_iteration(self) -> datetime: # FIXME: Find better name
151176
)
152177
if excess_energy.watt_hours > 0:
153178
self.log.info(f"There is {excess_energy} of excess energy, thus there is no need to charge")
154-
return next_price_minimum
179+
return next_price_minimum, minimum_has_to_be_rechecked
155180

156181
missing_energy = excess_energy * -1
157182
self.log.info(f"There is a need to charge {missing_energy}")
@@ -181,7 +206,7 @@ def _do_iteration(self) -> datetime: # FIXME: Find better name
181206
timestamp_starting_to_charge, timestamp_ending_to_charge, energy_bought
182207
)
183208

184-
return next_price_minimum
209+
return next_price_minimum, minimum_has_to_be_rechecked
185210

186211
def _charge_inverter(self, target_state_of_charge: int) -> None:
187212
"""

source/tibber_api_handler.py

+57-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from datetime import datetime
1+
from datetime import datetime, timedelta
22

3+
from dateutil import tz
34
from energy_amount import EnergyRate
45
from environment_variable_getter import EnvironmentVariableGetter
56
from gql import Client, gql
@@ -18,7 +19,7 @@ def __init__(self):
1819
self.client = Client(transport=transport, fetch_schema_from_transport=True)
1920
self.maximum_threshold = 0.03 # in €
2021

21-
def get_timestamp_of_next_price_minimum(self, first_iteration: bool = False) -> datetime:
22+
def get_timestamp_of_next_price_minimum(self, first_iteration: bool = False) -> tuple[datetime, bool]:
2223
"""
2324
This method performs a series of operations to determine the most cost-effective time to charge by analyzing
2425
upcoming energy rates retrieved from the Tibber API and returns its timestamp.
@@ -46,6 +47,8 @@ def get_timestamp_of_next_price_minimum(self, first_iteration: bool = False) ->
4647
4748
Returns:
4849
datetime: The timestamp of the next minimum energy rate.
50+
minimum_has_to_be_rechecked: Whether the price minimum has to be re-checked since not all the prices were
51+
available yet.
4952
"""
5053
self.log.debug("Finding the price minimum...")
5154
api_result = self._fetch_upcoming_prices_from_api()
@@ -58,7 +61,13 @@ def get_timestamp_of_next_price_minimum(self, first_iteration: bool = False) ->
5861
energy_rates_between_first_and_second_maximum
5962
)
6063

61-
return minimum_of_energy_rates.timestamp
64+
minimum_has_to_be_rechecked = (
65+
self._check_if_minimum_is_at_end_of_day_and_energy_rates_of_tomorrow_are_unavailable(
66+
minimum_of_energy_rates, upcoming_energy_rates
67+
)
68+
)
69+
70+
return minimum_of_energy_rates.timestamp, minimum_has_to_be_rechecked
6271

6372
def _fetch_upcoming_prices_from_api(self) -> dict:
6473
"""
@@ -212,3 +221,48 @@ def get_global_minimum_of_energy_rates(self, energy_rates_till_maximum: list[Ene
212221
f"Found {global_minimum_of_energy_rates} to be the global minimum of the energy rates between the first and second maximum"
213222
)
214223
return global_minimum_of_energy_rates
224+
225+
def _check_if_minimum_is_at_end_of_day_and_energy_rates_of_tomorrow_are_unavailable(
226+
self, price_minimum: EnergyRate, upcoming_energy_rates: list[EnergyRate]
227+
) -> bool:
228+
"""
229+
This method determines whether the timestamp of the `price_minimum` falls within the last 3 hours of the current
230+
day and checks if there are no energy rates available for the subsequent day.
231+
This is done since the price rates of the next day are only available after ~ 02:00 PM. If the price rates of
232+
the next day are unavailable while determining the price minimum, it is likely that the price minimum is just
233+
the last rate of the day but not actually the minimum.
234+
In this case we have to check in later (after ~ 02:00 PM) to re-request the prices from the Tibber API to get
235+
the values of the next day.
236+
237+
Args:
238+
price_minimum (EnergyRate): The energy rate with the minimum price.
239+
upcoming_energy_rates (list[EnergyRate]): List of upcoming energy rates.
240+
241+
Returns:
242+
bool: True if the price minimum is in the last 3 hours of the current day and there are no rates for
243+
the next day, otherwise False.
244+
"""
245+
246+
# We use 00:01 instead of 00:00 since the software runs just a few (milli)seconds after the start of the hour
247+
timezone = tz.gettz()
248+
end_of_day = (datetime.now(tz=timezone) + timedelta(days=1)).replace(hour=0, minute=1, second=0, microsecond=0)
249+
three_hours_before_end_of_day = end_of_day - timedelta(hours=3)
250+
251+
is_price_minimum_near_end_of_day = price_minimum.timestamp >= three_hours_before_end_of_day
252+
self.log.trace(
253+
f"The price minimum {price_minimum.timestamp} is at the end of the day: {is_price_minimum_near_end_of_day}"
254+
)
255+
256+
are_tomorrows_rates_unavailable = all(
257+
rate.timestamp.date() == price_minimum.timestamp.date() for rate in upcoming_energy_rates
258+
)
259+
self.log.trace(f"The price rates for tomorrow are unavailable: {are_tomorrows_rates_unavailable}")
260+
261+
return is_price_minimum_near_end_of_day and are_tomorrows_rates_unavailable
262+
263+
264+
if __name__ == "__main__":
265+
api_handler = TibberAPIHandler()
266+
timestamp, minimum_has_to_be_rechecked = api_handler.get_timestamp_of_next_price_minimum(first_iteration=True)
267+
print(f"The timestamp of the next minimum energy rate is {timestamp}")
268+
print(f"The minimum has to be re-checked: {minimum_has_to_be_rechecked}")

0 commit comments

Comments
 (0)