Skip to content

Commit 24fc5d5

Browse files
committed
Eliminated the INVERTER_MAX_TARGET_SOC restriction from the environment and charging logic. Updated the inverter charging process to account for consecutive cheap energy rates, allowing for extended charging times if favorable pricing persists.
1 parent e1be4d3 commit 24fc5d5

File tree

4 files changed

+71
-18
lines changed

4 files changed

+71
-18
lines changed

.env.example

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ TIBBER_API_TOKEN=
1313
INVERTER_HOSTNAME=
1414
INVERTER_BATTERY_CAPACITY=
1515
INVERTER_TARGET_MIN_STATE_OF_CHARGE=
16-
INVERTER_MAX_TARGET_SOC=
1716

1817
SEMSPORTAL_USERNAME=
1918
SEMSPORTAL_PASSWORD=

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ When the current time is a price minimum, the program wakes up and does the foll
134134
| `INVERTER_HOSTNAME` | The hostname or IP of the inverter. | - | [`inverter.mydomain.com`, `192.168.5.10`, ...] |
135135
| `INVERTER_BATTERY_CAPACITY` | The capacity of the battery in watt hours without any separators. | - | A number, typically between `3000` and `15000` |
136136
| `INVERTER_TARGET_MIN_STATE_OF_CHARGE` | The state of charge the battery shall have when reaching the next minimum as a buffer. | `20` | A number between `0` and `100`, typically between `0` and `40` |
137-
| `INVERTER_MAX_TARGET_SOC` | The maximum state of charge the inverter will charge to since the last few percent take a long time to charge. | `90` | A number between `0` and `100`, typically between `80` and `100` |
138137
| `SEMSPORTAL_USERNAME` | The username to login into the SEMSPortal. | - | A string, example: `[email protected]` |
139138
| `SEMSPORTAL_PASSWORD` | The password to login into the SEMSPortal. | - | A string, example: `my-secret-password` |
140139
| `SEMSPORTAL_POWERSTATION_ID` | The ID of the inverter in the SEMSPortal. This can be found at the end of the URL in the browser after logging in. | - | A string, example: `aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee` |

source/inverter_charge_controller.py

+25-15
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def _start(self) -> None:
7878
self.sems_portal_api_handler.write_values_to_database()
7979

8080
if first_iteration:
81-
next_price_minimum = self.tibber_api_handler.get_next_price_minimum(first_iteration)
81+
next_price_minimum, _ = self.tibber_api_handler.get_next_price_minimum(first_iteration)
8282
first_iteration = False
8383
else:
8484
next_price_minimum = self._do_iteration(next_price_minimum.rate)
@@ -94,7 +94,7 @@ def _start(self) -> None:
9494
)
9595
pause.until(time_to_sleep_to)
9696
self.log.info("Waking up since the the price minimum has to re-checked")
97-
next_price_minimum = self.tibber_api_handler.get_next_price_minimum(True)
97+
next_price_minimum, _ = self.tibber_api_handler.get_next_price_minimum(True)
9898

9999
self.log.info(f"The next price minimum is at {next_price_minimum.timestamp}. Waiting until then...")
100100

@@ -132,10 +132,11 @@ def _do_iteration(self, current_energy_rate: float) -> EnergyRate:
132132

133133
timestamp_now = TimeHandler.get_time()
134134

135-
next_price_minimum = self.tibber_api_handler.get_next_price_minimum()
135+
next_price_minimum, consecutive_energy_rate_is_cheap = self.tibber_api_handler.get_next_price_minimum()
136136
self.log.info(f"The next price minimum is at {next_price_minimum}")
137137

138138
if next_price_minimum.rate > current_energy_rate:
139+
# Information is unused at the moment
139140
self.log.info("The price of the upcoming minimum is higher than the current energy rate")
140141

