foxesscloud 2.5.3__py3-none-any.whl → 2.5.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxesscloud/foxesscloud.py +61 -59
- foxesscloud/openapi.py +61 -59
- {foxesscloud-2.5.3.dist-info → foxesscloud-2.5.5.dist-info}/METADATA +16 -6
- foxesscloud-2.5.5.dist-info/RECORD +7 -0
- foxesscloud-2.5.3.dist-info/RECORD +0 -7
- {foxesscloud-2.5.3.dist-info → foxesscloud-2.5.5.dist-info}/LICENCE +0 -0
- {foxesscloud-2.5.3.dist-info → foxesscloud-2.5.5.dist-info}/WHEEL +0 -0
- {foxesscloud-2.5.3.dist-info → foxesscloud-2.5.5.dist-info}/top_level.txt +0 -0
foxesscloud/foxesscloud.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 26 September 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2023
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "1.6.
|
13
|
+
version = "1.6.7"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -721,7 +721,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
721
721
|
output(f"success", 2)
|
722
722
|
return battery_settings
|
723
723
|
|
724
|
-
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10,
|
724
|
+
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, end_soc=100, start_soc=10):
|
725
725
|
output(f"\nConfiguring schedule",1)
|
726
726
|
charge = []
|
727
727
|
st1 = time_hours(st1)
|
@@ -730,7 +730,7 @@ def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, target_so
|
|
730
730
|
en2 = time_hours(en2)
|
731
731
|
span = None
|
732
732
|
if st2 is not None and en2 is not None and st2 != en2:
|
733
|
-
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc':
|
733
|
+
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc': end_soc})
|
734
734
|
span = {'start': st2, 'end': en2}
|
735
735
|
if st1 is not None and en1 is not None and st1 != en1:
|
736
736
|
charge.append({'start': st1, 'end': en1, 'mode': 'SelfUse', 'min_soc': start_soc})
|
@@ -2221,6 +2221,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2221
2221
|
if use.get('agile') is not None and use['agile'].get('strategy') is not None:
|
2222
2222
|
base_time_adjust = hours_difference(base_time, use['agile'].get('base_time') )
|
2223
2223
|
for s in use['agile']['strategy']:
|
2224
|
+
s['valid_for'] = [int((s['hour'] - base_time_adjust) * steps_per_hour + i) for i in range(0, steps_per_hour // 2)] if s.get('hour') is not None else None
|
2224
2225
|
strategy.append(s)
|
2225
2226
|
if strategy is None or len(strategy) == 0:
|
2226
2227
|
return []
|
@@ -2230,7 +2231,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2230
2231
|
start = s['start']
|
2231
2232
|
end = s['end']
|
2232
2233
|
if hour_overlap(s, remove):
|
2233
|
-
output(f" {hours_time(start)}-{hours_time(end)}
|
2234
|
+
output(f" {hours_time(start)}-{hours_time(end)} was removed from strategy", 2)
|
2234
2235
|
continue
|
2235
2236
|
# add segment
|
2236
2237
|
min_soc_now = s['min_soc'] if s.get('min_soc') is not None and s['min_soc'] > 10 else 10
|
@@ -2239,9 +2240,9 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2239
2240
|
fdsoc = s.get('fdsoc')
|
2240
2241
|
fdpwr = s.get('fdpwr')
|
2241
2242
|
price = s.get('price')
|
2242
|
-
|
2243
|
+
valid_for = s.get('valid_for')
|
2243
2244
|
segment = {'start': start, 'end': end, 'mode': mode, 'min_soc': min_soc_now, 'max_soc': max_soc,
|
2244
|
-
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, '
|
2245
|
+
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, 'valid_for': valid_for}
|
2245
2246
|
if quiet == 0:
|
2246
2247
|
s = f" {hours_time(start)}-{hours_time(end)} {mode}, min_soc {min_soc_now}%"
|
2247
2248
|
s += f", max_soc {max_soc}%" if max_soc is not None else ""
|
@@ -2264,11 +2265,6 @@ regions = {'A':'Eastern England', 'B':'East Midlands', 'C':'London', 'D':'Mersey
|
|
2264
2265
|
'J':'South Eastern England', 'K':'Southern Wales', 'L':'South Western England', 'M':'Yorkshire', 'N':'Southern Scotland', 'P':'Northern Scotland'}
|
2265
2266
|
|
2266
2267
|
|
2267
|
-
# preset weightings for average 30 minute pricing over charging duration:
|
2268
|
-
front_loaded = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5] # 3 hour average, front loaded
|
2269
|
-
first_hour = [1.0, 1.0] # lowest average price for first hour
|
2270
|
-
|
2271
|
-
|
2272
2268
|
tariff_config = {
|
2273
2269
|
'product': "AGILE-24-04-03", # product code to use for Octopus API
|
2274
2270
|
'region': "H", # region code to use for Octopus API
|
@@ -2277,7 +2273,7 @@ tariff_config = {
|
|
2277
2273
|
'plunge_price': [1, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2278
2274
|
'plunge_slots': 6, # number of 30 minute slots to use
|
2279
2275
|
'data_wrap': 6, # prices to show per line
|
2280
|
-
'show_data':
|
2276
|
+
'show_data': 1, # show pricing data
|
2281
2277
|
'show_plot': 1 # plot pricing data
|
2282
2278
|
}
|
2283
2279
|
|
@@ -2322,21 +2318,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2322
2318
|
# extract times and prices. Times are Zulu (UTC)
|
2323
2319
|
prices = [] # ordered list of 30 minute prices
|
2324
2320
|
for i in range(0, len(results)):
|
2325
|
-
|
2321
|
+
hour = i / 2
|
2322
|
+
start = (now.hour + hour) % 24
|
2326
2323
|
time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
|
2327
2324
|
prices.append({
|
2328
2325
|
'start': start,
|
2329
2326
|
'end': round_time(start + 0.5),
|
2330
2327
|
'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
|
2331
2328
|
'price': results[i]['value_inc_vat'],
|
2332
|
-
'
|
2329
|
+
'hour': hour})
|
2333
2330
|
tariff['agile']['base_time'] = period_from.replace('T', ' ')
|
2334
2331
|
tariff['agile']['prices'] = prices
|
2335
2332
|
plunge = []
|
2336
2333
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2337
2334
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2338
2335
|
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
|
2339
|
-
for i in range(0,
|
2336
|
+
for i in range(0, len(prices)):
|
2340
2337
|
# hour relative index into list of plunge prices, starting at 7am
|
2341
2338
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
2342
2339
|
if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
|
@@ -2361,13 +2358,13 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2361
2358
|
# show the results
|
2362
2359
|
if tariff_config['show_data'] > 0:
|
2363
2360
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2364
|
-
|
2365
|
-
s = f"\
|
2361
|
+
col = (now.hour * 2) % data_wrap
|
2362
|
+
s = f"\nPrice p/kWh inc VAT on {today}:"
|
2366
2363
|
for i in range(0, len(prices)):
|
2367
|
-
s += "\n" if i
|
2368
|
-
s += f" {prices[i]['
|
2369
|
-
|
2370
|
-
output(s
|
2364
|
+
s += (f"\n {prices[i]['time']} " + " " * col * 6) if i == 0 or col == 0 else ""
|
2365
|
+
s += f" {prices[i]['price']:4.1f}"
|
2366
|
+
col = (col + 1) % data_wrap
|
2367
|
+
output(s)
|
2371
2368
|
if tariff_config['show_plot'] > 0:
|
2372
2369
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2373
2370
|
x_timed = [i for i in range(0, len(prices))]
|
@@ -2394,7 +2391,8 @@ def get_best_charge_period(start, duration):
|
|
2394
2391
|
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2395
2392
|
key = key[0] if len(key) > 0 else None
|
2396
2393
|
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2397
|
-
span = int(duration * 2 + 0.99)
|
2394
|
+
span = int(duration * 2 + 0.99) # number of slots needed for charging
|
2395
|
+
last = (duration * 2) % 1 # amount of last slot used for charging
|
2398
2396
|
coverage = max([round_time(end - start), duration])
|
2399
2397
|
period = {'start': start, 'end': round_time(start + coverage)}
|
2400
2398
|
prices = tariff['agile']['prices']
|
@@ -2403,13 +2401,14 @@ def get_best_charge_period(start, duration):
|
|
2403
2401
|
return None
|
2404
2402
|
elif len(slots) == 1:
|
2405
2403
|
best = slots
|
2406
|
-
price = prices[slots[0]]['price']
|
2407
2404
|
best_start = start
|
2405
|
+
price = prices[best[0]]['price']
|
2408
2406
|
else:
|
2409
2407
|
# best charge time for duration
|
2410
2408
|
weighting = tariff_config.get('weighting')
|
2411
2409
|
times = []
|
2412
|
-
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2410
|
+
weights = ([1.0] * (span)) if weighting is None else (weighting + [1.0] * span)[:span]
|
2411
|
+
weights[-1] *= last if last > 0.0 else 1.0
|
2413
2412
|
best = None
|
2414
2413
|
price = None
|
2415
2414
|
for i in range(0, len(slots) - span + 1):
|
@@ -2594,7 +2593,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2594
2593
|
if strategy is not None:
|
2595
2594
|
period['mode'] = 'SelfUse'
|
2596
2595
|
for d in strategy:
|
2597
|
-
if hour_in(h, d) and (d.get('
|
2596
|
+
if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
|
2598
2597
|
mode = d['mode']
|
2599
2598
|
period['mode'] = mode
|
2600
2599
|
min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
|
@@ -2964,7 +2963,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2964
2963
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2965
2964
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2966
2965
|
solcast_from = time_hours(fsolcast.daily[today]['from']) if fsolcast.daily[today].get('from') is not None else 0
|
2967
|
-
output(f"\nSolcast
|
2966
|
+
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh") # get forecast.solar data and produce time line
|
2968
2967
|
solar_value = None
|
2969
2968
|
solar_profile = None
|
2970
2969
|
if forecast is None and solar_arrays is not None and (system_time.hour in forecast_times or run_after == 0):
|
@@ -2972,7 +2971,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2972
2971
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2973
2972
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2974
2973
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2975
|
-
output(f"\nSolar
|
2974
|
+
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
2976
2975
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2977
2976
|
output(f"\nNo forecasts available at this time")
|
2978
2977
|
# get generation data
|
@@ -3061,19 +3060,18 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3061
3060
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
3062
3061
|
start_soc = int(start_residual / capacity * 100 + 0.5)
|
3063
3062
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
3064
|
-
target_soc = charge_config
|
3065
|
-
target_kwh = target_soc / 100 * capacity if target_soc is not None else 0
|
3063
|
+
target_soc = charge_config.get('target_soc')
|
3064
|
+
target_kwh = capacity if full_charge is not None or force_charge == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
3066
3065
|
if target_kwh > (end_residual + kwh_needed):
|
3067
3066
|
kwh_needed = target_kwh - end_residual
|
3068
|
-
elif full_charge is not None or force_charge == 2:
|
3069
|
-
kwh_needed = capacity - start_residual
|
3070
3067
|
elif test_charge is not None:
|
3071
3068
|
output(f"\nTest charge of {test_charge}kWh")
|
3072
3069
|
kwh_needed = test_charge
|
3073
3070
|
charge_message = "** test charge **"
|
3074
3071
|
# work out charge needed
|
3075
|
-
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh']:
|
3076
|
-
output(f"\nNo charging needed
|
3072
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and test_charge is None:
|
3073
|
+
output(f"\nNo charging needed:")
|
3074
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3077
3075
|
charge_message = "no charge needed"
|
3078
3076
|
kwh_needed = 0.0
|
3079
3077
|
hours = 0.0
|
@@ -3086,26 +3084,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3086
3084
|
work_mode_timed[t]['min_soc'] = start_soc
|
3087
3085
|
else:
|
3088
3086
|
if test_charge is None:
|
3089
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh
|
3087
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh:")
|
3090
3088
|
charge_message = "with charge added"
|
3091
|
-
output(f"
|
3089
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3092
3090
|
# work out time to add kwh_needed to battery
|
3093
|
-
|
3094
|
-
hours = round_time(kwh_needed /
|
3095
|
-
# charge time exceeded or charge needed exceeds capacity
|
3096
|
-
|
3097
|
-
|
3098
|
-
hours = charge_time
|
3099
|
-
elif hours < charge_config['min_hours']:
|
3091
|
+
charge_rate = charge_power * charge_loss
|
3092
|
+
hours = round_time(kwh_needed / charge_rate)
|
3093
|
+
# check if charge time exceeded or charge needed exceeds capacity
|
3094
|
+
hours_to_full = round_time((capacity - start_residual) / (charge_rate) + 10)
|
3095
|
+
if hours < charge_config['min_hours']:
|
3100
3096
|
hours = charge_config['min_hours']
|
3101
|
-
|
3097
|
+
elif hours > charge_time:
|
3098
|
+
hours = charge_time
|
3099
|
+
elif hours > hours_to_full:
|
3100
|
+
kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
|
3101
|
+
required = hours_to_full + charge_time * kwh_shortfall / (end_residual - start_residual) # time to recover energy not added
|
3102
|
+
hours = required if required < charge_time else charge_time
|
3102
3103
|
# rework charge and discharge
|
3103
3104
|
charge_period = get_best_charge_period(start_at, hours)
|
3104
3105
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
3105
3106
|
price = charge_period.get('price') if charge_period is not None else None
|
3106
3107
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3107
3108
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
3108
|
-
|
3109
|
+
start_residual = interpolate(start_timed, bat_timed)
|
3110
|
+
end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
|
3111
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3112
|
+
output(f" Charge to: {end_soc:.0f} {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}" + (f" at {price:5.2f}p/kWh" if price is not None else ""))
|
3109
3113
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
3110
3114
|
j = i + 1
|
3111
3115
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3136,17 +3140,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3136
3140
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3137
3141
|
if show_data > 0:
|
3138
3142
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3139
|
-
s = f"\nBattery Energy kWh
|
3143
|
+
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3140
3144
|
h = base_hour + 1
|
3141
3145
|
t = steps_per_hour
|
3142
|
-
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3143
3146
|
while t < len(time_line) and bat_timed[t] is not None:
|
3144
|
-
|
3145
|
-
s += f" {hours_time(time_line[t])}"
|
3146
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f"
|
3147
|
+
col = h % data_wrap
|
3148
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
|
3149
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3147
3150
|
h += 1
|
3148
3151
|
t += steps_per_hour
|
3149
|
-
output(s
|
3152
|
+
output(s)
|
3150
3153
|
if show_plot > 0:
|
3151
3154
|
print()
|
3152
3155
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3198,7 +3201,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3198
3201
|
end1 = start1 if force_charge == 0 else start2
|
3199
3202
|
end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
|
3200
3203
|
if timed_mode > 1:
|
3201
|
-
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc,
|
3204
|
+
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
|
3202
3205
|
set_schedule(periods = periods)
|
3203
3206
|
else:
|
3204
3207
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
|
@@ -3274,17 +3277,16 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3274
3277
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3275
3278
|
if show_data > 0 and plots.get('SoC') is not None:
|
3276
3279
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3277
|
-
s = f"\nBattery Energy kWh
|
3280
|
+
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3278
3281
|
h = base_hour + 1
|
3279
3282
|
t = steps_per_hour
|
3280
|
-
|
3281
|
-
|
3282
|
-
s += "\n" if t
|
3283
|
-
s += f" {
|
3284
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3283
|
+
while t < len(time_line) and bat_timed[t] is not None:
|
3284
|
+
col = h % data_wrap
|
3285
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
|
3286
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3285
3287
|
h += 1
|
3286
3288
|
t += steps_per_hour
|
3287
|
-
print(s
|
3289
|
+
print(s)
|
3288
3290
|
if show_plot > 0:
|
3289
3291
|
print()
|
3290
3292
|
plt.figure(figsize=(figure_width, figure_width/2))
|
foxesscloud/openapi.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 26 September 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2024
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "2.5.
|
13
|
+
version = "2.5.5"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -669,7 +669,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
669
669
|
output(f"success", 2)
|
670
670
|
return battery_settings
|
671
671
|
|
672
|
-
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10,
|
672
|
+
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, end_soc=100, start_soc=10):
|
673
673
|
output(f"\nConfiguring schedule",1)
|
674
674
|
charge = []
|
675
675
|
st1 = time_hours(st1)
|
@@ -678,7 +678,7 @@ def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, target_so
|
|
678
678
|
en2 = time_hours(en2)
|
679
679
|
span = None
|
680
680
|
if st2 is not None and en2 is not None and st2 != en2:
|
681
|
-
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc':
|
681
|
+
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc': end_soc})
|
682
682
|
span = {'start': st2, 'end': en2}
|
683
683
|
if st1 is not None and en1 is not None and st1 != en1:
|
684
684
|
charge.append({'start': st1, 'end': en1, 'mode': 'SelfUse', 'min_soc': start_soc})
|
@@ -2085,6 +2085,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2085
2085
|
if use.get('agile') is not None and use['agile'].get('strategy') is not None:
|
2086
2086
|
base_time_adjust = hours_difference(base_time, use['agile'].get('base_time') )
|
2087
2087
|
for s in use['agile']['strategy']:
|
2088
|
+
s['valid_for'] = [int((s['hour'] - base_time_adjust) * steps_per_hour + i) for i in range(0, steps_per_hour // 2)] if s.get('hour') is not None else None
|
2088
2089
|
strategy.append(s)
|
2089
2090
|
if strategy is None or len(strategy) == 0:
|
2090
2091
|
return []
|
@@ -2094,7 +2095,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2094
2095
|
start = s['start']
|
2095
2096
|
end = s['end']
|
2096
2097
|
if hour_overlap(s, remove):
|
2097
|
-
output(f" {hours_time(start)}-{hours_time(end)}
|
2098
|
+
output(f" {hours_time(start)}-{hours_time(end)} was removed from strategy", 2)
|
2098
2099
|
continue
|
2099
2100
|
# add segment
|
2100
2101
|
min_soc_now = s['min_soc'] if s.get('min_soc') is not None and s['min_soc'] > 10 else 10
|
@@ -2103,9 +2104,9 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2103
2104
|
fdsoc = s.get('fdsoc')
|
2104
2105
|
fdpwr = s.get('fdpwr')
|
2105
2106
|
price = s.get('price')
|
2106
|
-
|
2107
|
+
valid_for = s.get('valid_for')
|
2107
2108
|
segment = {'start': start, 'end': end, 'mode': mode, 'min_soc': min_soc_now, 'max_soc': max_soc,
|
2108
|
-
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, '
|
2109
|
+
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, 'valid_for': valid_for}
|
2109
2110
|
if quiet == 0:
|
2110
2111
|
s = f" {hours_time(start)}-{hours_time(end)} {mode}, min_soc {min_soc_now}%"
|
2111
2112
|
s += f", max_soc {max_soc}%" if max_soc is not None else ""
|
@@ -2128,11 +2129,6 @@ regions = {'A':'Eastern England', 'B':'East Midlands', 'C':'London', 'D':'Mersey
|
|
2128
2129
|
'J':'South Eastern England', 'K':'Southern Wales', 'L':'South Western England', 'M':'Yorkshire', 'N':'Southern Scotland', 'P':'Northern Scotland'}
|
2129
2130
|
|
2130
2131
|
|
2131
|
-
# preset weightings for average 30 minute pricing over charging duration:
|
2132
|
-
front_loaded = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5] # 3 hour average, front loaded
|
2133
|
-
first_hour = [1.0, 1.0] # lowest average price for first hour
|
2134
|
-
|
2135
|
-
|
2136
2132
|
tariff_config = {
|
2137
2133
|
'product': "AGILE-24-04-03", # product code to use for Octopus API
|
2138
2134
|
'region': "H", # region code to use for Octopus API
|
@@ -2141,7 +2137,7 @@ tariff_config = {
|
|
2141
2137
|
'plunge_price': [1, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2142
2138
|
'plunge_slots': 6, # number of 30 minute slots to use
|
2143
2139
|
'data_wrap': 6, # prices to show per line
|
2144
|
-
'show_data':
|
2140
|
+
'show_data': 1, # show pricing data
|
2145
2141
|
'show_plot': 1 # plot pricing data
|
2146
2142
|
}
|
2147
2143
|
|
@@ -2186,21 +2182,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2186
2182
|
# extract times and prices. Times are Zulu (UTC)
|
2187
2183
|
prices = [] # ordered list of 30 minute prices
|
2188
2184
|
for i in range(0, len(results)):
|
2189
|
-
|
2185
|
+
hour = i / 2
|
2186
|
+
start = (now.hour + hour) % 24
|
2190
2187
|
time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
|
2191
2188
|
prices.append({
|
2192
2189
|
'start': start,
|
2193
2190
|
'end': round_time(start + 0.5),
|
2194
2191
|
'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
|
2195
2192
|
'price': results[i]['value_inc_vat'],
|
2196
|
-
'
|
2193
|
+
'hour': hour})
|
2197
2194
|
tariff['agile']['base_time'] = period_from.replace('T', ' ')
|
2198
2195
|
tariff['agile']['prices'] = prices
|
2199
2196
|
plunge = []
|
2200
2197
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2201
2198
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2202
2199
|
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
|
2203
|
-
for i in range(0,
|
2200
|
+
for i in range(0, len(prices)):
|
2204
2201
|
# hour relative index into list of plunge prices, starting at 7am
|
2205
2202
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
2206
2203
|
if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
|
@@ -2225,13 +2222,13 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2225
2222
|
# show the results
|
2226
2223
|
if tariff_config['show_data'] > 0:
|
2227
2224
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2228
|
-
|
2229
|
-
s = f"\
|
2225
|
+
col = (now.hour * 2) % data_wrap
|
2226
|
+
s = f"\nPrice p/kWh inc VAT on {today}:"
|
2230
2227
|
for i in range(0, len(prices)):
|
2231
|
-
s += "\n" if i
|
2232
|
-
s += f" {prices[i]['
|
2233
|
-
|
2234
|
-
output(s
|
2228
|
+
s += (f"\n {prices[i]['time']} " + " " * col * 6) if i == 0 or col == 0 else ""
|
2229
|
+
s += f" {prices[i]['price']:4.1f}"
|
2230
|
+
col = (col + 1) % data_wrap
|
2231
|
+
output(s)
|
2235
2232
|
if tariff_config['show_plot'] > 0:
|
2236
2233
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2237
2234
|
x_timed = [i for i in range(0, len(prices))]
|
@@ -2258,7 +2255,8 @@ def get_best_charge_period(start, duration):
|
|
2258
2255
|
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2259
2256
|
key = key[0] if len(key) > 0 else None
|
2260
2257
|
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2261
|
-
span = int(duration * 2 + 0.99)
|
2258
|
+
span = int(duration * 2 + 0.99) # number of slots needed for charging
|
2259
|
+
last = (duration * 2) % 1 # amount of last slot used for charging
|
2262
2260
|
coverage = max([round_time(end - start), duration])
|
2263
2261
|
period = {'start': start, 'end': round_time(start + coverage)}
|
2264
2262
|
prices = tariff['agile']['prices']
|
@@ -2267,13 +2265,14 @@ def get_best_charge_period(start, duration):
|
|
2267
2265
|
return None
|
2268
2266
|
elif len(slots) == 1:
|
2269
2267
|
best = slots
|
2270
|
-
price = prices[slots[0]]['price']
|
2271
2268
|
best_start = start
|
2269
|
+
price = prices[best[0]]['price']
|
2272
2270
|
else:
|
2273
2271
|
# best charge time for duration
|
2274
2272
|
weighting = tariff_config.get('weighting')
|
2275
2273
|
times = []
|
2276
|
-
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2274
|
+
weights = ([1.0] * (span)) if weighting is None else (weighting + [1.0] * span)[:span]
|
2275
|
+
weights[-1] *= last if last > 0.0 else 1.0
|
2277
2276
|
best = None
|
2278
2277
|
price = None
|
2279
2278
|
for i in range(0, len(slots) - span + 1):
|
@@ -2458,7 +2457,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2458
2457
|
if strategy is not None:
|
2459
2458
|
period['mode'] = 'SelfUse'
|
2460
2459
|
for d in strategy:
|
2461
|
-
if hour_in(h, d) and (d.get('
|
2460
|
+
if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
|
2462
2461
|
mode = d['mode']
|
2463
2462
|
period['mode'] = mode
|
2464
2463
|
min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
|
@@ -2827,7 +2826,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2827
2826
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2828
2827
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2829
2828
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2830
|
-
output(f"\nSolcast
|
2829
|
+
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
|
2831
2830
|
# get forecast.solar data and produce time line
|
2832
2831
|
solar_value = None
|
2833
2832
|
solar_profile = None
|
@@ -2836,7 +2835,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2836
2835
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2837
2836
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2838
2837
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2839
|
-
output(f"\nSolar
|
2838
|
+
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
2840
2839
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2841
2840
|
output(f"\nNo forecasts available at this time")
|
2842
2841
|
# get generation data
|
@@ -2925,19 +2924,18 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2925
2924
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
2926
2925
|
start_soc = int(start_residual / capacity * 100 + 0.5)
|
2927
2926
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
2928
|
-
target_soc = charge_config
|
2929
|
-
target_kwh = target_soc / 100 * capacity if target_soc is not None else 0
|
2927
|
+
target_soc = charge_config.get('target_soc')
|
2928
|
+
target_kwh = capacity if full_charge is not None or force_charge == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
2930
2929
|
if target_kwh > (end_residual + kwh_needed):
|
2931
2930
|
kwh_needed = target_kwh - end_residual
|
2932
|
-
elif full_charge is not None or force_charge == 2:
|
2933
|
-
kwh_needed = capacity - start_residual
|
2934
2931
|
elif test_charge is not None:
|
2935
2932
|
output(f"\nTest charge of {test_charge}kWh")
|
2936
2933
|
kwh_needed = test_charge
|
2937
2934
|
charge_message = "** test charge **"
|
2938
2935
|
# work out charge needed
|
2939
|
-
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and
|
2940
|
-
output(f"\nNo charging needed
|
2936
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and test_charge is None:
|
2937
|
+
output(f"\nNo charging needed:")
|
2938
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2941
2939
|
charge_message = "no charge needed"
|
2942
2940
|
kwh_needed = 0.0
|
2943
2941
|
hours = 0.0
|
@@ -2950,26 +2948,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2950
2948
|
work_mode_timed[t]['min_soc'] = start_soc
|
2951
2949
|
else:
|
2952
2950
|
if test_charge is None:
|
2953
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh
|
2951
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh:")
|
2954
2952
|
charge_message = "with charge added"
|
2955
|
-
output(f"
|
2953
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2956
2954
|
# work out time to add kwh_needed to battery
|
2957
|
-
|
2958
|
-
hours = round_time(kwh_needed /
|
2959
|
-
# charge time exceeded or charge needed exceeds capacity
|
2960
|
-
|
2961
|
-
|
2962
|
-
hours = charge_time
|
2963
|
-
elif hours < charge_config['min_hours']:
|
2955
|
+
charge_rate = charge_power * charge_loss
|
2956
|
+
hours = round_time(kwh_needed / charge_rate)
|
2957
|
+
# check if charge time exceeded or charge needed exceeds capacity
|
2958
|
+
hours_to_full = round_time((capacity - start_residual) / (charge_rate) + 10)
|
2959
|
+
if hours < charge_config['min_hours']:
|
2964
2960
|
hours = charge_config['min_hours']
|
2965
|
-
|
2961
|
+
elif hours > charge_time:
|
2962
|
+
hours = charge_time
|
2963
|
+
elif hours > hours_to_full:
|
2964
|
+
kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
|
2965
|
+
required = hours_to_full + charge_time * kwh_shortfall / (end_residual - start_residual) # time to recover energy not added
|
2966
|
+
hours = required if required < charge_time else charge_time
|
2966
2967
|
# rework charge and discharge
|
2967
2968
|
charge_period = get_best_charge_period(start_at, hours)
|
2968
2969
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
2969
2970
|
price = charge_period.get('price') if charge_period is not None else None
|
2970
2971
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
2971
2972
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
2972
|
-
|
2973
|
+
start_residual = interpolate(start_timed, bat_timed)
|
2974
|
+
end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
|
2975
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
2976
|
+
output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}" + (f" at {price:5.2f}p/kWh" if price is not None else ""))
|
2973
2977
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
2974
2978
|
j = i + 1
|
2975
2979
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3000,17 +3004,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3000
3004
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3001
3005
|
if show_data > 0:
|
3002
3006
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3003
|
-
s = f"\nBattery Energy kWh
|
3007
|
+
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3004
3008
|
h = base_hour + 1
|
3005
3009
|
t = steps_per_hour
|
3006
|
-
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3007
3010
|
while t < len(time_line) and bat_timed[t] is not None:
|
3008
|
-
|
3009
|
-
s += f" {hours_time(time_line[t])}"
|
3010
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f"
|
3011
|
+
col = h % data_wrap
|
3012
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
|
3013
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3011
3014
|
h += 1
|
3012
3015
|
t += steps_per_hour
|
3013
|
-
output(s
|
3016
|
+
output(s)
|
3014
3017
|
if show_plot > 0:
|
3015
3018
|
print()
|
3016
3019
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3062,7 +3065,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3062
3065
|
end1 = start1 if force_charge == 0 else start2
|
3063
3066
|
end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
|
3064
3067
|
if timed_mode > 1:
|
3065
|
-
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc,
|
3068
|
+
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
|
3066
3069
|
set_schedule(periods = periods)
|
3067
3070
|
else:
|
3068
3071
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
|
@@ -3137,17 +3140,16 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3137
3140
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3138
3141
|
if show_data > 0 and plots.get('SoC') is not None:
|
3139
3142
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3140
|
-
s = f"\nBattery Energy kWh
|
3143
|
+
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3141
3144
|
h = base_hour + 1
|
3142
3145
|
t = steps_per_hour
|
3143
|
-
|
3144
|
-
|
3145
|
-
s += "\n" if t
|
3146
|
-
s += f" {
|
3147
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3146
|
+
while t < len(time_line) and bat_timed[t] is not None:
|
3147
|
+
col = h % data_wrap
|
3148
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
|
3149
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3148
3150
|
h += 1
|
3149
3151
|
t += steps_per_hour
|
3150
|
-
print(s
|
3152
|
+
print(s)
|
3151
3153
|
if show_plot > 0:
|
3152
3154
|
print()
|
3153
3155
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.5.
|
3
|
+
Version: 2.5.5
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -528,11 +528,11 @@ This gets the latest 30 minute pricing and uses this to work out the best off pe
|
|
528
528
|
+ forecast_times: a list of times when a forecast can be obtained from Solcast / forecast.solar, aligned with the host system time
|
529
529
|
+ strategy: an optional list of times and work modes (see below)
|
530
530
|
+ update: optional, 1 (the default) sets the current tariff to Agile Octopus. Setting to 0 does not change the current tariff
|
531
|
-
+ weighting: optional, default is None (see below)
|
531
|
+
+ weighting: optional, default is None / flat (see below)
|
532
532
|
+ time_shift: optional system time shift in hours. The default is for system time to be UTC and to apply the current day light saving time (e.g. GMT/BST)
|
533
533
|
+ plunge_price: list of prices in p/kWh when plunge pricing is used (see below). The default is [0, 5].
|
534
534
|
+ plunge_slots: the number of 30 minute slots to use for plunge pricing. The default is 6, allowing up to 3 hours.
|
535
|
-
+ show_data: show 30 minute Agile pricing data. Default is
|
535
|
+
+ show_data: show 30 minute Agile pricing data. Default is 1.
|
536
536
|
+ show_plot: plot 30 minute Agile pricing data. Default is 1.
|
537
537
|
|
538
538
|
Product codes include:
|
@@ -562,9 +562,7 @@ Region codes include:
|
|
562
562
|
Pricing for tomorrow is updated around 5pm each day. If run before this time, prices from yesterday are used. By default, prices for tomorrow are fetched after 5pm. The setting for this is:
|
563
563
|
+ f.agile_update_time = 17
|
564
564
|
|
565
|
-
The best charging period is determined based on the weighted average of the 30 minute prices over the duration. The default is flat (all prices are weighted equally). You can
|
566
|
-
+ f.front_loaded: [1.0, 0.9, 0.8, 0.7, 0.6, 0.5]
|
567
|
-
+ f.first_hour: [1.0, 1.0]
|
565
|
+
The best charging period is determined based on the weighted average of the 30 minute prices over the duration. The default is flat (all prices are weighted equally, except the last slot, which is pro rata to the charge duration used). You can over-ride the default weighting by providing a list of 30 minute values to apply.
|
568
566
|
|
569
567
|
set_tariff() can configure multiple off-peak and peak periods for any tariff using the 'times' parameter. Times is a list of tuples:
|
570
568
|
+ containing values for key, 'start', 'end' and optional 'force'.
|
@@ -785,6 +783,18 @@ This setting can be:
|
|
785
783
|
|
786
784
|
# Version Info
|
787
785
|
|
786
|
+
2.5.5<br>
|
787
|
+
Improve validation of plunge price periods so they don't repeat across days.
|
788
|
+
Correct start and end soc times and values when charging using best Agile time periods.
|
789
|
+
Extend charge times when charge needed exceeds battery capacity.
|
790
|
+
|
791
|
+
|
792
|
+
2.5.4<br>
|
793
|
+
Remove preset 'weighting' that were not used.
|
794
|
+
Update weighting to apply the requested charge duration correctly.
|
795
|
+
Reformat price and SoC tables to reduce wrapping and make them easier to read on small screens.
|
796
|
+
Change default for set_tariff() to show Agile 30 minute prices.
|
797
|
+
|
788
798
|
2.5.3<br>
|
789
799
|
Reverted change to allow updates during a charge period to avoid removing charge in progress.
|
790
800
|
Update contingency and show how this relates to battery SoC.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=biC30gvvQHFTu3CCDhuK6KRGNi2hFbgFxHjSykj-UpE,211062
|
2
|
+
foxesscloud/openapi.py,sha256=MEzJPleAV8-2gR17e2qmQezVeotiqkHMzeCBDVJOfsY,204698
|
3
|
+
foxesscloud-2.5.5.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.5.5.dist-info/METADATA,sha256=kvZCDYUyu7v1OkE_dik_f_4V4OXQTaZKwOYWYFTircU,55462
|
5
|
+
foxesscloud-2.5.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.5.5.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.5.5.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=LkRglTcW4sAVPRgXxkhqyt7fcDcDk00ui_nnsdSU51Y,210817
|
2
|
-
foxesscloud/openapi.py,sha256=2hyHrGpYNUQybmKCLRZmujW8CfnyS0bjKdxZwPpkjCU,204531
|
3
|
-
foxesscloud-2.5.3.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.5.3.dist-info/METADATA,sha256=iXU6pABWNwPSUXW4-NrJWCiy3ZecYsiJPIKNAamHR-g,54937
|
5
|
-
foxesscloud-2.5.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.5.3.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.5.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|