Skip to content

Commit 7c7f984

Browse files
authored
Update nsw_script.py
1 parent cd3e4c1 commit 7c7f984

1 file changed

Lines changed: 183 additions & 87 deletions

File tree

nsw_script.py

Lines changed: 183 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,196 @@
1+
min_soc = 5 # % Minimum battery SOC
2+
action = 'auto'
3+
# Pricing Decisions
4+
IMPORT_TOLERANCE = 1
5+
BATTERY_SOC_NEEDED = 9
6+
BUY_DELTA_THRESHOLD = 87
7+
CUT_OFF_THRESHOLD = 47
8+
MORNING_SELL_MARGIN = 82
9+
MORNING_SELL_SOC = 42
10+
BATTERY_EXPORT_SOC_THRESHOLD = 24
11+
GOOD_SUN_DAY = 44
12+
GOOD_SUN_HOUR = 64
13+
BAD_SUN_DAY_KEEP_SOC = 16
14+
ALWAYS_IMPORT_SOC = 22 # The battery SOC at which we always import
15+
16+
# Determine Local Time
117
hour = interval_time.hour
2-
desired_soc = 20.0
3-
min_sell_soc = 30 # 'Take the money (sell > 80c)' down to this SOC
4-
sell_price_threshold = 85 # 20 cents
5-
sell_price_threshold_1 = 20 # sell in morning
6-
sell_price_threshold_2 = 1000 # Sell during peak
7-
buy_price_morning = 5 # Morning buy price
8-
buy_top_up_price = 20
9-
# This is when we expect the solar production to meet our house load
10-
minutes_after_sunrise_solar_matches_load = 30
11-
solar_production_time = sunrise + timedelta(minutes=minutes_after_sunrise_solar_matches_load)
18+
current_hour = interval_time.hour
19+
gti = weather_data.get('hourly', {}).get('global_tilted_irradiance_instant', [-1] * 48)
20+
tomorrow_morning_hours_away = 24 - hour
21+
global_tilted_irradiance_tomorrow = sum(gti[tomorrow_morning_hours_away:])
22+
soc_diff = 0.0
23+
time_left = 0.0
24+
soc_diff_remaining = 0.0
25+
night_reserve = BATTERY_SOC_NEEDED
26+
if global_tilted_irradiance_tomorrow < (GOOD_SUN_DAY * 100):
27+
night_reserve += BAD_SUN_DAY_KEEP_SOC
28+
if forecast and hour > 16:
29+
morning_peak = max(forecast[:-6])
30+
if morning_peak > 400:
31+
night_reserve += 10
32+
elif forecast and hour < 6:
33+
morning_away = hour * 2
34+
morning_peak = max(forecast[morning_away:])
35+
if morning_peak > 400:
36+
night_reserve += 10
1237