141142
if next_price_minimum.is_minimum_that_has_to_be_rechecked:
@@ -203,6 +204,7 @@ def _do_iteration(self, current_energy_rate: float) -> EnergyRate:
203204
summary_of_energy_vales = {
204205
"timestamp now": str(timestamp_now),
205206
"next price minimum": str(next_price_minimum.timestamp),
207+
"consecutive_energy_rate_is_cheap": consecutive_energy_rate_is_cheap,
206208
"expected power harvested till next minimum": expected_power_harvested_till_next_minimum,
207209
"expected energy usage till next minimum": expected_energy_usage_till_next_minimum,
208210
"current state of charge": current_state_of_charge,
@@ -232,20 +234,11 @@ def _do_iteration(self, current_energy_rate: float) -> EnergyRate:
232234
)
233235
self.log.info(f"Need to charge to {required_state_of_charge} %")
234236

235-
max_target_soc_environment_variable = "INVERTER_MAX_TARGET_SOC"
236-
max_target_soc = int(EnvironmentVariableGetter.get(max_target_soc_environment_variable, 90))
237-
if required_state_of_charge > max_target_soc:
238-
self.log.info(
239-
"The target state of charge is more than the maximum allowed charge set in the environment "
240-
+ f'("{max_target_soc_environment_variable}") --> Setting it to {max_target_soc} %'
241-
)
242-
required_state_of_charge = max_target_soc
243-
244237
energy_bought_before_charging = self.sems_portal_api_handler.get_energy_buy()
245238
timestamp_starting_to_charge = TimeHandler.get_time()
246239
self.log.debug(f"The amount of energy bought before charging is {energy_bought_before_charging}")
247240

248-
self._charge_inverter(required_state_of_charge)
241+
self._charge_inverter(required_state_of_charge, consecutive_energy_rate_is_cheap)
249242

250243
timestamp_ending_to_charge = TimeHandler.get_time()
251244

@@ -268,22 +261,31 @@ def _do_iteration(self, current_energy_rate: float) -> EnergyRate:
268261

269262
return next_price_minimum
270263

271-
def _charge_inverter(self, target_state_of_charge: int) -> None:
264+
def _charge_inverter(self, target_state_of_charge: int, consecutive_energy_rate_is_cheap: bool) -> None:
272265
"""
273266
Charges the inverter until a given state of charge is reached.
274267
Checks every few minutes the current state of charge and compares to the target value.
268+
Charges the inverter for a maximum of one hour. If consecutive_energy_rate_is_cheap is True it charges for a
269+
maximum of two hours.
270+
--> Stops when the target state of charge or the maximum charge time is reached (whichever comes first)
275271
276272
Args:
277273
target_state_of_charge: The desired state of charge percentage to reach before stopping the charging process.
278274
"""
279275
charging_progress_check_interval = timedelta(minutes=5)
280276
dry_run = EnvironmentVariableGetter.get(name_of_variable="DRY_RUN", default_value=True)
281277

278+
maximum_end_charging_time = TimeHandler.get_time().replace(minute=0, second=0) + timedelta(hours=1)
279+
if consecutive_energy_rate_is_cheap:
280+
maximum_end_charging_time += timedelta(hours=1)
281+
282282
self.log.info("Starting to charge")
283283
self.inverter.set_operation_mode(OperationMode.ECO_CHARGE)
284284

285285
self.log.info(
286-
f"Set the inverter to charge, the target state of charge is {target_state_of_charge} %. Checking the charging progress every {charging_progress_check_interval}..."
286+
f"Set the inverter to charge, the target state of charge is {target_state_of_charge} %. "
287+
+ f"The maximum end charging time is {maximum_end_charging_time.strftime("%H:%M:%S")}. "
288+
+ f"Checking the charging progress every {charging_progress_check_interval}..."
287289
)
288290

289291
error_counter = 0
@@ -326,6 +328,14 @@ def _charge_inverter(self, target_state_of_charge: int) -> None:
326328
self.inverter.set_operation_mode(OperationMode.GENERAL)
327329
break
328330

