@@ -700,8 +700,12 @@ def minute_data_import_export(self, now_utc, key):
700700
701701 import_today = {}
702702 for entity_id in entity_ids :
703- import_today = self .minute_data (self .get_history (entity_id = entity_id , days = self .forecast_days + 1 )[0 ],
704- self .forecast_days + 1 , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True , accumulate = import_today )
703+ history = self .get_history (entity_id = entity_id , days = self .forecast_days + 1 )
704+ if history :
705+ import_today = self .minute_data (history [0 ], self .forecast_days + 1 , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True , accumulate = import_today )
706+ else :
707+ self .log ("WARN: Unable to fetch history for {}" .format (entity_id ))
708+
705709 return import_today
706710
707711 def minute_data_load (self , now_utc ):
@@ -716,7 +720,7 @@ def minute_data_load(self, now_utc):
716720 for entity_id in entity_ids :
717721 history = self .get_history (entity_id = entity_id , days = self .max_days_previous )
718722 if history :
719- load_minutes = self .minute_data (history [0 ], self .max_days_previous , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , scale = self .get_arg ( ' load_scaling' , 1.0 ) , clean_increment = True , accumulate = load_minutes )
723+ load_minutes = self .minute_data (history [0 ], self .max_days_previous , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , scale = self .load_scaling , clean_increment = True , accumulate = load_minutes )
720724 else :
721725 self .log ("WARN: Unable to fetch history for {}" .format (entity_id ))
722726 return load_minutes
@@ -864,13 +868,13 @@ def dp2(self, value):
864868 """
865869 Round to 2 decimal places
866870 """
867- return math . ceil (value * 100 )/ 100
871+ return round (value * 100 )/ 100
868872
869873 def dp3 (self , value ):
870874 """
871875 Round to 3 decimal places
872876 """
873- return math . ceil (value * 1000 )/ 1000
877+ return round (value * 1000 )/ 1000
874878
875879 def in_charge_window (self , charge_window , minute_abs ):
876880 """
@@ -1419,6 +1423,7 @@ def rate_scan_export(self, rates):
14191423 self .rate_export_min_minute = rate_min_minute
14201424 self .rate_export_max_minute = rate_max_minute
14211425 self .rate_export_average = rate_average
1426+ self .rate_export_threshold = rate_average * rate_high_threshold
14221427
14231428 # Find discharging windows
14241429 self .high_export_rates = self .rate_scan_window (rates , rate_low_min_window , rate_average * rate_high_threshold , True )
@@ -1442,23 +1447,23 @@ def publish_rates_export(self):
14421447 if window_n == 0 and not SIMULATE :
14431448 self .set_state ("predbat.high_rate_export_start" , state = rate_high_start_date .strftime (time_format_time ), attributes = {'friendly_name' : 'Next high export rate start' , 'state_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
14441449 self .set_state ("predbat.high_rate_export_end" , state = rate_high_end_date .strftime (time_format_time ), attributes = {'friendly_name' : 'Next high export rate end' , 'state_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1445- self .set_state ("predbat.high_rate_export_cost" , state = rate_high_average , attributes = {'friendly_name' : 'Next high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1450+ self .set_state ("predbat.high_rate_export_cost" , state = self . dp2 ( rate_high_average ) , attributes = {'friendly_name' : 'Next high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
14461451 if window_n == 1 and not SIMULATE :
14471452 self .set_state ("predbat.high_rate_export_start_2" , state = rate_high_start_date .strftime (time_format_time ), attributes = {'friendly_name' : 'Next+1 high export rate start' , 'state_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
14481453 self .set_state ("predbat.high_rate_export_end_2" , state = rate_high_end_date .strftime (time_format_time ), attributes = {'friendly_name' : 'Next+1 high export rate end' , 'state_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1449- self .set_state ("predbat.high_rate_export_cost_2" , state = rate_high_average , attributes = {'friendly_name' : 'Next+1 high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1454+ self .set_state ("predbat.high_rate_export_cost_2" , state = self . dp2 ( rate_high_average ) , attributes = {'friendly_name' : 'Next+1 high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
14501455 window_n += 1
14511456
14521457 # Clear rates that aren't available
14531458 if not self .high_export_rates and not SIMULATE :
1454- self .log ("No high export rate period found" )
1459+ self .log ("No high rate period found" )
14551460 self .set_state ("predbat.high_rate_export_start" , state = 'undefined' , attributes = {'friendly_name' : 'Next high export rate start' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
14561461 self .set_state ("predbat.high_rate_export_end" , state = 'undefined' , attributes = {'friendly_name' : 'Next high export rate end' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1457- self .set_state ("predbat.high_rate_export_cost" , state = self .rate_export_average , attributes = {'friendly_name' : 'Next high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1462+ self .set_state ("predbat.high_rate_export_cost" , state = self .dp2 ( self . rate_export_average ) , attributes = {'friendly_name' : 'Next high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
14581463 if len (self .high_export_rates ) < 2 and not SIMULATE :
14591464 self .set_state ("predbat.high_rate_export_start_2" , state = 'undefined' , attributes = {'friendly_name' : 'Next+1 high export rate start' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
14601465 self .set_state ("predbat.high_rate_export_end_2" , state = 'undefined' , attributes = {'friendly_name' : 'Next+1 high export rate end' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1461- self .set_state ("predbat.high_rate_export_cost_2" , state = self .rate_export_average , attributes = {'friendly_name' : 'Next+1 high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1466+ self .set_state ("predbat.high_rate_export_cost_2" , state = self .dp2 ( self . rate_export_average ) , attributes = {'friendly_name' : 'Next+1 high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
14621467
14631468
14641469 def rate_minmax (self , rates ):
@@ -1519,7 +1524,7 @@ def rate_scan(self, rates, octopus_slots):
15191524 Scan the rates and work out min/max and charging windows
15201525 """
15211526 rate_low_min_window = 5
1522- rate_low_threshold = self .get_arg ( ' rate_low_threshold' , 0.8 )
1527+ rate_low_threshold = self .rate_low_threshold
15231528 self .low_rates = []
15241529
15251530 rate_min , rate_max , rate_average , rate_min_minute , rate_max_minute = self .rate_minmax (rates )
@@ -1530,6 +1535,7 @@ def rate_scan(self, rates, octopus_slots):
15301535 self .rate_min_minute = rate_min_minute
15311536 self .rate_max_minute = rate_max_minute
15321537 self .rate_average = rate_average
1538+ self .rate_threshold = rate_average * rate_low_threshold
15331539
15341540 # Add in any planned octopus slots
15351541 if octopus_slots :
@@ -1593,7 +1599,7 @@ def publish_rates(self, rates, export):
15931599 for minute in range (0 , self .forecast_minutes + 24 * 60 , 30 ):
15941600 minute_timestamp = self .midnight_utc + timedelta (minutes = minute )
15951601 stamp = minute_timestamp .strftime (TIME_FORMAT )
1596- rates_time [stamp ] = rates [minute ]
1602+ rates_time [stamp ] = self . dp2 ( rates [minute ])
15971603
15981604 if export :
15991605 self .publish_rates_export ()
@@ -1602,9 +1608,9 @@ def publish_rates(self, rates, export):
16021608
16031609 if not SIMULATE :
16041610 if export :
1605- self .set_state ("predbat.rates_export" , state = rates [self .minutes_now ], attributes = {'results' : rates_time , 'friendly_name' : 'Export rates' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1611+ self .set_state ("predbat.rates_export" , state = self . dp2 ( rates [self .minutes_now ]) , attributes = {'min' : self . dp2 ( self . rate_export_min ), 'max' : self . dp2 ( self . rate_export_max ), 'average' : self . dp2 ( self . rate_export_average ), 'threshold' : self . dp2 ( self . rate_export_threshold ), 'results' : rates_time , 'friendly_name' : 'Export rates' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
16061612 else :
1607- self .set_state ("predbat.rates" , state = rates [self .minutes_now ], attributes = {'results' : rates_time , 'friendly_name' : 'Import rates' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1613+ self .set_state ("predbat.rates" , state = self . dp2 ( rates [self .minutes_now ]) , attributes = {'min' : self . dp2 ( self . rate_min ), 'max' : self . dp2 ( self . rate_max ), 'average' : self . dp2 ( self . rate_average ), 'threshold' : self . dp2 ( self . rate_threshold ), 'results' : rates_time , 'friendly_name' : 'Import rates' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
16081614 return rates
16091615
16101616 def today_cost (self , import_today , export_today ):
@@ -1834,7 +1840,7 @@ def optimise_charge_limit(self, window_n, record_charge_windows, try_charge_limi
18341840 # Metric adjustment based on 10% outcome weighting
18351841 if metric10 > metricmid :
18361842 metric_diff = metric10 - metricmid
1837- metric_diff *= self .get_arg ( ' pv_metric10_weight' , 0.0 )
1843+ metric_diff *= self .pv_metric10_weight
18381844 metric += metric_diff
18391845 metric = self .dp2 (metric )
18401846
@@ -1859,7 +1865,7 @@ def optimise_charge_limit(self, window_n, record_charge_windows, try_charge_limi
18591865
18601866 prev_soc = try_soc
18611867 prev_metric = metric
1862- loop_soc -= max (self .get_arg ( ' best_soc_step' , 0.5 ) , 0.1 )
1868+ loop_soc -= max (self .best_soc_step , 0.1 )
18631869
18641870 # Add margin last
18651871 best_soc = min (best_soc + self .best_soc_margin , self .soc_max )
@@ -1908,7 +1914,7 @@ def optimise_discharge(self, window_n, record_charge_windows, try_charge_limit,
19081914 # Metric adjustment based on 10% outcome weighting
19091915 if metric10 > metricmid :
19101916 metric_diff = metric10 - metricmid
1911- metric_diff *= self .get_arg ( ' pv_metric10_weight' , 0.0 )
1917+ metric_diff *= self .pv_metric10_weight
19121918 metric += metric_diff
19131919 metric = self .dp2 (metric )
19141920
@@ -2020,20 +2026,44 @@ def update_pred(self):
20202026 self .forecast_days = int ((forecast_hours + 23 )/ 24 )
20212027 self .forecast_minutes = forecast_hours * 60
20222028
2023- load_minutes = self .minute_data_load (now_utc )
2024-
2029+ # Metric config
20252030 self .metric_house = self .get_arg ('metric_house' , 38.0 )
20262031 self .metric_battery = self .get_arg ('metric_battery' , 7.5 )
20272032 self .metric_export = self .get_arg ('metric_export' , 4.0 )
20282033 self .metric_min_improvement = self .get_arg ('metric_min_improvement' , 5.0 )
20292034 self .notify_devices = self .get_arg ('notify_devices' , ['notify' ])
2035+ self .pv_scaling = self .get_arg ('pv_scaling' , 1.0 )
2036+ self .pv_metric10_weight = self .get_arg ('pv_metric10_weight' , 0.0 )
2037+ self .load_scaling = self .get_arg ('load_scaling' , 1.0 )
2038+ self .best_soc_pass_margin = self .get_arg ('best_soc_pass_margin' , 0.0 )
2039+ self .rate_low_threshold = self .get_arg ('rate_low_threshold' , 0.8 )
2040+ self .best_soc_step = self .get_arg ('best_soc_step' , 0.5 )
2041+
2042+ # Battery charging options
2043+ self .battery_loss = 1.0 - self .get_arg ('battery_loss' , 0.05 )
2044+ self .battery_loss_discharge = 1.0 - self .get_arg ('battery_loss_discharge' , 0 )
2045+ self .battery_scaling = self .get_arg ('battery_scaling' , 1.0 )
2046+ self .best_soc_margin = self .get_arg ('best_soc_margin' , 0 )
2047+ self .best_soc_min = self .get_arg ('best_soc_min' , 0.5 )
2048+ self .best_soc_keep = self .get_arg ('best_soc_keep' , 0.5 )
2049+ self .set_soc_minutes = self .get_arg ('set_soc_minutes' , 30 )
2050+ self .set_window_minutes = self .get_arg ('set_window_minutes' , 30 )
2051+
2052+ # Car options
2053+ self .car_charging_hold = self .get_arg ('car_charging_hold' , False )
2054+ self .car_charging_threshold = float (self .get_arg ('car_charging_threshold' , 6.0 )) / 60.0
2055+ self .car_charging_energy_scale = self .get_arg ('car_charging_energy_scale' , 1.0 )
2056+
20302057 self .rate_import = {}
20312058 self .rate_export = {}
20322059 self .rate_slots = []
20332060 self .low_rates = []
20342061 self .octopus_slots = []
20352062 self .cost_today_sofar = 0
20362063
2064+ # Load previous load data
2065+ load_minutes = self .minute_data_load (now_utc )
2066+
20372067 # Car charging information
20382068 self .car_charging_battery_size = float (self .get_arg ('car_charging_battery_size' , 100.0 ))
20392069 self .car_charging_rate = (float (self .get_arg ('car_charging_rate' , 7.4 )))
@@ -2140,16 +2170,6 @@ def update_pred(self):
21402170 if self .import_today :
21412171 self .cost_today_sofar = self .today_cost (self .import_today , self .export_today )
21422172
2143- # Battery charging options
2144- self .battery_loss = 1.0 - self .get_arg ('battery_loss' , 0.05 )
2145- self .battery_loss_discharge = 1.0 - self .get_arg ('battery_loss_discharge' , 0 )
2146- self .battery_scaling = self .get_arg ('battery_scaling' , 1.0 )
2147- self .best_soc_margin = self .get_arg ('best_soc_margin' , 0 )
2148- self .best_soc_min = self .get_arg ('best_soc_min' , 0.5 )
2149- self .best_soc_keep = self .get_arg ('best_soc_keep' , 0.5 )
2150- self .set_soc_minutes = self .get_arg ('set_soc_minutes' , 30 )
2151- self .set_window_minutes = self .get_arg ('set_window_minutes' , 30 )
2152-
21532173 # Find the inverters
21542174 self .num_inverters = int (self .get_arg ('num_inverters' , 1 ))
21552175 self .inverter_limit = 0.0
@@ -2233,20 +2253,17 @@ def update_pred(self):
22332253 pv_forecast_data += self .get_state (entity_id = self .get_arg ('pv_forecast_d6' , indirect = False ), attribute = 'detailedForecast' )
22342254 if 'pv_forecast_d7' in self .args :
22352255 pv_forecast_data += self .get_state (entity_id = self .get_arg ('pv_forecast_d7' , indirect = False ), attribute = 'detailedForecast' )
2236- pv_forecast_minute = self .minute_data (pv_forecast_data , self .forecast_days + 1 , self .midnight_utc , 'pv_estimate' + str (self .get_arg ('pv_estimate' , '' )), 'period_start' , backwards = False , divide_by = 30 , scale = self .get_arg ( ' pv_scaling' , 1.0 ) )
2237- pv_forecast_minute10 = self .minute_data (pv_forecast_data , self .forecast_days + 1 , self .midnight_utc , 'pv_estimate10' , 'period_start' , backwards = False , divide_by = 30 , scale = self .get_arg ( ' pv_scaling' , 1.0 ) )
2256+ pv_forecast_minute = self .minute_data (pv_forecast_data , self .forecast_days + 1 , self .midnight_utc , 'pv_estimate' + str (self .get_arg ('pv_estimate' , '' )), 'period_start' , backwards = False , divide_by = 30 , scale = self .pv_scaling )
2257+ pv_forecast_minute10 = self .minute_data (pv_forecast_data , self .forecast_days + 1 , self .midnight_utc , 'pv_estimate10' , 'period_start' , backwards = False , divide_by = 30 , scale = self .pv_scaling )
22382258 else :
22392259 pv_forecast_minute = {}
22402260 pv_forecast_minute10 = {}
22412261
22422262 # Car charging hold - when enabled battery is held during car charging in simulation
2243- self .car_charging_hold = self .get_arg ('car_charging_hold' , False )
2244- self .car_charging_threshold = float (self .get_arg ('car_charging_threshold' , 6.0 )) / 60.0
2245-
22462263 self .car_charging_energy = {}
22472264 if 'car_charging_energy' in self .args :
22482265 self .car_charging_energy = self .minute_data (self .get_history (entity_id = self .get_arg ('car_charging_energy' , indirect = False ), days = self .max_days_previous )[0 ],
2249- self .max_days_previous , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True , scale = self .get_arg ( ' car_charging_energy_scale' , 1.0 ) )
2266+ self .max_days_previous , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True , scale = self .car_charging_energy_scale )
22502267 self .log ("Car charging hold {} with energy data" .format (self .car_charging_hold ))
22512268 else :
22522269 self .log ("Car charging hold {} threshold {}" .format (self .car_charging_hold , self .car_charging_threshold * 60.0 ))
@@ -2269,7 +2286,7 @@ def update_pred(self):
22692286 self .log ("Optimise all charge windows n={}" .format (record_charge_windows ))
22702287 best_soc , best_metric , best_cost , soc_min , soc_min_minute = self .optimise_charge_limit (0 , record_charge_windows , self .charge_limit_best , self .charge_window_best , self .discharge_window_best , self .discharge_limits_best , load_minutes , pv_forecast_minute , pv_forecast_minute10 , all_n = record_charge_windows , end_record = end_record )
22712288 if record_charge_windows > 1 :
2272- best_soc = min (best_soc + self .get_arg ( ' best_soc_pass_margin' , 0.0 ) , self .soc_max )
2289+ best_soc = min (best_soc + self .best_soc_pass_margin , self .soc_max )
22732290
22742291 # Set all to optimisation
22752292 self .charge_limit_best = [best_soc if n < record_charge_windows else self .soc_max for n in range (0 , len (self .charge_limit_best ))]
0 commit comments