13-
if 0 <= hour < 6:
14-
if sell_price > sell_price_threshold:
15-
action = 'export'
16-
reason = f'nsw: sell price greater than {sell_price_threshold} cents between midnight and 6am'
17-
elif buy_price < buy_price_morning and battery_soc >= desired_soc:
18-
action = 'import'
19-
reason = f'nsw: buy price less than {buy_price_morning} cents between midnight and 6am'
38+
def find_first_index(gti, threshold):
39+
for i in range(len(gti)):
40+
if gti[i] > threshold:
41+
return i
42+
return -1
43+
44+
def find_last_index(gti, threshold):
45+
for i in range(len(gti)):
46+
if gti[i] < threshold:
47+
return i
48+
return -1
49+
50+
first_good_gti = find_first_index(gti, GOOD_SUN_HOUR * 10)
51+
last_good_gti = find_last_index(gti[:24], GOOD_SUN_HOUR * 10)
52+
solar_charge_time = (interval_time.hour >= first_good_gti) and (interval_time.hour <= last_good_gti)
53+
daytime = sunrise.time() < interval_time.time() < sunset.time()
54+
if daytime:
55+
soc_diff = battery_soc - night_reserve
56+
time_left = (((sunset - interval_time).total_seconds() - 1800) % 86400) / 3600
57+
elif not daytime:
58+
night_hours = (((sunrise - sunset).total_seconds()) % 86400) / 3600
59+
time_left = (((sunrise - interval_time).total_seconds() + 1800) % 86400) / 3600
60+
soc_diff = (battery_soc - night_reserve)
61+
soc_diff_remaining = battery_soc - night_reserve * (time_left / night_hours)
62+
63+
action = 'auto'
64+
if rrp > 990:
65+
action = 'export'
66+
reason = f'RRP {rrp} is high, exporting'
67+
reason = f'Default to auto: {solar_charge_time} soc {night_reserve:.0f}%->{soc_diff:.1f}%, time left {time_left:.1f}h, battery soc {battery_soc:.1f}%'
68+
global_tilted_irradiance_today = sum(gti)
69+
if global_tilted_irradiance_today > (GOOD_SUN_DAY * 100):
70+
if interval_time.hour < 12:
71+
low_buy_price = min(buy_forecast)
72+
if rrp < 0:
73+
action = 'charge'
74+
elif action in ['auto', 'charge', 'import'] and sell_price > low_buy_price:
75+
action = 'discharge'
76+
reason += ' not charging too early'
77+
78+
if interval_time.hour < 10:
79+
if battery_soc > MORNING_SELL_SOC:
80+
low_buy_price = min(buy_forecast)
81+
if sell_price > (low_buy_price + MORNING_SELL_MARGIN):
82+
action = 'export'
83+
reason += f' lots of SOC, good sun and better buys coming {low_buy_price}c'
84+
else:
85+
reason += f' low buy not enough {low_buy_price}c'
86+
else:
87+
reason += f' low PV day {global_tilted_irradiance_today:.2f}W/m2'
88+
89+
if 14 < interval_time.hour < 16 and battery_soc < 60 and action != 'import' and buy_price < 30:
90+
if buy_forecast and buy_price > min(buy_forecast[:6]):
91+
reason += ' wait for lower buy soon'
2092
else:
21-
action = 'auto'
22-
reason = 'nsw: default to auto mode between midnight and 6am'
23-
# Stop charging/discharging between 6 AM and 1 PM
24-
if 6 <= hour < 13:
25-
if sell_price > sell_price_threshold_1:
26-
action = 'export'
27-
reason = f'nsw: sell price greater than {sell_price_threshold_1} cents between 6am and 1pm'
28-
elif battery_soc > 50 and buy_price > 0:
29-
action = 'auto'
30-
reason = 'nsw: buy price is over 0 wait for afternoon to buy'
31-
elif buy_price < buy_price_morning:
3293
action = 'import'
33-
reason = f'nsw: buy price less than {buy_price_morning} cents between 6am and 1pm'
34-
else:
94+
reason += ' panic buy SOC < 50'
95+
96+
global_tilted_irradiance_past = sum(gti[:interval_time.hour])
97+
global_tilted_irradiance_to_2pm = sum(gti[:15])
98+
reason += f" tomorrow PV {global_tilted_irradiance_tomorrow:.1f}W/m2"
99+
if 4 < interval_time.hour < 16 and buy_forecast and battery_soc:
100+
charge_fors = max(1, int(6 * battery_soc / 100))
101+
low_buy_price = round(max(sorted(buy_forecast)[:charge_fors]), 2)
102+
precent_pv_past = round(global_tilted_irradiance_past / global_tilted_irradiance_to_2pm * 100, 2)
103+
reason += f' pv {precent_pv_past}% vs {battery_soc}%'
104+
tolerant_low_price = round(low_buy_price * ((100 + IMPORT_TOLERANCE) / 100), 2)
105+
if action in ['auto', 'charge'] and (battery_soc - ALWAYS_IMPORT_SOC) < precent_pv_past:
106+
if buy_forecast and buy_price > min(buy_forecast[:6]) and battery_soc > 85:
107+
reason += ' wait for lower buy soon'
108+
elif buy_price < tolerant_low_price:
109+
action = 'import'
110+
reason += f' buy price {buy_price} is lower than {tolerant_low_price}'
111+
else:
112+
if action == 'import':
113+
action = 'auto'
114+
reason += f' wait on {action} not import {tolerant_low_price}'
115+
if action == 'import' and current_hour < 10 and global_tilted_irradiance_to_2pm > 4000:
35116
action = 'auto'
36-
reason = 'nsw: default to auto mode between 6am and 1pm'
37-
# Stop charging/discharging between 6 AM and 1 PM
38-
if 13 <= hour < 15:
39-
if sell_price > sell_price_threshold_2:
40-
action = 'export'
41-
reason = f'nsw: sell price greater than {sell_price_threshold_2} cents between 6am and 1pm'
42-
elif battery_soc < 60 and buy_price < sell_price_threshold_2:
43-
action = 'import'
44-
reason = f'nsw: buy price less than {sell_price_threshold_2} cents between 1pm and 3pm'
45-
else:
117+
reason += f' wait for more sun before importing {global_tilted_irradiance_to_2pm} to go'
118+
if action == 'export' and current_hour > 16 and battery_soc < min_soc:
119+
action = 'auto'
120+
reason += f' battery SOC < {min_soc}%'
121+
# no point exporting unless this sell price is
122+
# lower than the sorted forecasted sell prices
123+
windows_can_export = int(battery_soc / (100 - BATTERY_EXPORT_SOC_THRESHOLD))
124+
if sell_forecast and action == 'export':
125+
# Count sells above the current sell_price
126+
sorted_sell_forecast = sorted(sell_forecast)
127+
cut_off = sorted_sell_forecast[min(windows_can_export, len(sorted_sell_forecast) - 1)]
128+
max_buy_forecast = max(buy_forecast)
129+
if sell_price < (max_buy_forecast * BUY_DELTA_THRESHOLD / 100):
46130
action = 'auto'
47-
reason = 'nsw: default to auto mode between 6am and 1pm'
48-
if rrp < 0:
49-
feed_in_limitation = 0
50-
reason += f' setting feed in to {feed_in_limitation}'
51-
# Ensure 'auto' action during peak demand times from 3 PM to 9 PM, unless sell_price > 20 cents
52-
elif 15 <= hour < 21:
53-
if sell_price > sell_price_threshold and battery_soc >= desired_soc:
54-
action = 'export'
55-
reason = f'nsw: sell price greater than {sell_price_threshold} cents during peak hours'
56-
else:
131+
reason += f' sell price {sell_price} is higher than buy price {max_buy_forecast}'
132+
elif sell_price < (cut_off - CUT_OFF_THRESHOLD):
57133
action = 'auto'
58-
reason = f'nsw: hour between 3pm and 9pm or battery SOC below {desired_soc}%'
59-
# Manage battery between 9 PM and midnight
60-
if 21 <= hour < 24:
61-
if sell_price > sell_price_threshold:
62-
action = 'export'
63-
reason = f'nsw: sell price greater than {sell_price_threshold} cents between 9pm and midnight'
64-
elif buy_price < 10 and battery_soc < 30:
65-
action = 'import'
66-
reason = 'nsw: buy price less than 10 cents between 9pm and midnight'
134+
reason += f' sell price {sell_price} is lower than 3c within {cut_off}'
67135
else:
68-
action = 'auto'
69-
reason = 'nsw: default to auto mode between 9pm and midnight'
136+
reason += f' okay to export {windows_can_export}'
70137

