@@ -715,7 +715,7 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
715715 # Hold based on data
716716 car_energy = 0
717717 for offset in range (0 , step ):
718- car_energy += self .get_from_incrementing (self .car_charging_energy , minute_yesterday - offset ) * self . get_arg ( 'car_charging_energy_scale' , 1.0 )
718+ car_energy += self .get_from_incrementing (self .car_charging_energy , minute_yesterday - offset )
719719
720720 if self .debug_enable and car_energy > 0.0 and (minute % 60 ) == 0 and (minute < 60 * 48 ):
721721 self .log ("Hour {} car charging hold with data {} load now {} metric {}" .format (minute / 60 , car_energy , load_yesterday , metric ))
@@ -739,127 +739,76 @@ def run_prediction(self, charge_limit, charge_window, discharge_window, discharg
739739 if record :
740740 load_kwh += load_yesterday
741741
742- # Are we within the charging time window?
743- if self .charge_enable and (charge_window_n >= 0 ) and soc < charge_limit [charge_window_n ]:
744- old_soc = soc
745- soc = min (soc + (self .charge_rate * step ), charge_limit [charge_window_n ])
746-
747- # Apply battery loss to computed charging energy
748- # For now we ignore PV in this as it's probably not a major factor when mains charging is enabled
749- if record :
750- energy = max (0 , soc - old_soc - pv_now ) / self .battery_loss
751-
752- # Must add in grid import for load
753- energy += load_yesterday
754- import_kwh += energy
755- import_kwh_battery += energy
756- if minute_absolute in self .rate_import :
757- metric += self .rate_import [minute_absolute ] * energy
758- else :
759- metric += self .metric_battery * energy
742+ diff = load_yesterday - pv_now
760743
761- if self .debug_enable and (minute % 60 ) == 0 :
762- self .log ("Hour {} battery charging target soc {}" .format (minute / 60 , charge_limit [charge_window_n ]))
744+ # Battery behaviour
745+ battery_draw = 0
746+ if self .charge_enable and (charge_window_n >= 0 ) and soc < charge_limit [charge_window_n ]:
747+ # Charge enable
748+ battery_draw = - max (min (self .charge_rate * step , charge_limit [charge_window_n ] - soc ), 0 )
763749 elif (discharge_window_n >= 0 ) and (soc > self .reserve ) and discharge_enable [discharge_window_n ]:
764- # If force discharging the battery
765- # Stop when the battery runs out also
766- discharge_has_run = True
767-
768- # Work out draw
750+ # Discharge enable
769751 battery_draw = self .discharge_rate * step
770- if soc - self .reserve < battery_draw :
752+ if ( soc - self .reserve ) < battery_draw :
771753 battery_draw = soc - self .reserve
772- soc -= battery_draw
773- diff = load_yesterday - pv_now - battery_draw
774-
775- if diff >= 0 :
776- # Importing despite full draw?
777- energy = diff
778- if record :
779- import_kwh += energy
780- import_kwh_house += energy
781- if minute_absolute in self .rate_import :
782- metric += self .rate_import [minute_absolute ] * energy
783- else :
784- metric += self .metric_house * energy
785- else :
786- # Export
787- energy = - diff
788- if record :
789- export_kwh += energy
790- # self.log("Discharging minute {} rate {} diff {} export {}".format(minute, battery_draw, diff, energy))
791- if minute_absolute in self .rate_export :
792- metric -= self .rate_export [minute_absolute ] * energy
793- else :
794- metric -= self .metric_export * energy
795-
754+ discharge_has_run = True
796755 else :
797- diff = load_yesterday - pv_now
798-
799- # Apply battery loss to charging from PV
800- if diff < 0 :
801- diff *= self .battery_loss
802-
803- # Max charge rate, export over the cap
804- if diff < - (self .charge_rate * step ):
805- soc -= self .charge_rate * step
806- if record :
807- energy = - (diff + self .charge_rate * step )
808- export_kwh += energy
809- if minute_absolute in self .rate_export :
810- metric -= self .rate_export [minute_absolute ] * energy
811- else :
812- metric -= self .metric_export * energy
813-
814- # Max discharge rate, draw from grid over the cap
815- if diff > (self .discharge_rate * step ):
816- soc -= self .discharge_rate * step
817- if record :
818- energy = diff - (self .discharge_rate * step )
819- import_kwh += energy
820- if self .charge_enable and (charge_window_n >= 0 ):
821- # If the battery is on charge anyhow then imports are kind of the same as battery charging (price wise)
822- import_kwh_battery += energy
823- else :
824- # self.log("importing to minute %s amount %s kw total %s kwh total draw %s" % (minute, energy, import_kwh_house, diff))
825- import_kwh_house += energy
826-
827- if minute_absolute in self .rate_import :
828- metric += self .rate_import [minute_absolute ] * energy
829- else :
830- if self .charge_enable and (charge_window_n >= 0 ):
831- metric += self .metric_battery * energy
832- else :
833- metric += self .metric_house * energy
756+ # ECO Mode
757+ if diff > 0 :
758+ battery_draw = min (diff , self .discharge_rate * step )
834759 else :
835- soc -= diff
836-
837- # Flat battery, draw from grid over the cap
838- if soc < self .reserve :
760+ battery_draw = max (diff , - self .discharge_rate * step )
761+
762+ # Clamp battery at reserve
763+ if battery_draw > 0 :
764+ soc -= battery_draw / self .battery_loss_discharge
765+ if soc < self .reserve :
766+ battery_draw -= (self .reserve - soc ) * self .battery_loss_discharge
767+ soc = self .reserve
768+
769+ # Clamp battery at max
770+ if battery_draw < 0 :
771+ soc -= battery_draw * self .battery_loss
772+ if soc > self .soc_max :
773+ battery_draw += (soc - self .soc_max ) / self .battery_loss
774+ soc = self .soc_max
775+
776+ if (self .debug_enable ) and (minute % 30 == 0 ):
777+ self .log ("Time {} battery {} load {} (load {} pv {}) grid {} soc {}" .format (self .time_abs_str (minute_absolute ), battery_draw , diff , load_yesterday , pv_now , diff - battery_draw , soc ))
778+
779+ # Work out left over energy after battery adjustment
780+ diff -= battery_draw
781+
782+ if diff > 0 :
783+ # Import
839784 if record :
840- energy = self .reserve - soc
841- import_kwh += energy
842- import_kwh_house += energy
843- if minute_absolute in self .rate_import :
844- metric += self .rate_import [minute_absolute ] * energy
785+ import_kwh += diff
786+ if self .charge_enable and (charge_window_n >= 0 ):
787+ # If the battery is on charge anyhow then imports are at battery charging rate
788+ import_kwh_battery += diff
845789 else :
846- metric += self .metric_house * energy
847- soc = self . reserve
790+ # self.log("importing to minute %s amount %s kw total %s kwh total draw %s" % (minute, energy, import_kwh_house, diff))
791+ import_kwh_house += diff
848792
849- # Full battery, export over the cap
850- if soc > self .soc_max :
793+ if minute_absolute in self .rate_import :
794+ metric += self .rate_import [minute_absolute ] * diff
795+ else :
796+ if self .charge_enable and (charge_window_n >= 0 ):
797+ metric += self .metric_battery * diff
798+ else :
799+ metric += self .metric_house * diff
800+ diff = 0
801+ else :
802+ # Export
851803 if record :
852- energy = soc - self . soc_max
804+ energy = - diff
853805 export_kwh += energy
854806 if minute_absolute in self .rate_export :
855807 metric -= self .rate_export [minute_absolute ] * energy
856808 else :
857809 metric -= self .metric_export * energy
858- soc = self .soc_max
859-
860- if self .debug_enable and minute % 60 == 0 :
861- self .log ("Hour {} load_yesterday {} pv_now {} soc {}" .format (minute / 60 , load_yesterday , pv_now , soc ))
862-
810+ diff = 0
811+
863812 predict_soc [minute ] = self .dp3 (soc )
864813
865814 # Only store every 10 minutes for data-set size
@@ -1098,7 +1047,9 @@ def rate_scan_export(self, rates):
10981047
10991048 # Find charging window
11001049 self .high_export_rates = self .rate_scan_window (rates , rate_low_min_window , rate_average * rate_high_threshold , True )
1050+ return rates
11011051
1052+ def publish_rates_export (self ):
11021053 if self .high_export_rates :
11031054 window_n = 0
11041055 for window in self .high_export_rates :
@@ -1128,13 +1079,12 @@ def rate_scan_export(self, rates):
11281079 self .log ("No high export rate period found" )
11291080 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' })
11301081 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' })
1131- self .set_state ("predbat.high_rate_export_cost" , state = rate_average , attributes = {'friendly_name' : 'Next high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1082+ 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' })
11321083 if len (self .high_export_rates ) < 2 and not SIMULATE :
11331084 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' })
11341085 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' })
1135- self .set_state ("predbat.high_rate_export_cost_2" , state = rate_average , attributes = {'friendly_name' : 'Next+1 high export rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1086+ 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' })
11361087
1137- return rates
11381088
11391089 def rate_minmax (self , rates ):
11401090 """
@@ -1222,7 +1172,9 @@ def rate_scan(self, rates, octopus_slots):
12221172
12231173 # Find charging window
12241174 self .low_rates = self .rate_scan_window (rates , rate_low_min_window , rate_average * rate_low_threshold , False )
1175+ return rates
12251176
1177+ def publish_rates_import (self ):
12261178 # Output rate info
12271179 if self .low_rates :
12281180 window_n = 0
@@ -1253,13 +1205,11 @@ def rate_scan(self, rates, octopus_slots):
12531205 self .log ("No low rate period found" )
12541206 self .set_state ("predbat.low_rate_start" , state = 'undefined' , attributes = {'friendly_name' : 'Next low rate start' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
12551207 self .set_state ("predbat.low_rate_end" , state = 'undefined' , attributes = {'friendly_name' : 'Next low rate end' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1256- self .set_state ("predbat.low_rate_cost" , state = rate_average , attributes = {'friendly_name' : 'Next low rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1208+ self .set_state ("predbat.low_rate_cost" , state = self . rate_average , attributes = {'friendly_name' : 'Next low rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
12571209 if len (self .low_rates ) < 2 and not SIMULATE :
12581210 self .set_state ("predbat.low_rate_start_2" , state = 'undefined' , attributes = {'friendly_name' : 'Next+1 low rate start' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
12591211 self .set_state ("predbat.low_rate_end_2" , state = 'undefined' , attributes = {'friendly_name' : 'Next+1 low rate end' , 'device_class' : 'timestamp' , 'icon' : 'mdi:table-clock' })
1260- self .set_state ("predbat.low_rate_cost_2" , state = rate_average , attributes = {'friendly_name' : 'Next+1 low rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
1261-
1262- return rates
1212+ self .set_state ("predbat.low_rate_cost_2" , state = self .rate_average , attributes = {'friendly_name' : 'Next+1 low rate cost' , 'state_class' : 'measurement' , 'unit_of_measurement' : 'p' , 'icon' : 'mdi:currency-usd' })
12631213
12641214 def publish_rates (self , rates , export ):
12651215 """
@@ -1272,6 +1222,11 @@ def publish_rates(self, rates, export):
12721222 stamp = minute_timestamp .strftime (TIME_FORMAT )
12731223 rates_time [stamp ] = rates [minute ]
12741224
1225+ if export :
1226+ self .publish_rates_export ()
1227+ else :
1228+ self .publish_rates_import ()
1229+
12751230 if not SIMULATE :
12761231 if export :
12771232 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' })
@@ -1398,6 +1353,7 @@ def reset(self):
13981353 self .octopus_slots = []
13991354 self .reserve = 0
14001355 self .battery_loss = 1.0
1356+ self .battery_loss_discharge = 1.0
14011357 self .battery_scaling = 1.0
14021358 self .best_soc_min = 0
14031359 self .best_soc_margin = 0
@@ -1744,6 +1700,7 @@ def update_pred(self):
17441700
17451701 # Battery charging options
17461702 self .battery_loss = 1.0 - self .get_arg ('battery_loss' , 0.05 )
1703+ self .battery_loss_discharge = 1.0 - self .get_arg ('battery_loss_discharge' , 0 )
17471704 self .battery_scaling = self .get_arg ('battery_scaling' , 1.0 )
17481705 self .best_soc_margin = self .get_arg ('best_soc_margin' , 0 )
17491706 self .best_soc_min = self .get_arg ('best_soc_min' , 0.5 )
@@ -1837,7 +1794,7 @@ def update_pred(self):
18371794 self .car_charging_energy = {}
18381795 if 'car_charging_energy' in self .args :
18391796 self .car_charging_energy = self .minute_data (self .get_history (entity_id = self .get_arg ('car_charging_energy' , indirect = False ), days = self .days_previous + 1 )[0 ],
1840- self .days_previous + 1 , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True )
1797+ self .days_previous + 1 , now_utc , 'state' , 'last_updated' , backwards = True , smoothing = True , clean_increment = True , scale = self . get_arg ( 'car_charging_energy_scale' , 1.0 ) )
18411798 self .log ("Car charging hold {} with energy data" .format (self .car_charging_hold ))
18421799 else :
18431800 self .log ("Car charging hold {} threshold {}" .format (self .car_charging_hold , self .car_charging_threshold * 60.0 ))
0 commit comments