88from datetime import datetime , timedelta
99import math
1010import re
11+ import time
1112import pytz
1213import appdaemon .plugins .hass .hassapi as hass
1314import requests
1617TIME_FORMAT_SECONDS = "%Y-%m-%dT%H:%M:%S.%f%z"
1718TIME_FORMAT_OCTOPUS = "%Y-%m-%d %H:%M:%S%z"
1819MAX_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
2123class 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