71-
if (interval_time.hour > 15) and battery_soc > 80 and sell_price > 10:
72-
best_upcoming = max(sell_forecast)
73-
if best_upcoming < (sell_price + 5):
138+
if action == 'export' and battery_soc > night_reserve and interval_time > sunset and buy_price < 98:
139+
action = 'auto'
140+
reason += ' in night reserve mode'
141+
if buy_price > 25:
74142
action = 'export'
75-
reason = f'nsw: {best_upcoming} < sell within 5c of max'
76-
else:
77-
reason += f' best upcoming: {best_upcoming}c'
143+
solar = 'curtail'
144+
feed_in_power_limitation = 1000
145+
reason += f' aim higher during high prices {feed_in_power_limitation}'
78146

79-
if (hour < 5) and battery_soc > desired_soc and sell_price > 20:
80-
action = 'export'
81-
reason = f'nsw: pre 5am use it or lose it down to {desired_soc}%'
82-
83-
if (hour > 21 or hour < 5) and (battery_soc < desired_soc):
84-
if (buy_price < buy_top_up_price):
85-
best_upcoming = min(buy_forecast)
86-
if buy_price < (best_upcoming + 2):
87-
action = 'import'
88-
reason = f'nsw: low soc and price under top up and within 2 cents of best upcoming {best_upcoming}'
89-
else:
90-
reason += f' not within 2 cents of best {best_upcoming}'
91-
else:
92-
reason += f' waiting to top up {buy_price} < {buy_top_up_price}'
147+
if sell_forecast and buy_forecast and soc_diff_remaining > 25:
148+
high_sell_price = max(sorted(sell_forecast)[:-3]) if sell_forecast else 0
149+
if high_sell_price < (sell_price + 5):
150+
action = 'export'
151+
reason += f' export: with 5c of high_sell_price {high_sell_price}c/kWh'
152+
if sell_forecast and buy_forecast and soc_diff_remaining > 15:
153+
high_sell_price = max(sorted(sell_forecast)[:-3]) if sell_forecast else 0
154+
low_buy_price = min(buy_forecast) * 1.3
155+
if sell_price > low_buy_price:
156+
action = 'export'
157+
reason += f' export: sell price {sell_price} is higher than low buy price {low_buy_price}c/kWh'
158+
if not daytime and time_left < 2 and soc_diff_remaining > 0:
159+
high_sell_price = max(sorted(sell_forecast)[:-3]) if sell_forecast else 0
160+
if sell_price > (high_sell_price - 5):
161+
action = 'export'
162+
reason += f' export: sell price {sell_price} is higher than high sell price {high_sell_price}c/kWh'
93163

