Skip to content

Commit fbd0510

Browse files
Publish min, max, average and threshold rates into rates
Publish min, max, average and threshold rates into rates Some config tidy up Fix rounding to use nearest rather than round to zero for two decimal places
1 parent b03feb3 commit fbd0510

1 file changed

Lines changed: 53 additions & 36 deletions

File tree

apps/predbat/predbat.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)