Skip to content

Commit 74a372e

Browse files
committed
Implement behavior for when the current price minimum is less expensive than the next price minimum
1 parent c2bc57e commit 74a372e

File tree

2 files changed

+84
-84
lines changed

2 files changed

+84
-84
lines changed

source/inverter_charge_controller.py

+57-64
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ def _start(self) -> None:
8080
next_price_minimum = self.tibber_api_handler.get_next_price_minimum(first_iteration)
8181
first_iteration = False
8282
else:
83-
self.write_newlines_to_log_file()
8483
next_price_minimum = self._do_iteration(next_price_minimum)
8584

8685
if next_price_minimum.has_to_be_rechecked:
@@ -124,13 +123,19 @@ def _do_iteration(self, current_energy_rate: EnergyRate) -> EnergyRate:
124123
- Sets the maximum charging duration of the current energy rate.
125124
- Gets the current state of the inverter.
126125
- Gets the average power consumption.
127-
- Calculates the estimated minimum state of charge until the next price minimum.
128-
- If the estimated minimum state of charge is lower than the target minimum state of charge, the method
129-
calculates the required state of charge to reach the target minimum state of charge and initiates charging.
126+
- Calculates the estimated minimum and maximum state of charge until the next price minimum.
127+
- If the next price minimum is higher than the current one
128+
- If the estimated minimum state of charge is higher than the target minimum state of charge no charging is
129+
initiated.
130+
- Else the method calculates the required state of charge to reach the target minimum state of charge and
131+
start charging.
132+
- Else the method calculates how much power can be bought such that no energy of the sun is wasted.
133+
"Wasted" being the energy is sold instead of used to charge the battery.
130134
131135
Returns:
132136
EnergyRate: The next price minimum energy rate.
133137
"""
138+
self.write_newlines_to_log_file()
134139
self.log.info(
135140
"Waiting is over, now is the a price minimum. Checking what has to be done to reach the next minimum..."
136141
)
@@ -145,77 +150,89 @@ def _do_iteration(self, current_energy_rate: EnergyRate) -> EnergyRate:
145150
f"{current_energy_rate.maximum_charging_duration}"
146151
)
147152

148-
if next_price_minimum > current_energy_rate:
149-
# Information is unused at the moment
150-
self.log.info("The price of the upcoming minimum is higher than the current energy rate")
151-
152153
current_state_of_charge = self.inverter.get_state_of_charge()
153154
self.log.info(f"The battery is currently is at {current_state_of_charge}")
154155

155156
average_power_consumption = self._get_average_power_consumption()
156157
self.log.info(f"The average power consumption is {average_power_consumption}")
157158

158-
minimum_of_soc_until_next_price_minimum = self._get_minimum_of_soc_until_next_price_minimum(
159-
next_price_minimum.timestamp,
160-
average_power_consumption,
161-
current_state_of_charge,
162-
next_price_minimum.has_to_be_rechecked,
163-
)
164-
self.log.info(
165-
f"The expected minimum of state of charge until the next price minimum with the current state of charge is "
166-
f"{minimum_of_soc_until_next_price_minimum}"
167-
)
168-
169159
target_min_soc = StateOfCharge.from_percentage(
170160
int(EnvironmentVariableGetter.get("INVERTER_TARGET_MIN_STATE_OF_CHARGE", 15))
171161
)
172162
self.log.info(f"The battery shall be at least at {target_min_soc} at all times")
173163

164+
target_max_soc = StateOfCharge.from_percentage(
165+
int(EnvironmentVariableGetter.get("INVERTER_TARGET_MAX_STATE_OF_CHARGE", 95))
166+
)
167+
self.log.info(f"The battery shall be at most be charged up to {target_max_soc}")
168+
169+
minimum_of_soc_until_next_price_minimum, maximum_of_soc_until_next_price_minimum = (
170+
self.sun_forecast_handler.calculate_min_and_max_of_soc_in_timeframe(
171+
timestamp_now,
172+
next_price_minimum.timestamp,
173+
average_power_consumption,
174+
current_state_of_charge,
175+
next_price_minimum.has_to_be_rechecked,
176+
)
177+
)
178+
self.log.info(
179+
f"The expected minimum of state of charge until the next price minimum with the current state of charge is "
180+
f"{minimum_of_soc_until_next_price_minimum}, "
181+
f"the expected maximum is {maximum_of_soc_until_next_price_minimum}"
182+
)
183+
174184
summary_of_energy_vales = {
175185
"timestamp now": str(timestamp_now),
176186
"next price minimum": next_price_minimum,
177-
"minimum_has_to_be_rechecked": next_price_minimum.has_to_be_rechecked,
187+
"next price minimum has to be rechecked": next_price_minimum.has_to_be_rechecked,
178188
"maximum charging duration": current_energy_rate.format_maximum_charging_duration(),
179189
"current state of charge": current_state_of_charge,
180190
"average power consumption": average_power_consumption.watts,
181-
"minimum of soc until next price minimum": minimum_of_soc_until_next_price_minimum,
182191
"target min soc": target_min_soc,
192+
"target max soc": target_max_soc,
193+
"minimum of soc until next price minimum": minimum_of_soc_until_next_price_minimum,
194+
"maximum of soc until next price minimum": maximum_of_soc_until_next_price_minimum,
183195
}
184196
self.log.debug(f"Summary of energy values: {summary_of_energy_vales}")
185197

186-
if minimum_of_soc_until_next_price_minimum > target_min_soc:
198+
if current_energy_rate > next_price_minimum:
187199
self.log.info(
188-
"The expected minimum state of charge until the next price minimum without additional charging "
189-
"is higher than the target minimum state of charge --> There is no need to charge"
200+
f"The price of the current minimum ({next_price_minimum.rate} ct/kWh) is higher than the one of "
201+
f"the upcoming minimum ({current_energy_rate.rate} ct/kWh) "
202+
"--> Will only charge the battery to reach the next price minimum"
190203
)
191-
self.iteration_cache = {}
192-
return next_price_minimum
204+
if minimum_of_soc_until_next_price_minimum > target_min_soc:
205+
self.log.info(
206+
"The expected minimum state of charge until the next price minimum without additional charging "
207+
"is higher than the target minimum state of charge --> There is no need to charge"
208+
)
209+
self.iteration_cache = {}
210+
return next_price_minimum
193211

194-
required_state_of_charge = current_state_of_charge + (target_min_soc - minimum_of_soc_until_next_price_minimum)
195-
self.log.info(f"There is a need to charge to {required_state_of_charge} (from {current_state_of_charge})")
196-
maximum_possible_state_of_charge = StateOfCharge.from_percentage(100)
197-
if required_state_of_charge > maximum_possible_state_of_charge:
212+
charging_target_soc = current_state_of_charge + (target_min_soc - minimum_of_soc_until_next_price_minimum)
213+
else:
198214
self.log.info(
199-
"The target state of charge is higher than possible "
200-
f"--> Setting it to {maximum_possible_state_of_charge}"
215+
f"The price of the upcoming minimum ({next_price_minimum.rate} ct/kWh) is higher than the one of "
216+
f"the current minimum ({current_energy_rate.rate} ct/kWh)"
217+
"--> Will charge as much as possible without wasting any energy of the sun"
218+
)
219+
charging_target_soc = current_state_of_charge + (
220+
StateOfCharge.from_percentage(100) - maximum_of_soc_until_next_price_minimum
201221
)
202-
required_state_of_charge = maximum_possible_state_of_charge
222+
self.log.info(f"The calculated target state of charge is {charging_target_soc}")
203223

204-
max_target_soc = StateOfCharge.from_percentage(
205-
int(EnvironmentVariableGetter.get("INVERTER_TARGET_MAX_STATE_OF_CHARGE", 95))
206-
)
207-
if required_state_of_charge > max_target_soc:
224+
if charging_target_soc > target_max_soc:
208225
self.log.info(
209226
"The target state of charge is more than the maximum allowed charge set in the environment "
210-
f"--> Setting it to {max_target_soc}"
227+
f"--> Setting it to {target_max_soc}"
211228
)
212-
required_state_of_charge = max_target_soc
229+
charging_target_soc = target_max_soc
213230

214231
energy_bought_before_charging = self.sems_portal_api_handler.get_energy_buy()
215232
timestamp_starting_to_charge = TimeHandler.get_time()
216233
self.log.debug(f"The amount of energy bought before charging is {energy_bought_before_charging}")
217234

218-
self._charge_inverter(required_state_of_charge, current_energy_rate.maximum_charging_duration)
235+
self._charge_inverter(charging_target_soc, current_energy_rate.maximum_charging_duration)
219236

220237
timestamp_ending_to_charge = TimeHandler.get_time()
221238

@@ -408,30 +425,6 @@ def _get_average_power_consumption(self) -> Power:
408425
self._set_cache_key(cache_key, average_power_consumption)
409426
return average_power_consumption
410427

411-
def _get_minimum_of_soc_until_next_price_minimum(
412-
self,
413-
next_price_minimum_timestamp: datetime,
414-
average_power_consumption: Power,
415-
current_soc: StateOfCharge,
416-
minimum_has_to_rechecked: bool,
417-
) -> StateOfCharge:
418-
cache_key = "minimum_of_soc_until_next_price_minimum"
419-
minimum_of_soc_until_next_price_minimum = self._get_value_from_cache_if_exists(cache_key)
420-
if minimum_of_soc_until_next_price_minimum:
421-
return minimum_of_soc_until_next_price_minimum
422-
423-
minimum_of_soc_until_next_price_minimum, _ = (
424-
self.sun_forecast_handler.calculate_minimum_of_soc_and_power_generation_in_timeframe(
425-
TimeHandler.get_time(),
426-
next_price_minimum_timestamp,
427-
average_power_consumption,
428-
current_soc,
429-
minimum_has_to_rechecked,
430-
)
431-
)
432-
self._set_cache_key(cache_key, minimum_of_soc_until_next_price_minimum)
433-
return minimum_of_soc_until_next_price_minimum
434-
435428
def _get_value_from_cache_if_exists(self, cache_key: str) -> Optional[Any]:
436429
if cache_key not in self.iteration_cache.keys():
437430
return None

source/sun_forecast_handler.py

+27-20
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ def __init__(self):
2222

2323
self.database_handler = DatabaseHandler("solar_forecast")
2424

25-
def calculate_minimum_of_soc_and_power_generation_in_timeframe(
25+
def calculate_min_and_max_of_soc_in_timeframe(
2626
self,
2727
timeframe_start: datetime,
2828
timeframe_end: datetime,
2929
average_power_usage: Power,
3030
starting_soc: StateOfCharge,
31-
minimum_has_to_rechecked: bool = False,
32-
) -> tuple[StateOfCharge, EnergyAmount]:
31+
minimum_has_to_rechecked: bool,
32+
) -> tuple[StateOfCharge, StateOfCharge]:
3333
"""
34-
Calculates the minimum state of charge (SOC) and total power generation within a specified timeframe.
34+
Calculates the minimum state of charge (SOC) and maximum state of charge within a specified timeframe.
3535
It considers average power usage, initial SOC and optionally adjusts for higher power usage in cases where the
3636
pricing for the next day is unavailable. This function uses solar data and iteratively computes power usage and
3737
generation for subintervals within the timeframe.
@@ -41,12 +41,12 @@ def calculate_minimum_of_soc_and_power_generation_in_timeframe(
4141
timeframe_end: The ending timestamp of the timeframe.
4242
average_power_usage: The average power consumption over the timeframe.
4343
starting_soc: The battery's state of charge at the beginning of the timeframe.
44-
minimum_has_to_rechecked (optional): Whether to increase the power usage by POWER_USAGE_INCREASE_FACTOR
44+
minimum_has_to_rechecked: Whether to increase the power usage by POWER_USAGE_INCREASE_FACTOR
4545
4646
Returns:
4747
A tuple containing:
4848
- The minimum state of charge observed during the timeframe.
49-
- The total amount of power generated within the timeframe.
49+
- The maximum state of charge observed during the timeframe.
5050
"""
5151
self.log.debug(
5252
"Calculating the estimated minimum of state of charge and power generation in the timeframe "
@@ -66,8 +66,9 @@ def calculate_minimum_of_soc_and_power_generation_in_timeframe(
6666
current_timeframe_start = timeframe_start
6767
soc_after_current_timeframe = starting_soc
6868
minimum_soc = starting_soc
69-
total_power_usage = EnergyAmount(0)
70-
total_power_generation = EnergyAmount(0)
69+
maximum_soc = starting_soc
70+
total_energy_used = EnergyAmount(0)
71+
total_energy_harvested = EnergyAmount(0)
7172

7273
first_iteration = True
7374
while True:
@@ -83,40 +84,46 @@ def calculate_minimum_of_soc_and_power_generation_in_timeframe(
8384

8485
current_timeframe_end = current_timeframe_start + current_timeframe_duration
8586

86-
power_usage_during_timeframe = self._calculate_energy_usage_in_timeframe(
87+
energy_usage_during_timeframe = self._calculate_energy_usage_in_timeframe(
8788
current_timeframe_start, current_timeframe_duration, average_power_usage, power_usage_increase_factor
8889
)
89-
total_power_usage += power_usage_during_timeframe
90-
power_generation_during_timeframe = self._get_energy_produced_in_timeframe_from_solar_data(
90+
total_energy_used += energy_usage_during_timeframe
91+
energy_harvested_during_timeframe = self._get_energy_harvested_in_timeframe_from_solar_data(
9192
current_timeframe_end, current_timeframe_duration, solar_data
9293
)
93-
total_power_generation += power_generation_during_timeframe
94+
total_energy_harvested += energy_harvested_during_timeframe
9495
soc_after_current_timeframe = StateOfCharge(
95-
soc_after_current_timeframe.absolute - power_usage_during_timeframe + power_generation_during_timeframe
96+
soc_after_current_timeframe.absolute
97+
- energy_usage_during_timeframe
98+
+ energy_harvested_during_timeframe
9699
)
97100

98101
if soc_after_current_timeframe < minimum_soc:
99102
log_text = " (new minimum)"
100103
minimum_soc = soc_after_current_timeframe
104+
elif soc_after_current_timeframe > maximum_soc:
105+
log_text = " (new maximum)"
106+
maximum_soc = soc_after_current_timeframe
101107
else:
102108
log_text = ""
103109
self.log.debug(
104110
f"{current_timeframe_end}"
105111
f" - estimated SOC: {soc_after_current_timeframe}{log_text}"
106-
f" - expected power usage: {power_usage_during_timeframe}"
107-
f" - expected power generation: {power_generation_during_timeframe}"
112+
f" - expected energy used: {energy_usage_during_timeframe}"
113+
f" - expected energy harvested: {energy_harvested_during_timeframe}"
108114
)
109115
current_timeframe_start += current_timeframe_duration
110116

111117
if current_timeframe_start >= timeframe_end:
112118
break
113119

114120
self.log.debug(
115-
f"From {timeframe_start} to {timeframe_end} the expected minimum of state of charge is {minimum_soc}, the "
116-
f"expected total amount of power usage is {total_power_usage} and the expected total amount of power "
117-
f"generated is {total_power_generation}"
121+
f"From {timeframe_start} to {timeframe_end} the expected minimum of state of charge is {minimum_soc}, "
122+
f"the expected maximum of state of charge is {maximum_soc}, "
123+
f"the expected total amount of energy used is {total_energy_used} "
124+
f"and the expected total amount of energy harvested is {total_energy_harvested}"
118125
)
119-
return minimum_soc, total_power_generation
126+
return minimum_soc, maximum_soc
120127

121128
def retrieve_solar_data(self, timeframe_start: datetime, timeframe_end: datetime) -> dict[str, Power]:
122129
"""
@@ -303,7 +310,7 @@ def _calculate_energy_usage_in_timeframe(
303310
return average_power_usage * factor_energy_usage_during_the_day
304311
return average_power_usage * factor_energy_usage_during_the_night
305312

306-
def _get_energy_produced_in_timeframe_from_solar_data(
313+
def _get_energy_harvested_in_timeframe_from_solar_data(
307314
self, timeframe_end: datetime, timeframe_duration: timedelta, solar_data: dict[str, Power]
308315
) -> EnergyAmount:
309316
"""

0 commit comments

Comments
 (0)