94-
if 4 < hour < 8 and interval_time < solar_production_time and battery_soc > 10 and sell_price > 15:
95-
action = 'export'
96-
reason += f'nsw: before solar production time use it or lose it {solar_production_time}'
164+
if sell_forecast and soc_diff < 5:
165+
# Now we only want to export if the RRP is high enough
166+
high_sell_price = max(sorted(sell_forecast)[:-3]) if sell_forecast else 0
167+
if sell_price < high_sell_price and action == 'export':
168+
action = 'auto'
169+
reason += f' waiting for {high_sell_price}c, battery SOC {battery_soc:.1f}%'
97170

98-
if rrp > 800 and battery_soc > min_sell_soc:
99-
action = 'export'
100-
reason += f'take the money down to {min_sell_soc}%'
171+
# Spike Hacking
172+
if forecast and action == 'export':
173+
over_count = int(np.sum(np.array(forecast) > (rrp + 2000)))
174+
is_spike = (max(forecast) - rrp) > 1000
175+
if rrp > 1000:
176+
action = 'export'
177+
feed_in_power_limitation = 20000
178+
reason += f' exporting at ${rrp}/MWH, feed in power limitation {feed_in_power_limitation}W'
179+
elif is_spike and over_count > 1:
180+
action = 'auto'
181+
reason += f' not exporting, {over_count} prices over sell price {sell_price}c'
182+
else:
183+
reason += f' exporting at ${rrp}/MWH'
184+
if battery_soc and battery_soc < 10 and action == 'export':
185+
action = 'auto'
186+
reason += ' battery SOC < 7%'
187+
# Stop always exporting if battery SOC is low
188+
always_export_rrp = 1000
189+
if soc_diff < 10:
190+
always_export_rrp = None
191+
elif battery_soc < (soc_diff + 20):
192+
always_export_rrp = 10000
193+
reason += f" increasing always_export_rrp: {always_export_rrp}"
194+
if battery_soc < night_reserve:
195+
always_export_rrp = None
196+
reason += f" remove always_export_rrp under night reserve: {night_reserve:.1f}%"

0 commit comments

Comments
 (0)