Skip to content

Commit 59f8b7b

Browse files
Optimise windows in price order (low to high)
Also workaround GivTCP bug setting charge windows
1 parent e334de6 commit 59f8b7b

1 file changed

Lines changed: 57 additions & 12 deletions

File tree

predbat.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from datetime import datetime, timedelta
99
import math
1010
import re
11+
import time
1112
import pytz
1213
import appdaemon.plugins.hass.hassapi as hass
1314
import requests
@@ -16,7 +17,8 @@
1617
TIME_FORMAT_SECONDS = "%Y-%m-%dT%H:%M:%S.%f%z"
1718
TIME_FORMAT_OCTOPUS = "%Y-%m-%d %H:%M:%S%z"
1819
MAX_CHARGE_LIMITS = 10
19-
SIMULATE = False # Debug option, when set don't write to entities but simulate each 15 min period
20+
SIMULATE = False # Debug option, when set don't write to entities but simulate each 30 min period
21+
SIMULATE_LENGTH = 23*60 # How many periods to simulate, set to 0 for just current
2022

2123
class PredBat(hass.Hass):
2224
"""
@@ -586,6 +588,20 @@ def adjust_battery_target(self, soc):
586588
else:
587589
self.log("Current SOC is {} already at target".format(current_soc))
588590

591+
def write_and_poll_option(self, name, entity, new_value):
592+
"""
593+
GivTCP Workaround, keep writing until correct
594+
"""
595+
old_value = ""
596+
tries = 16
597+
while old_value != new_value and tries > 0:
598+
entity.call_service("select_option", option=new_value)
599+
time.sleep(2)
600+
old_value = entity.get_state()
601+
tries -=1
602+
if tries == 0:
603+
self.log("WARN: Trying to write {} to {} didn't complete".format(name, new_value))
604+
589605
def adjust_force_discharge(self, force_discharge, new_start_time=None, new_end_time=None):
590606
"""
591607
Adjust force discharge on/off
@@ -626,7 +642,8 @@ def adjust_force_discharge(self, force_discharge, new_start_time=None, new_end_t
626642
if SIMULATE:
627643
self.sim_discharge_start = new_start
628644
else:
629-
entity_discharge_start_time.call_service("select_option", option=new_start)
645+
self.write_and_poll_option("discharge_start_time", entity_discharge_start_time, new_start)
646+
# entity_discharge_start_time.call_service("select_option", option=new_start)
630647

631648
# Change end time
632649
if new_end and new_end != old_end:
@@ -636,7 +653,8 @@ def adjust_force_discharge(self, force_discharge, new_start_time=None, new_end_t
636653
if SIMULATE:
637654
self.sim_discharge_end = new_end
638655
else:
639-
entity_discharge_end_time.call_service("select_option", option=new_end)
656+
self.write_and_poll_option("discharge_end_time", entity_discharge_end_time, new_end)
657+
# entity_discharge_end_time.call_service("select_option", option=new_end)
640658

641659
# Change inverter mode
642660
if old_inverter_mode != new_inverter_mode:
@@ -670,6 +688,8 @@ def adjust_charge_window(self, charge_start_time, charge_end_time):
670688
new_start = charge_start_time.strftime("%H:%M:%S")
671689
new_end = charge_end_time.strftime("%H:%M:%S")
672690

691+
self.log("Charge window is {} - {}, being changed to {} - {}".format(old_start, old_end, new_start, new_end))
692+
673693
if not SIMULATE and old_charge_schedule_enable == 'off':
674694
# Enable scheduled charge if not turned on
675695
entity_start = self.get_entity(self.get_arg('scheduled_charge_enable', indirect=False))
@@ -686,7 +706,8 @@ def adjust_charge_window(self, charge_start_time, charge_end_time):
686706
self.log("Simulate sim_charge_start_time now {}".format(new_start))
687707
else:
688708
entity_start = self.get_entity(self.get_arg('charge_start_time', indirect=False))
689-
entity_start.call_service("select_option", option=new_start)
709+
# entity_start.call_service("select_option", option=new_start)
710+
self.write_and_poll_option("charge_start_time", entity_start, new_start)
690711

691712
# Program end slot
692713
if new_end != old_end:
@@ -695,7 +716,8 @@ def adjust_charge_window(self, charge_start_time, charge_end_time):
695716
self.log("Simulate sim_charge_end_time now {}".format(new_end))
696717
else:
697718
entity_end = self.get_entity(self.get_arg('charge_end_time', indirect=False))
698-
entity_end.call_service("select_option", option=new_end)
719+
self.write_and_poll_option("charge_end_time", entity_end, new_end)
720+
# entity_end.call_service("select_option", option=new_end)
699721

700722
if new_start != old_start or new_end != old_end:
701723
if self.get_arg('set_window_notify', False) and not SIMULATE:
@@ -1202,7 +1224,7 @@ def optimise_charge_limit(self, window_n, record_charge_windows, try_charge_limi
12021224
best_cost = 0
12031225
prev_soc = self.soc_max + 1
12041226

1205-
while loop_soc > self.reserve:
1227+
while loop_soc >= self.reserve:
12061228
was_debug = self.debug_enable
12071229
self.debug_enable = False
12081230

@@ -1320,6 +1342,27 @@ def optimise_discharge(self, window_n, record_charge_windows, try_charge_limit,
13201342

13211343
return best_discharge, best_metric, best_cost, soc_min
13221344

1345+
def window_sort_func(self, window):
1346+
"""
1347+
Helper sort index function
1348+
"""
1349+
return window['average']
1350+
1351+
def sort_window_by_price(self, windows):
1352+
"""
1353+
Sort the charge windows by lowest price first, return a list of window IDs
1354+
"""
1355+
window_with_id = windows[:]
1356+
wid = 0
1357+
for window in window_with_id:
1358+
window['id'] = wid
1359+
wid += 1
1360+
window_with_id.sort(key=self.window_sort_func)
1361+
id_list = []
1362+
for window in window_with_id:
1363+
id_list.append(window['id'])
1364+
# self.log("Sorted window ids {}".format(id_list))
1365+
return id_list
13231366

13241367
def update_pred(self):
13251368
"""
@@ -1571,10 +1614,11 @@ def update_pred(self):
15711614
# Set all to min
15721615
self.charge_limit_best = [self.reserve if n < record_charge_windows else self.soc_max for n in range(0, len(self.charge_limit_best))]
15731616

1574-
for window_n in range(0, record_charge_windows):
1617+
# Optimise in price order, cheapest first
1618+
for window_n in self.sort_window_by_price(self.charge_window_best[:record_charge_windows]):
15751619
best_soc, best_metric, best_cost, soc_min = self.optimise_charge_limit(window_n, record_charge_windows, self.charge_limit_best, self.charge_window_best, self.discharge_window_best, self.discharge_enable_best, load_minutes, pv_forecast_minute, pv_forecast_minute10)
15761620

1577-
if self.debug_enable:
1621+
if self.debug_enable or 1:
15781622
self.log("Best charge limit window {} (adjusted) soc calculated at {} min {} (margin added {} and min {}) with metric {} cost {}".format(window_n, self.dp2(best_soc), self.dp2(soc_min), self.best_soc_margin, self.best_soc_min, self.dp2(best_metric), self.dp2(best_cost)))
15791623
self.charge_limit_best[window_n] = best_soc
15801624

@@ -1629,6 +1673,7 @@ def update_pred(self):
16291673

16301674
# Avoid adjust avoid start time forward when it's already started
16311675
if (self.charge_start_time_minutes < self.minutes_now) and (self.minutes_now >= minutes_start):
1676+
self.log("Include original start {} with our start which is {}".format(self.charge_start_time_minutes, minutes_start))
16321677
minutes_start = self.charge_start_time_minutes
16331678

16341679
# Check if start is within 24 hours of now and end is in the future
@@ -1643,8 +1688,8 @@ def update_pred(self):
16431688
(minutes_start - self.minutes_now) <= self.set_window_minutes or
16441689
(self.charge_start_time_minutes - self.minutes_now) <= self.set_window_minutes
16451690
):
1646-
self.log("Configurating charge window now (now {} target set_window_minutes {} charge start time {}".format(self.time_abs_str(self.minutes_now), self.set_window_minutes, self.time_abs_str(minutes_start)))
1647-
self.adjust_charge_window(charge_start_time, charge_end_time)
1691+
self.log("Configuring charge window now (now {} target set_window_minutes {} charge start time {}".format(self.time_abs_str(self.minutes_now), self.set_window_minutes, self.time_abs_str(minutes_start)))
1692+
self.adjust_charge_window(charge_start_time, charge_end_time)
16481693
else:
16491694
self.log("Not setting charging window yet as not within the window (now {} target set_window_minutes {} charge start time {}".format(self.time_abs_str(self.minutes_now),self.set_window_minutes, self.time_abs_str(minutes_start)))
16501695

@@ -1726,8 +1771,8 @@ def initialize(self):
17261771
self.reset()
17271772
self.auto_config()
17281773

1729-
if SIMULATE:
1730-
for offset in range (0, 23*60, 30):
1774+
if SIMULATE and SIMULATE_LENGTH:
1775+
for offset in range (0, SIMULATE_LENGTH, 30):
17311776
now = datetime.now()
17321777
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
17331778
minutes_now = int((now - midnight).seconds / 60)

0 commit comments

Comments
 (0)