331+
if TimeHandler.get_time() > maximum_end_charging_time:
332+
self.log.info(
333+
f"The maximum end charging time of {maximum_end_charging_time} has been reached "
334+
+ "--> Stopping the charging process"
335+
)
336+
self.inverter.set_operation_mode(OperationMode.GENERAL)
337+
break
338+
329339
self.log.debug(
330340
f"Charging is still ongoing (current: {current_state_of_charge} %, target: >= {target_state_of_charge} %) --> Waiting for another {charging_progress_check_interval}..."
331341
)

source/tibber_api_handler.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def __init__(self):
2222

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

25-
def get_next_price_minimum(self, first_iteration: bool = False) -> EnergyRate:
25+
def get_next_price_minimum(self, first_iteration: bool = False) -> [EnergyRate, bool]:
2626
"""
2727
This method performs a series of operations to determine the most cost-effective time to charge by analyzing
2828
upcoming energy rates retrieved from the Tibber API and returns its timestamp.
@@ -49,6 +49,8 @@ def get_next_price_minimum(self, first_iteration: bool = False) -> EnergyRate:
4949
5050
Returns:
5151
EnergyRate: The next price minimum energy rate.
52+
bool: Whether the energy rate after the minimum is at most self.maximum_threshold more expensive than the
53+
minimum. This is used to determine the maximum charging time.
5254
"""
5355
self.log.debug("Finding the price minimum...")
5456
api_result = self._fetch_upcoming_prices_from_api()
@@ -78,8 +80,51 @@ def get_next_price_minimum(self, first_iteration: bool = False) -> EnergyRate:
7880
):
7981
minimum_of_energy_rates.is_minimum_that_has_to_be_rechecked = True
8082

83+
energy_rate_after_minimum = self._get_energy_rate_after_minimum(minimum_of_energy_rates, upcoming_energy_rates)
84+
energy_rate_after_minimum_is_below_threshold = self._is_consecutive_energy_rate_below_threshold(
85+
minimum_of_energy_rates, energy_rate_after_minimum
86+
)
87+
88+
return minimum_of_energy_rates, energy_rate_after_minimum_is_below_threshold
89+
90+
def _get_energy_rate_after_minimum(
91+
self, minimum_of_energy_rates: EnergyRate, upcoming_energy_rates: list[EnergyRate]
92+
) -> EnergyRate:
93+
"""
94+
Determines the energy rate that immediately follows the minimum energy rate in the list of upcoming energy rates.
95+
If the minimum energy rate is the last element in the list, the same minimum value is returned.
96+
97+
Args:
98+
minimum_of_energy_rates: The minimum energy rate in the list of energy rates.
99+
upcoming_energy_rates: A list of energy rates where the minimum energy rate resides.
100+
101+
Returns:
102+
The energy rate that comes directly after the provided minimum energy rate within the list, or the minimum
103+
energy rate itself if it is the last one in the list.
104+
"""
105+
index = upcoming_energy_rates.index(minimum_of_energy_rates)
106+
if index + 1 < len(upcoming_energy_rates):
107+
return upcoming_energy_rates[index + 1]
108+
109+
self.log.debug("Can't get the energy rate after the minimum since the minimum is the last one in the list")
81110
return minimum_of_energy_rates
82111

112+
def _is_consecutive_energy_rate_below_threshold(
113+
self, first_energy_rate: EnergyRate, second_energy_rate: EnergyRate
114+
) -> bool:
115+
"""
116+
Checks if the difference between two consecutive energy rates is below the defined threshold.
117+
118+
Args:
119+
first_energy_rate: The first energy rate object to compare.
120+
second_energy_rate: The second energy rate object to compare.
121+
122+
Returns:
123+
bool: True if the difference between the rates of the two energy rate objects is less than or
124+
equal to the `maximum_threshold`, otherwise False.
125+
"""
126+
return second_energy_rate.rate - first_energy_rate.rate <= self.maximum_threshold
127+
83128
@staticmethod
84129
def _check_if_next_three_prices_are_greater_than_current_one(all_upcoming_energy_rates: list[EnergyRate]) -> bool:
85130
"""

0 commit comments

Comments
 (0)