foxesscloud 2.5.1__py3-none-any.whl → 2.5.2__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 +88 -84
- foxesscloud/openapi.py +88 -84
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.2.dist-info}/METADATA +9 -3
- foxesscloud-2.5.2.dist-info/RECORD +7 -0
- foxesscloud-2.5.1.dist-info/RECORD +0 -7
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.2.dist-info}/LICENCE +0 -0
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.2.dist-info}/WHEEL +0 -0
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.2.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: 22 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.4"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -834,13 +834,15 @@ merge_settings = { # keys to add
|
|
834
834
|
'WorkMode': {'keys': {
|
835
835
|
'h115__': 'operation_mode__work_mode',
|
836
836
|
'h116__': 'operation_mode__work_mode',
|
837
|
-
'h117__': 'operation_mode__work_mode'
|
837
|
+
'h117__': 'operation_mode__work_mode',
|
838
|
+
# 'k106__': 'operation_mode__work_mode',
|
838
839
|
},
|
839
840
|
'values': ['SelfUse', 'Feedin', 'Backup']},
|
840
841
|
'BatteryVolt': {'keys': {
|
841
842
|
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
842
843
|
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
843
|
-
'h117__': ['h117__15', 'h117__16', 'h117__17']
|
844
|
+
'h117__': ['h117__15', 'h117__16', 'h117__17'],
|
845
|
+
# 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
|
844
846
|
},
|
845
847
|
'type': 'list',
|
846
848
|
'valueType': 'float',
|
@@ -849,6 +851,7 @@ merge_settings = { # keys to add
|
|
849
851
|
'h115__': 'h115__17',
|
850
852
|
'h116__': 'h116__18',
|
851
853
|
'h117__': 'h117__18',
|
854
|
+
# 'k106__': 'k106__xx',
|
852
855
|
},
|
853
856
|
'type': 'list',
|
854
857
|
'valueType': 'int',
|
@@ -1497,7 +1500,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1497
1500
|
if e > 0.0:
|
1498
1501
|
kwh += e
|
1499
1502
|
if tariff is not None:
|
1500
|
-
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3')]):
|
1503
|
+
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3'), tariff.get('off_peak4')]):
|
1501
1504
|
kwh_off += e
|
1502
1505
|
elif hour_in(h, [tariff.get('peak1'), tariff.get('peak2')]):
|
1503
1506
|
kwh_peak += e
|
@@ -2348,56 +2351,36 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2348
2351
|
strategy.append(prices[t])
|
2349
2352
|
output(f" {format_period(prices[t])} at {prices[t]['price']:.1f}p", 1)
|
2350
2353
|
tariff['agile']['strategy'] = strategy
|
2351
|
-
for key in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2354
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2352
2355
|
if tariff.get(key) is None:
|
2353
2356
|
continue
|
2354
2357
|
if tariff['agile'].get(key) is None:
|
2355
2358
|
tariff['agile'][key] = {}
|
2356
2359
|
# get price index for AM/PM charge times
|
2357
|
-
slots = []
|
2358
|
-
for i in range(0, len(prices)):
|
2359
|
-
if hour_in(time_hours(prices[i]['start']), tariff[key]):
|
2360
|
-
slots.append(i)
|
2360
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), tariff[key])]
|
2361
2361
|
tariff['agile'][key]['slots'] = slots
|
2362
|
-
|
2363
|
-
weighting = tariff_config.get('weighting')
|
2364
|
-
tariff['agile'][key]['times'] = []
|
2365
|
-
for j in range (0, len(slots)):
|
2366
|
-
span = j + 1
|
2367
|
-
weights = (([1.0] * (span-1) if weighting is None else weighting) + [0.5] * span)[:span]
|
2368
|
-
best = None
|
2369
|
-
price = None
|
2370
|
-
for i in range(0, len(slots) - j):
|
2371
|
-
t = slots[i: i + span]
|
2372
|
-
p_span = [prices[x]['price'] for x in t]
|
2373
|
-
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2374
|
-
if price is None or wavg < price:
|
2375
|
-
price = wavg
|
2376
|
-
best = t
|
2377
|
-
# save best time slot for charge duration
|
2378
|
-
start = prices[best[0]]['start']
|
2379
|
-
tariff['agile'][key]['times'].append({'start': start, 'end': round_time(start + span / 2), 'price': price, 'best': best, 'key': key})
|
2362
|
+
tariff['agile'][key]['avg'] = avg([prices[t]['price'] for t in slots])
|
2380
2363
|
# show the results
|
2381
2364
|
if tariff_config['show_data'] > 0:
|
2382
2365
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2383
2366
|
t = (now.hour * 2) % data_wrap
|
2384
|
-
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t *
|
2367
|
+
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
|
2385
2368
|
for i in range(0, len(prices)):
|
2386
2369
|
s += "\n" if i > 0 and t % data_wrap == 0 else ""
|
2387
|
-
s += f" {prices[i]['time']} {prices[i]['price']:4.1f}"
|
2370
|
+
s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
|
2388
2371
|
t += 1
|
2389
|
-
output(s)
|
2372
|
+
output(s[:-1])
|
2390
2373
|
if tariff_config['show_plot'] > 0:
|
2391
2374
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2392
2375
|
x_timed = [i for i in range(0, len(prices))]
|
2393
2376
|
plt.xticks(ticks=x_timed, labels=[prices[x]['time'] for x in x_timed], rotation=90, fontsize=8, ha='center')
|
2394
2377
|
plt.plot(x_timed, [prices[x]['price'] for x in x_timed], label='30 minute price', color='blue')
|
2395
2378
|
s = ""
|
2396
|
-
for key in ['off_peak1', 'off_peak2']:
|
2397
|
-
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['
|
2398
|
-
p = tariff['agile'][key]
|
2399
|
-
plt.plot(x_timed, [p['
|
2400
|
-
s += f"\n {
|
2379
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2380
|
+
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['slots']) > 0:
|
2381
|
+
p = tariff['agile'][key]
|
2382
|
+
plt.plot(x_timed, [p['avg'] if x in p['slots'] else None for x in x_timed], label=f"{key} {p['avg']:.1f}p")
|
2383
|
+
s += f"\n {hours_time(prices[p['slots'][0]]['start'])}-{hours_time(prices[p['slots'][-1]]['end'])} at {p['avg']:.1f}p"
|
2401
2384
|
output(f"\nCharge times{s}" if s != "" else "", 1)
|
2402
2385
|
plt.title(f"Pricing on {today} p/kWh inc VAT", fontsize=10)
|
2403
2386
|
plt.legend(fontsize=8)
|
@@ -2408,13 +2391,40 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2408
2391
|
# return the best charge time:
|
2409
2392
|
def get_best_charge_period(start, duration):
|
2410
2393
|
global tariff
|
2411
|
-
if tariff is None:
|
2412
|
-
return None
|
2413
|
-
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3'] if hour_in(start, tariff.get(k))]
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2394
|
+
if tariff is None or tariff.get('agile') is None or tariff['agile'].get('prices') is None:
|
2395
|
+
return None
|
2396
|
+
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2397
|
+
key = key[0] if len(key) > 0 else None
|
2398
|
+
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2399
|
+
span = int(duration * 2 + 0.99)
|
2400
|
+
coverage = max([round_time(end - start), duration])
|
2401
|
+
period = {'start': start, 'end': round_time(start + coverage)}
|
2402
|
+
prices = tariff['agile']['prices']
|
2403
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), period)]
|
2404
|
+
if len(slots) == 0:
|
2405
|
+
return None
|
2406
|
+
elif len(slots) == 1:
|
2407
|
+
best = slots
|
2408
|
+
price = prices[slots[0]]['price']
|
2409
|
+
best_start = start
|
2410
|
+
else:
|
2411
|
+
# best charge time for duration
|
2412
|
+
weighting = tariff_config.get('weighting')
|
2413
|
+
times = []
|
2414
|
+
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2415
|
+
best = None
|
2416
|
+
price = None
|
2417
|
+
for i in range(0, len(slots) - span + 1):
|
2418
|
+
t = slots[i: i + span]
|
2419
|
+
p_span = [prices[x]['price'] for x in t]
|
2420
|
+
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2421
|
+
if price is None or wavg < price:
|
2422
|
+
price = wavg
|
2423
|
+
best = t
|
2424
|
+
best_start = prices[best[0]]['start']
|
2425
|
+
# save best time slot for charge duration
|
2426
|
+
tariff['agile']['best'] = {'start': best_start, 'end': round_time(best_start + span / 2), 'price': price, 'slots': best, 'key': key}
|
2427
|
+
return tariff['agile']['best']
|
2418
2428
|
|
2419
2429
|
# pushover app key for set_tariff()
|
2420
2430
|
set_tariff_app_key = "apx24dswzinhrbeb62sdensvt42aqe"
|
@@ -2458,7 +2468,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2458
2468
|
times = [times]
|
2459
2469
|
output(f"\n{use['name']}:")
|
2460
2470
|
for t in times:
|
2461
|
-
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'peak1', 'peak2']:
|
2471
|
+
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4', 'peak1', 'peak2']:
|
2462
2472
|
output(f"** set_tariff(): invalid time period {t}")
|
2463
2473
|
continue
|
2464
2474
|
key = t[0]
|
@@ -2497,7 +2507,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2497
2507
|
elif type(strategy) is not list:
|
2498
2508
|
strategy = [strategy]
|
2499
2509
|
output(f"\nStrategy")
|
2500
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3')])
|
2510
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2501
2511
|
output_close(plot=tariff_config['show_plot'])
|
2502
2512
|
if update == 1:
|
2503
2513
|
tariff = use
|
@@ -2691,7 +2701,7 @@ charge_config = {
|
|
2691
2701
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2692
2702
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
2693
2703
|
},
|
2694
|
-
'save': 'charge_needed
|
2704
|
+
'save': 'charge_needed ###.txt' # save calculation data for analysis
|
2695
2705
|
}
|
2696
2706
|
|
2697
2707
|
# app key for charge_needed (used to send output via pushover)
|
@@ -2706,7 +2716,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2706
2716
|
# forecast_times: list of hours when forecast can be fetched (UTC)
|
2707
2717
|
# force_charge: 1 = set force charge, 2 = charge for whole period
|
2708
2718
|
|
2709
|
-
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None,
|
2719
|
+
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2710
2720
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2711
2721
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2712
2722
|
global timed_strategy, steps_per_hour, base_time
|
@@ -2758,17 +2768,20 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2758
2768
|
time_change = (change_hour - base_hour) * steps_per_hour
|
2759
2769
|
# get charge times
|
2760
2770
|
times = []
|
2761
|
-
for k in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2771
|
+
for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2762
2772
|
if tariff is not None and tariff.get(k) is not None:
|
2763
|
-
start = time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2764
|
-
end = time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2773
|
+
start = round_time(time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2774
|
+
end = round_time(time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2765
2775
|
force = 0 if tariff[k].get('force') is not None and tariff[k]['force'] == 0 else force_charge
|
2766
2776
|
times.append({'key': k, 'start': start, 'end': end, 'force': force})
|
2767
2777
|
if len(times) == 0:
|
2768
|
-
times.append({'key': 'off_peak1', 'start': round_time(base_hour +
|
2769
|
-
output(f"Charge time: {hours_time(base_hour +
|
2778
|
+
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'force': force_charge})
|
2779
|
+
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2770
2780
|
time_to_end1 = None
|
2781
|
+
start_now = (int(hour_now * 2 + 1) / 2) % 24
|
2771
2782
|
for t in times:
|
2783
|
+
if hour_in(start_now, t):
|
2784
|
+
t['start'] = start_now
|
2772
2785
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2773
2786
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2774
2787
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -2785,9 +2798,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2785
2798
|
time_to_start = times[0]['time_to_start']
|
2786
2799
|
time_to_end = times[0]['time_to_end']
|
2787
2800
|
charge_time = times[0]['charge_time']
|
2788
|
-
if hour_in(hour_now, {'start': round_time(start_at - 0.25), 'end': round_time(end_by + 0.25)}) and update_settings > 0:
|
2789
|
-
print(f"\nInverter settings will not be changed less than 15 minutes before or after the next charging period")
|
2790
|
-
update_settings = 0
|
2791
2801
|
# work out time window and times with clock changes
|
2792
2802
|
time_to_next = int(time_to_start)
|
2793
2803
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
@@ -2941,8 +2951,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2941
2951
|
output(f"\nConsumption (kWh):")
|
2942
2952
|
s = ""
|
2943
2953
|
for h in history:
|
2944
|
-
s += f"
|
2945
|
-
output(s)
|
2954
|
+
s += f" {h['date']} {h['total']:4.1f},"
|
2955
|
+
output(' ' + s[:-1])
|
2946
2956
|
output(f" Average of last {consumption_days} {day_tomorrow if consumption_span=='weekday' else 'day'}s: {consumption:.1f}kWh")
|
2947
2957
|
# time line buckets of consumption
|
2948
2958
|
daily_sum = sum(consumption_by_hour)
|
@@ -2951,14 +2961,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2951
2961
|
solcast_value = None
|
2952
2962
|
solcast_profile = None
|
2953
2963
|
if forecast is None and solcast_api_key is not None and solcast_api_key != 'my.solcast_api_key' and (system_time.hour in forecast_times or run_after == 0):
|
2954
|
-
fsolcast = Solcast(quiet=True,
|
2964
|
+
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'))
|
2955
2965
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2956
2966
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2957
2967
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2958
|
-
|
2959
|
-
output(f"\nSolcast forecast for {today} = {fsolcast.daily[today]['kwh']:.1f}, {tomorrow} = {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2960
|
-
else:
|
2961
|
-
output(f"\nSolcast forecast for {forecast_day} = {solcast_value:.1f}kWh")
|
2968
|
+
output(f"\nSolcast forecast:\n {today}: {fsolcast.daily[today]['kwh']:.1f} (remaining)\n {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2962
2969
|
# get forecast.solar data and produce time line
|
2963
2970
|
solar_value = None
|
2964
2971
|
solar_profile = None
|
@@ -2967,10 +2974,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2967
2974
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2968
2975
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2969
2976
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2970
|
-
|
2971
|
-
output(f"\nSolar forecast for {today} = {fsolar.daily[today]['kwh']:.1f}, {tomorrow} = {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2972
|
-
else:
|
2973
|
-
output(f"\nSolar forecast for {forecast_day} = {solar_value:.1f}kWh")
|
2977
|
+
output(f"\nSolar forecast:\n {today}: {fsolar.daily[today]['kwh']:.1f}\n {tomorrow}: {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2974
2978
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2975
2979
|
output(f"\nNo forecasts available at this time")
|
2976
2980
|
# get generation data
|
@@ -2990,8 +2994,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2990
2994
|
output(f"\nGeneration (kWh):")
|
2991
2995
|
s = ""
|
2992
2996
|
for d in sorted(pv_history.keys())[-gen_days:]:
|
2993
|
-
s += f"
|
2994
|
-
output(s)
|
2997
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
2998
|
+
output(' ' + s[:-1])
|
2995
2999
|
generation = pv_sum / gen_days
|
2996
3000
|
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2997
3001
|
# choose expected value and produce generation time line
|
@@ -3070,15 +3074,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3070
3074
|
kwh_needed = test_charge
|
3071
3075
|
charge_message = "** test charge **"
|
3072
3076
|
# work out charge needed
|
3073
|
-
if kwh_min > reserve and kwh_needed < charge_config['min_kwh']:
|
3074
|
-
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc
|
3077
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh']:
|
3078
|
+
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:3.0f}%)")
|
3075
3079
|
charge_message = "no charge needed"
|
3076
3080
|
kwh_needed = 0.0
|
3077
3081
|
hours = 0.0
|
3078
3082
|
start_timed = time_to_end
|
3079
3083
|
end_timed = time_to_end
|
3080
3084
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3081
|
-
output(f"
|
3085
|
+
output(f" End SoC: {end_soc}% at {hours_time(adjusted_hour(time_to_end, time_line))}")
|
3082
3086
|
# rebuild the battery residual with min_soc for battery hold
|
3083
3087
|
if force_charge > 0 and timed_mode > 1:
|
3084
3088
|
for t in range(int(time_to_start), int(time_to_end)):
|
@@ -3086,7 +3090,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3086
3090
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3087
3091
|
else:
|
3088
3092
|
if test_charge is None:
|
3089
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc
|
3093
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:3.0f}%)")
|
3090
3094
|
charge_message = "with charge added"
|
3091
3095
|
# work out time to add kwh_needed to battery
|
3092
3096
|
taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
|
@@ -3104,7 +3108,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3104
3108
|
price = charge_period.get('price') if charge_period is not None else None
|
3105
3109
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3106
3110
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
3107
|
-
output(f" Charge
|
3111
|
+
output(f" Charge: {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 ""))
|
3108
3112
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
3109
3113
|
j = i + 1
|
3110
3114
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3129,8 +3133,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3129
3133
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3130
3134
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3131
3135
|
# show the state
|
3132
|
-
output(f" Start SoC {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
3133
|
-
output(f" End SoC {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3136
|
+
output(f" Start SoC: {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
3137
|
+
output(f" End SoC: {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3134
3138
|
# show what we have worked out
|
3135
3139
|
if show_data == 3:
|
3136
3140
|
output(f"\nTime, Generation, Charge, Consumption, Discharge, Residual, kWh")
|
@@ -3139,17 +3143,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3139
3143
|
output(f" {hours_time(h)}, {generation_timed[i]:6.3f}, {charge_timed[i]:6.3f}, {consumption_timed[i]:6.3f}, {discharge_timed[i]:6.3f}, {bat_timed[i]:6.3f}")
|
3140
3144
|
if show_data > 0:
|
3141
3145
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3142
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3146
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3143
3147
|
h = base_hour + 1
|
3144
3148
|
t = steps_per_hour
|
3145
|
-
s += " " * (
|
3149
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3146
3150
|
while t < len(time_line) and bat_timed[t] is not None:
|
3147
3151
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3148
3152
|
s += f" {hours_time(time_line[t])}"
|
3149
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}
|
3153
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
|
3150
3154
|
h += 1
|
3151
3155
|
t += steps_per_hour
|
3152
|
-
output(s)
|
3156
|
+
output(s[:-1])
|
3153
3157
|
if show_plot > 0:
|
3154
3158
|
print()
|
3155
3159
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3242,7 +3246,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3242
3246
|
run_time = len(time_line)
|
3243
3247
|
base_hour = int(time_hours(base_time[11:16]))
|
3244
3248
|
start_day = base_time[:10]
|
3245
|
-
print(f"Run at {start_day} {hours_time(hour_now)} with SoC
|
3249
|
+
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:3.0f}%")
|
3246
3250
|
now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
|
3247
3251
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3248
3252
|
if v is None:
|
@@ -3277,17 +3281,17 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3277
3281
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3278
3282
|
if show_data > 0 and plots.get('SoC') is not None:
|
3279
3283
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3280
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3284
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3281
3285
|
h = base_hour + 1
|
3282
3286
|
t = steps_per_hour
|
3283
|
-
s += " " * (
|
3287
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3284
3288
|
while t < len(time_line) and plots['SoC'][t] is not None:
|
3285
3289
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3286
3290
|
s += f" {hours_time(time_line[t])}"
|
3287
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}
|
3291
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3288
3292
|
h += 1
|
3289
3293
|
t += steps_per_hour
|
3290
|
-
print(s)
|
3294
|
+
print(s[:-1])
|
3291
3295
|
if show_plot > 0:
|
3292
3296
|
print()
|
3293
3297
|
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: 22 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.2"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -786,13 +786,15 @@ merge_settings = { # keys to add
|
|
786
786
|
'WorkMode': {'keys': {
|
787
787
|
'h115__': 'operation_mode__work_mode',
|
788
788
|
'h116__': 'operation_mode__work_mode',
|
789
|
-
'h117__': 'operation_mode__work_mode'
|
789
|
+
'h117__': 'operation_mode__work_mode',
|
790
|
+
# 'k106__': 'operation_mode__work_mode',
|
790
791
|
},
|
791
792
|
'values': ['SelfUse', 'Feedin', 'Backup']},
|
792
793
|
'BatteryVolt': {'keys': {
|
793
794
|
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
794
795
|
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
795
|
-
'h117__': ['h117__15', 'h117__16', 'h117__17']
|
796
|
+
'h117__': ['h117__15', 'h117__16', 'h117__17'],
|
797
|
+
# 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
|
796
798
|
},
|
797
799
|
'type': 'list',
|
798
800
|
'valueType': 'float',
|
@@ -801,11 +803,11 @@ merge_settings = { # keys to add
|
|
801
803
|
'h115__': 'h115__17',
|
802
804
|
'h116__': 'h116__18',
|
803
805
|
'h117__': 'h117__18',
|
806
|
+
# 'k106__': 'k106__xx',
|
804
807
|
},
|
805
808
|
'type': 'list',
|
806
809
|
'valueType': 'int',
|
807
810
|
'unit': '℃'},
|
808
|
-
|
809
811
|
}
|
810
812
|
|
811
813
|
def get_ui():
|
@@ -1391,7 +1393,7 @@ def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=Non
|
|
1391
1393
|
if e > 0.0:
|
1392
1394
|
kwh += e
|
1393
1395
|
if tariff is not None:
|
1394
|
-
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3')]):
|
1396
|
+
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3'), tariff.get('off_peak4')]):
|
1395
1397
|
kwh_off += e
|
1396
1398
|
elif hour_in(h, [tariff.get('peak1'), tariff.get('peak2')]):
|
1397
1399
|
kwh_peak += e
|
@@ -2209,56 +2211,36 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2209
2211
|
strategy.append(prices[t])
|
2210
2212
|
output(f" {format_period(prices[t])} at {prices[t]['price']:.1f}p", 1)
|
2211
2213
|
tariff['agile']['strategy'] = strategy
|
2212
|
-
for key in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2214
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2213
2215
|
if tariff.get(key) is None:
|
2214
2216
|
continue
|
2215
2217
|
if tariff['agile'].get(key) is None:
|
2216
2218
|
tariff['agile'][key] = {}
|
2217
2219
|
# get price index for AM/PM charge times
|
2218
|
-
slots = []
|
2219
|
-
for i in range(0, len(prices)):
|
2220
|
-
if hour_in(time_hours(prices[i]['start']), tariff[key]):
|
2221
|
-
slots.append(i)
|
2220
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), tariff[key])]
|
2222
2221
|
tariff['agile'][key]['slots'] = slots
|
2223
|
-
|
2224
|
-
weighting = tariff_config.get('weighting')
|
2225
|
-
tariff['agile'][key]['times'] = []
|
2226
|
-
for j in range (0, len(slots)):
|
2227
|
-
span = j + 1
|
2228
|
-
weights = (([1.0] * (span-1) if weighting is None else weighting) + [0.5] * span)[:span]
|
2229
|
-
best = None
|
2230
|
-
price = None
|
2231
|
-
for i in range(0, len(slots) - j):
|
2232
|
-
t = slots[i: i + span]
|
2233
|
-
p_span = [prices[x]['price'] for x in t]
|
2234
|
-
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2235
|
-
if price is None or wavg < price:
|
2236
|
-
price = wavg
|
2237
|
-
best = t
|
2238
|
-
# save best time slot for charge duration
|
2239
|
-
start = prices[best[0]]['start']
|
2240
|
-
tariff['agile'][key]['times'].append({'start': start, 'end': round_time(start + span / 2), 'price': price, 'best': best, 'key': key})
|
2222
|
+
tariff['agile'][key]['avg'] = avg([prices[t]['price'] for t in slots])
|
2241
2223
|
# show the results
|
2242
2224
|
if tariff_config['show_data'] > 0:
|
2243
2225
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2244
2226
|
t = (now.hour * 2) % data_wrap
|
2245
|
-
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t *
|
2227
|
+
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
|
2246
2228
|
for i in range(0, len(prices)):
|
2247
2229
|
s += "\n" if i > 0 and t % data_wrap == 0 else ""
|
2248
|
-
s += f" {prices[i]['time']} {prices[i]['price']:4.1f}"
|
2230
|
+
s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
|
2249
2231
|
t += 1
|
2250
|
-
output(s)
|
2232
|
+
output(s[:-1])
|
2251
2233
|
if tariff_config['show_plot'] > 0:
|
2252
2234
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2253
2235
|
x_timed = [i for i in range(0, len(prices))]
|
2254
2236
|
plt.xticks(ticks=x_timed, labels=[prices[x]['time'] for x in x_timed], rotation=90, fontsize=8, ha='center')
|
2255
2237
|
plt.plot(x_timed, [prices[x]['price'] for x in x_timed], label='30 minute price', color='blue')
|
2256
2238
|
s = ""
|
2257
|
-
for key in ['off_peak1', 'off_peak2']:
|
2258
|
-
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['
|
2259
|
-
p = tariff['agile'][key]
|
2260
|
-
plt.plot(x_timed, [p['
|
2261
|
-
s += f"\n {
|
2239
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2240
|
+
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['slots']) > 0:
|
2241
|
+
p = tariff['agile'][key]
|
2242
|
+
plt.plot(x_timed, [p['avg'] if x in p['slots'] else None for x in x_timed], label=f"{key} {p['avg']:.1f}p")
|
2243
|
+
s += f"\n {hours_time(prices[p['slots'][0]]['start'])}-{hours_time(prices[p['slots'][-1]]['end'])} at {p['avg']:.1f}p"
|
2262
2244
|
output(f"\nCharge times{s}" if s != "" else "", 1)
|
2263
2245
|
plt.title(f"Pricing on {today} p/kWh inc VAT", fontsize=10)
|
2264
2246
|
plt.legend(fontsize=8)
|
@@ -2269,13 +2251,40 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2269
2251
|
# return the best charge time:
|
2270
2252
|
def get_best_charge_period(start, duration):
|
2271
2253
|
global tariff
|
2272
|
-
if tariff is None:
|
2254
|
+
if tariff is None or tariff.get('agile') is None or tariff['agile'].get('prices') is None:
|
2255
|
+
return None
|
2256
|
+
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2257
|
+
key = key[0] if len(key) > 0 else None
|
2258
|
+
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2259
|
+
span = int(duration * 2 + 0.99)
|
2260
|
+
coverage = max([round_time(end - start), duration])
|
2261
|
+
period = {'start': start, 'end': round_time(start + coverage)}
|
2262
|
+
prices = tariff['agile']['prices']
|
2263
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), period)]
|
2264
|
+
if len(slots) == 0:
|
2273
2265
|
return None
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2266
|
+
elif len(slots) == 1:
|
2267
|
+
best = slots
|
2268
|
+
price = prices[slots[0]]['price']
|
2269
|
+
best_start = start
|
2270
|
+
else:
|
2271
|
+
# best charge time for duration
|
2272
|
+
weighting = tariff_config.get('weighting')
|
2273
|
+
times = []
|
2274
|
+
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2275
|
+
best = None
|
2276
|
+
price = None
|
2277
|
+
for i in range(0, len(slots) - span + 1):
|
2278
|
+
t = slots[i: i + span]
|
2279
|
+
p_span = [prices[x]['price'] for x in t]
|
2280
|
+
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2281
|
+
if price is None or wavg < price:
|
2282
|
+
price = wavg
|
2283
|
+
best = t
|
2284
|
+
best_start = prices[best[0]]['start']
|
2285
|
+
# save best time slot for charge duration
|
2286
|
+
tariff['agile']['best'] = {'start': best_start, 'end': round_time(best_start + span / 2), 'price': price, 'slots': best, 'key': key}
|
2287
|
+
return tariff['agile']['best']
|
2279
2288
|
|
2280
2289
|
# pushover app key for set_tariff()
|
2281
2290
|
set_tariff_app_key = "apx24dswzinhrbeb62sdensvt42aqe"
|
@@ -2319,7 +2328,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2319
2328
|
times = [times]
|
2320
2329
|
output(f"\n{use['name']}:")
|
2321
2330
|
for t in times:
|
2322
|
-
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'peak1', 'peak2']:
|
2331
|
+
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4', 'peak1', 'peak2']:
|
2323
2332
|
output(f"** set_tariff(): invalid time period {t}")
|
2324
2333
|
continue
|
2325
2334
|
key = t[0]
|
@@ -2358,7 +2367,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2358
2367
|
elif type(strategy) is not list:
|
2359
2368
|
strategy = [strategy]
|
2360
2369
|
output(f"\nStrategy")
|
2361
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3')])
|
2370
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2362
2371
|
output_close(plot=tariff_config['show_plot'])
|
2363
2372
|
if update == 1:
|
2364
2373
|
tariff = use
|
@@ -2553,7 +2562,7 @@ charge_config = {
|
|
2553
2562
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2554
2563
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
2555
2564
|
},
|
2556
|
-
'save': 'charge_needed
|
2565
|
+
'save': 'charge_needed ###.txt' # save calculation data for analysis
|
2557
2566
|
}
|
2558
2567
|
|
2559
2568
|
# app key for charge_needed (used to send output via pushover)
|
@@ -2568,7 +2577,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2568
2577
|
# forecast_times: list of hours when forecast can be fetched (UTC)
|
2569
2578
|
# force_charge: 1 = set force charge, 2 = charge for whole period
|
2570
2579
|
|
2571
|
-
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None,
|
2580
|
+
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2572
2581
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2573
2582
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2574
2583
|
global timed_strategy, steps_per_hour, base_time
|
@@ -2604,6 +2613,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2604
2613
|
now = system_time + timedelta(hours=time_offset)
|
2605
2614
|
today = datetime.strftime(now, '%Y-%m-%d')
|
2606
2615
|
base_hour = now.hour
|
2616
|
+
base_time = today + f" {hours_time(base_hour)}"
|
2607
2617
|
hour_now = now.hour + now.minute / 60
|
2608
2618
|
output(f" datetime = {today} {hours_time(hour_now)}", 2)
|
2609
2619
|
yesterday = datetime.strftime(now - timedelta(days=1), '%Y-%m-%d')
|
@@ -2619,17 +2629,20 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2619
2629
|
time_change = (change_hour - base_hour) * steps_per_hour
|
2620
2630
|
# get charge times
|
2621
2631
|
times = []
|
2622
|
-
for k in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2632
|
+
for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2623
2633
|
if tariff is not None and tariff.get(k) is not None:
|
2624
|
-
start = time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2625
|
-
end = time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2634
|
+
start = round_time(time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2635
|
+
end = round_time(time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2626
2636
|
force = 0 if tariff[k].get('force') is not None and tariff[k]['force'] == 0 else force_charge
|
2627
2637
|
times.append({'key': k, 'start': start, 'end': end, 'force': force})
|
2628
2638
|
if len(times) == 0:
|
2629
|
-
times.append({'key': 'off_peak1', 'start': round_time(base_hour +
|
2630
|
-
output(f"Charge time: {hours_time(base_hour +
|
2639
|
+
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'force': force_charge})
|
2640
|
+
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2631
2641
|
time_to_end1 = None
|
2642
|
+
start_now = (int(hour_now * 2 + 1) / 2) % 24
|
2632
2643
|
for t in times:
|
2644
|
+
if hour_in(start_now, t):
|
2645
|
+
t['start'] = start_now
|
2633
2646
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2634
2647
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2635
2648
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -2646,9 +2659,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2646
2659
|
time_to_start = times[0]['time_to_start']
|
2647
2660
|
time_to_end = times[0]['time_to_end']
|
2648
2661
|
charge_time = times[0]['charge_time']
|
2649
|
-
if hour_in(hour_now, {'start': round_time(start_at - 0.25), 'end': round_time(end_by + 0.25)}) and update_settings > 0:
|
2650
|
-
print(f"\nInverter settings will not be changed less than 15 minutes before or after the next charging period")
|
2651
|
-
update_settings = 0
|
2652
2662
|
# work out time window and times with clock changes
|
2653
2663
|
time_to_next = int(time_to_start)
|
2654
2664
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
@@ -2802,8 +2812,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2802
2812
|
output(f"\nConsumption (kWh):")
|
2803
2813
|
s = ""
|
2804
2814
|
for h in history:
|
2805
|
-
s += f"
|
2806
|
-
output(s)
|
2815
|
+
s += f" {h['date']}: {h['total']:4.1f},"
|
2816
|
+
output(' ' + s[:-1])
|
2807
2817
|
output(f" Average of last {consumption_days} {day_tomorrow if consumption_span=='weekday' else 'day'}s: {consumption:.1f}kWh")
|
2808
2818
|
# time line buckets of consumption
|
2809
2819
|
daily_sum = sum(consumption_by_hour)
|
@@ -2812,14 +2822,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2812
2822
|
solcast_value = None
|
2813
2823
|
solcast_profile = None
|
2814
2824
|
if forecast is None and solcast_api_key is not None and solcast_api_key != 'my.solcast_api_key' and (system_time.hour in forecast_times or run_after == 0):
|
2815
|
-
fsolcast = Solcast(quiet=True,
|
2825
|
+
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'))
|
2816
2826
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2817
2827
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2818
2828
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2819
|
-
|
2820
|
-
output(f"\nSolcast forecast for {today} = {fsolcast.daily[today]['kwh']:.1f}, {tomorrow} = {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2821
|
-
else:
|
2822
|
-
output(f"\nSolcast forecast for {forecast_day} = {solcast_value:.1f}kWh")
|
2829
|
+
output(f"\nSolcast forecast:\n {today}: {fsolcast.daily[today]['kwh']:.1f} (remaining)\n {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2823
2830
|
# get forecast.solar data and produce time line
|
2824
2831
|
solar_value = None
|
2825
2832
|
solar_profile = None
|
@@ -2828,10 +2835,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2828
2835
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2829
2836
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2830
2837
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2831
|
-
|
2832
|
-
output(f"\nSolar forecast for {today} = {fsolar.daily[today]['kwh']:.1f}, {tomorrow} = {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2833
|
-
else:
|
2834
|
-
output(f"\nSolar forecast for {forecast_day} = {solar_value:.1f}kWh")
|
2838
|
+
output(f"\nSolar forecast:\n {today}: {fsolcast.daily[today]['kwh']:.1f}\n {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2835
2839
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2836
2840
|
output(f"\nNo forecasts available at this time")
|
2837
2841
|
# get generation data
|
@@ -2851,8 +2855,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2851
2855
|
output(f"\nGeneration (kWh):")
|
2852
2856
|
s = ""
|
2853
2857
|
for d in sorted(pv_history.keys())[-gen_days:]:
|
2854
|
-
s += f"
|
2855
|
-
output(s)
|
2858
|
+
s += f" {d}: {pv_history[d]:4.1f},"
|
2859
|
+
output(' ' + s[:-1])
|
2856
2860
|
generation = pv_sum / gen_days
|
2857
2861
|
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2858
2862
|
# choose expected value and produce generation time line
|
@@ -2931,15 +2935,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2931
2935
|
kwh_needed = test_charge
|
2932
2936
|
charge_message = "** test charge **"
|
2933
2937
|
# work out charge needed
|
2934
|
-
if kwh_min > reserve and kwh_needed < charge_config['min_kwh'] and full_charge is None and test_charge is None and force_charge != 2:
|
2935
|
-
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc
|
2938
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and full_charge is None and test_charge is None and force_charge != 2:
|
2939
|
+
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:3.0f}%)")
|
2936
2940
|
charge_message = "no charge needed"
|
2937
2941
|
kwh_needed = 0.0
|
2938
2942
|
hours = 0.0
|
2939
2943
|
start_timed = time_to_end
|
2940
2944
|
end_timed = time_to_end
|
2941
2945
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
2942
|
-
output(f"
|
2946
|
+
output(f" End SoC: {end_soc}% at {hours_time(adjusted_hour(time_to_end, time_line))}")
|
2943
2947
|
# rebuild the battery residual with min_soc for battery hold
|
2944
2948
|
if force_charge > 0 and timed_mode > 1:
|
2945
2949
|
for t in range(int(time_to_start), int(time_to_end)):
|
@@ -2947,7 +2951,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2947
2951
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
2948
2952
|
else:
|
2949
2953
|
if test_charge is None:
|
2950
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc
|
2954
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:3.0f}%)")
|
2951
2955
|
charge_message = "with charge added"
|
2952
2956
|
# work out time to add kwh_needed to battery
|
2953
2957
|
taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
|
@@ -2965,7 +2969,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2965
2969
|
price = charge_period.get('price') if charge_period is not None else None
|
2966
2970
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
2967
2971
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
2968
|
-
output(f" Charge
|
2972
|
+
output(f" Charge: {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 ""))
|
2969
2973
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
2970
2974
|
j = i + 1
|
2971
2975
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -2990,8 +2994,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2990
2994
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
2991
2995
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
2992
2996
|
# show the state
|
2993
|
-
output(f" Start SoC {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
2994
|
-
output(f" End SoC {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
2997
|
+
output(f" Start SoC: {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
2998
|
+
output(f" End SoC: {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
2995
2999
|
# show what we have worked out
|
2996
3000
|
if show_data == 3:
|
2997
3001
|
output(f"\nTime, Generation, Charge, Consumption, Discharge, Residual, kWh")
|
@@ -3000,17 +3004,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3000
3004
|
output(f" {hours_time(h)}, {generation_timed[i]:6.3f}, {charge_timed[i]:6.3f}, {consumption_timed[i]:6.3f}, {discharge_timed[i]:6.3f}, {bat_timed[i]:6.3f}")
|
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:\n" if show_data == 2 else f"\nBattery SoC
|
3007
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3004
3008
|
h = base_hour + 1
|
3005
3009
|
t = steps_per_hour
|
3006
|
-
s += " " * (
|
3010
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3007
3011
|
while t < len(time_line) and bat_timed[t] is not None:
|
3008
3012
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3009
3013
|
s += f" {hours_time(time_line[t])}"
|
3010
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}
|
3014
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
|
3011
3015
|
h += 1
|
3012
3016
|
t += steps_per_hour
|
3013
|
-
output(s)
|
3017
|
+
output(s[:-1])
|
3014
3018
|
if show_plot > 0:
|
3015
3019
|
print()
|
3016
3020
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3102,7 +3106,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3102
3106
|
run_time = len(time_line)
|
3103
3107
|
base_hour = int(time_hours(base_time[11:16]))
|
3104
3108
|
start_day = base_time[:10]
|
3105
|
-
print(f"Run at {start_day} {hours_time(hour_now)} with SoC
|
3109
|
+
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:3.0f}%")
|
3106
3110
|
now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
|
3107
3111
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3108
3112
|
if v is None:
|
@@ -3137,17 +3141,17 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3137
3141
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3138
3142
|
if show_data > 0 and plots.get('SoC') is not None:
|
3139
3143
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3140
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3144
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3141
3145
|
h = base_hour + 1
|
3142
3146
|
t = steps_per_hour
|
3143
|
-
s += " " * (
|
3147
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3144
3148
|
while t < len(time_line) and plots['SoC'][t] is not None:
|
3145
3149
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3146
3150
|
s += f" {hours_time(time_line[t])}"
|
3147
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}
|
3151
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3148
3152
|
h += 1
|
3149
3153
|
t += steps_per_hour
|
3150
|
-
print(s)
|
3154
|
+
print(s[:-1])
|
3151
3155
|
if show_plot > 0:
|
3152
3156
|
print()
|
3153
3157
|
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.2
|
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
|
@@ -239,7 +239,7 @@ The summary includes the following attributes:
|
|
239
239
|
|
240
240
|
For power values (unit = kW), the summary performs a Riemann sum of the data, integrating kW over the day to estimate energy in kWh. In this case, the following attributes are also added:
|
241
241
|
+ kwh: the total energy generated or consumed
|
242
|
-
+ kwh_off: the total energy consumed or generated during the off-peak time of use (off_peak1, off_peak2, off_peak3)
|
242
|
+
+ kwh_off: the total energy consumed or generated during the off-peak time of use (off_peak1, off_peak2, off_peak3, off_peak4)
|
243
243
|
+ kwh_peak: the total energy consumed or generated during the peak time of use (peak1, peak2)
|
244
244
|
+ kwh_neg: the total energy from -ve power flow (all other totals are based on +ve power flow)
|
245
245
|
|
@@ -565,7 +565,7 @@ The best charging period is determined based on the weighted average of the 30 m
|
|
565
565
|
|
566
566
|
set_tariff() can configure multiple off-peak and peak periods for any tariff using the 'times' parameter. Times is a list of tuples:
|
567
567
|
+ containing values for key, 'start', 'end' and optional 'force'.
|
568
|
-
+ recongnised keys are: 'off_peak1', 'off_peak2', 'off_peak3', 'peak1', 'peak2'
|
568
|
+
+ recongnised keys are: 'off_peak1', 'off_peak2', 'off_peak3', 'off_peak4', 'peak1', 'peak2'
|
569
569
|
+ a tuple containing a key with no values will remove the time period from the tariff.
|
570
570
|
|
571
571
|
For example, this parameter configures an AM charging period between 11pm and 8am and a PM charging period between 12 noon and 4pm and removes the time period 'peak2':
|
@@ -782,6 +782,12 @@ This setting can be:
|
|
782
782
|
|
783
783
|
# Version Info
|
784
784
|
|
785
|
+
2.5.2<br>
|
786
|
+
Updates to allow charge_needed() to run during a charge period.
|
787
|
+
Add suport for 'off_peak4' charge period.
|
788
|
+
Change Solcast forecast in charge_needed() so it does not get todays estimate to save API calls.
|
789
|
+
Include contingency and reserve when checking minimum battery level.
|
790
|
+
|
785
791
|
2.5.1<br>
|
786
792
|
Fix anomaly in scheduler support when get_device and get_flag return different results.
|
787
793
|
Add 'show_data' to charge_compare() and display run time and starting SoC.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=sf5LJSJ2gP_Bs87rMVgfFJJLhU_tdOAM0aun2J0ZNlk,210279
|
2
|
+
foxesscloud/openapi.py,sha256=8mgfPW7DZg9m7rjTLaf1uofY6mco0Ad4xv0o03pZML8,203654
|
3
|
+
foxesscloud-2.5.2.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.5.2.dist-info/METADATA,sha256=wQD_1dfXG1Ta5UOnSVyq6L2NtRR6PB2gHySnGV_XD9U,54292
|
5
|
+
foxesscloud-2.5.2.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.5.2.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.5.2.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=UYcUEZnOS2vVLGNYGSC4Cyiyb6yrZeyG-SN1ZDAT9YA,210145
|
2
|
-
foxesscloud/openapi.py,sha256=PPkVlnwl3bZsY0TmoTsebLu-fM_SUAwIKKsrg9og3LQ,203465
|
3
|
-
foxesscloud-2.5.1.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.5.1.dist-info/METADATA,sha256=4Egwh0nhUqi3DtJ5xQumMJwd-ncFPoDpHHBz6O2jLu8,53979
|
5
|
-
foxesscloud-2.5.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.5.1.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.5.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|