foxesscloud 2.6.9__py3-none-any.whl → 2.7.1__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 +132 -111
- foxesscloud/openapi.py +129 -109
- {foxesscloud-2.6.9.dist-info → foxesscloud-2.7.1.dist-info}/METADATA +26 -8
- foxesscloud-2.7.1.dist-info/RECORD +7 -0
- foxesscloud-2.6.9.dist-info/RECORD +0 -7
- {foxesscloud-2.6.9.dist-info → foxesscloud-2.7.1.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.9.dist-info → foxesscloud-2.7.1.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.9.dist-info → foxesscloud-2.7.1.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: 07 November 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.8.
|
13
|
+
version = "1.8.2"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -798,10 +798,10 @@ def get_charge():
|
|
798
798
|
def time_period(t):
|
799
799
|
result = f"{t['startTime']['hour']:02d}:{t['startTime']['minute']:02d}-{t['endTime']['hour']:02d}:{t['endTime']['minute']:02d}"
|
800
800
|
if t['startTime']['hour'] != t['endTime']['hour'] or t['startTime']['minute'] != t['endTime']['minute']:
|
801
|
-
result += f" Charge from grid" if t['enableGrid'] else f"
|
801
|
+
result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
|
802
802
|
return result
|
803
803
|
|
804
|
-
def set_charge(ch1=
|
804
|
+
def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force=0, enable=1):
|
805
805
|
global device_sn, battery_settings, debug_setting, messages, schedule
|
806
806
|
if get_device() is None:
|
807
807
|
return None
|
@@ -1081,6 +1081,8 @@ def get_remote_settings(key):
|
|
1081
1081
|
|
1082
1082
|
def get_named_settings(name):
|
1083
1083
|
global named_settings
|
1084
|
+
if get_device() is None:
|
1085
|
+
return None
|
1084
1086
|
if type(name) is list:
|
1085
1087
|
result = []
|
1086
1088
|
for n in name:
|
@@ -1748,9 +1750,7 @@ def plot_raw(result, plot=1, station=0):
|
|
1748
1750
|
def report_value_profile(result):
|
1749
1751
|
if type(result) is not list or result[0]['type'] != 'day':
|
1750
1752
|
return (None, None)
|
1751
|
-
data = []
|
1752
|
-
for h in range(0,24):
|
1753
|
-
data.append((0.0, 0)) # value sum, count of values
|
1753
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1754
1754
|
totals = 0
|
1755
1755
|
n = 0
|
1756
1756
|
for day in result:
|
@@ -1780,6 +1780,30 @@ def report_value_profile(result):
|
|
1780
1780
|
# forwards compatibility
|
1781
1781
|
get_history = get_raw
|
1782
1782
|
|
1783
|
+
# rescale history data based on time and steps
|
1784
|
+
def rescale_history(data, steps):
|
1785
|
+
if data is None or len(data) < 1:
|
1786
|
+
return None
|
1787
|
+
result = [None for i in range(0, 24 * steps)]
|
1788
|
+
bst = 1 if 'BST' in data[0]['time'] else 0
|
1789
|
+
average = 0.0
|
1790
|
+
n = 0
|
1791
|
+
i = 0
|
1792
|
+
for d in data:
|
1793
|
+
h = round_time(time_hours(d['time'][11:]) + bst)
|
1794
|
+
new_i = int(h * steps)
|
1795
|
+
if new_i != i and i < len(result):
|
1796
|
+
result[i] = average / n if n > 0 else None
|
1797
|
+
average = 0.0
|
1798
|
+
n = 0
|
1799
|
+
i = new_i
|
1800
|
+
if d['value'] is not None:
|
1801
|
+
average += d['value']
|
1802
|
+
n += 1
|
1803
|
+
if n > 0 and i < len(result):
|
1804
|
+
result[i] = average / n
|
1805
|
+
return result
|
1806
|
+
|
1783
1807
|
##################################################################################################
|
1784
1808
|
# get energy report data in kWh
|
1785
1809
|
##################################################################################################
|
@@ -2752,8 +2776,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2752
2776
|
strategy = get_strategy(timed_mode=timed_mode)
|
2753
2777
|
for i in range(0, run_time):
|
2754
2778
|
h = time_line[i]
|
2755
|
-
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2756
|
-
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2779
|
+
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2780
|
+
'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2757
2781
|
if strategy is not None:
|
2758
2782
|
period['mode'] = 'SelfUse'
|
2759
2783
|
for d in strategy:
|
@@ -2780,41 +2804,55 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2780
2804
|
global charge_config, steps_per_hour
|
2781
2805
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2782
2806
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2783
|
-
charge_loss = charge_config['
|
2784
|
-
discharge_loss = charge_config['
|
2807
|
+
charge_loss = charge_config['_charge_loss']
|
2808
|
+
discharge_loss = charge_config['_discharge_loss']
|
2785
2809
|
charge_limit = charge_config['charge_limit']
|
2786
2810
|
float_charge = charge_config['float_charge']
|
2787
2811
|
run_time = len(work_mode_timed)
|
2788
2812
|
for i in range(0, run_time):
|
2789
2813
|
w = work_mode_timed[i]
|
2790
2814
|
w['kwh'] = kwh_current
|
2815
|
+
kwh_next = kwh_current
|
2791
2816
|
max_now = w['max_soc'] * capacity / 100
|
2792
|
-
|
2793
|
-
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2794
|
-
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2795
|
-
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2796
|
-
if kwh_current > capacity:
|
2797
|
-
# battery is full
|
2798
|
-
kwh_current = capacity
|
2799
|
-
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2800
|
-
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2817
|
+
min_soc_now = w['min_soc']
|
2801
2818
|
reserve_now = capacity * min_soc_now / 100
|
2802
|
-
|
2819
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2820
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2821
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2822
|
+
# charge from grid or force charge
|
2823
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2824
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2825
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2826
|
+
# force discharge
|
2827
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2828
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2829
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2830
|
+
else:
|
2831
|
+
# normal discharge
|
2832
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2833
|
+
if kwh_next > capacity:
|
2834
|
+
# battery is full
|
2835
|
+
kwh_next = capacity
|
2836
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2803
2837
|
# battery is empty, check if charge is needed
|
2804
|
-
|
2805
|
-
|
2806
|
-
|
2838
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2839
|
+
kwh_next = reserve_now
|
2840
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2807
2841
|
if reserve_drain <= reserve_limit:
|
2842
|
+
# float charge
|
2808
2843
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2844
|
+
kwh_next = reserve_drain
|
2809
2845
|
else:
|
2810
2846
|
# BMS power drain
|
2847
|
+
kwh_next = reserve_drain
|
2811
2848
|
reserve_drain -= bms_loss / steps_per_hour
|
2812
2849
|
else:
|
2813
2850
|
# reset drain level
|
2814
2851
|
reserve_drain = reserve_now
|
2815
|
-
if kwh_min is not None and
|
2816
|
-
kwh_min =
|
2817
|
-
|
2852
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2853
|
+
kwh_min = kwh_next
|
2854
|
+
kwh_current = kwh_next
|
2855
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2818
2856
|
|
2819
2857
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2820
2858
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -2874,6 +2912,8 @@ charge_config = {
|
|
2874
2912
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2875
2913
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2876
2914
|
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2915
|
+
'charge_loss': None, # loss converting charge energy to stored energy
|
2916
|
+
'discharge_loss': None, # loss converting stored energy to discharge energy
|
2877
2917
|
'inverter_power': 101, # Inverter power consumption in W
|
2878
2918
|
'bms_power': 50, # BMS power consumption in W
|
2879
2919
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -3020,14 +3060,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3020
3060
|
output(f"full_charge = {full_charge}")
|
3021
3061
|
if test_soc is not None:
|
3022
3062
|
current_soc = test_soc
|
3023
|
-
capacity = 14.
|
3063
|
+
capacity = 14.46
|
3024
3064
|
residual = test_soc * capacity / 100
|
3025
3065
|
bat_volt = 317.4
|
3026
3066
|
bat_power = 0.0
|
3027
3067
|
temperature = 30
|
3028
3068
|
bms_charge_current = 15
|
3029
|
-
charge_loss = battery_params[2]['charge_loss']
|
3030
|
-
discharge_loss = battery_params[2]['discharge_loss']
|
3069
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
|
3070
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
|
3031
3071
|
bat_current = 0.0
|
3032
3072
|
device_power = 6.0
|
3033
3073
|
device_current = 35
|
@@ -3049,8 +3089,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3049
3089
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
3050
3090
|
return None
|
3051
3091
|
bms_charge_current = battery.get('charge_rate')
|
3052
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3053
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
3092
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3093
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
3054
3094
|
device_power = device.get('power')
|
3055
3095
|
device_current = device.get('max_charge_current')
|
3056
3096
|
model = device.get('deviceType')
|
@@ -3079,7 +3119,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3079
3119
|
output(f" Temperature: {temperature:.1f}°C")
|
3080
3120
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
3081
3121
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
3082
|
-
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
|
3122
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
|
3083
3123
|
# inverter losses
|
3084
3124
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
3085
3125
|
operating_loss = inverter_power / 1000
|
@@ -3108,8 +3148,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3108
3148
|
charge_config['charge_limit'] = charge_limit
|
3109
3149
|
charge_config['charge_power'] = charge_power
|
3110
3150
|
charge_config['float_charge'] = float_charge
|
3111
|
-
charge_config['
|
3112
|
-
charge_config['
|
3151
|
+
charge_config['_charge_loss'] = charge_loss
|
3152
|
+
charge_config['_discharge_loss'] = discharge_loss
|
3113
3153
|
# display what we have
|
3114
3154
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
3115
3155
|
output(f"\nDevice Info:")
|
@@ -3171,27 +3211,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3171
3211
|
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
3172
3212
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
3173
3213
|
output(f"\nNo forecasts available at this time")
|
3174
|
-
# get generation data
|
3175
|
-
generation = None
|
3176
|
-
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
3177
|
-
gen_days = charge_config['generation_days']
|
3178
|
-
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
3179
|
-
pv_history = {}
|
3180
|
-
if history is not None and len(history) > 0:
|
3181
|
-
for day in history:
|
3182
|
-
date = day['date']
|
3183
|
-
if pv_history.get(date) is None:
|
3184
|
-
pv_history[date] = 0.0
|
3185
|
-
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
3186
|
-
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
3187
|
-
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
3188
|
-
output(f"\nGeneration (kWh):")
|
3189
|
-
s = ""
|
3190
|
-
for d in sorted(pv_history.keys())[-gen_days:]:
|
3191
|
-
s += f" {d} {pv_history[d]:4.1f},"
|
3192
|
-
output(' ' + s[:-1])
|
3193
|
-
generation = pv_sum / gen_days
|
3194
|
-
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
3195
3214
|
# choose expected value and produce generation time line
|
3196
3215
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
3197
3216
|
sun_name = seasonal_sun[quarter]['name']
|
@@ -3209,11 +3228,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3209
3228
|
elif solar_value is not None:
|
3210
3229
|
expected = solar_value
|
3211
3230
|
generation_timed = solar_timed
|
3212
|
-
elif generation is None or generation == 0.0:
|
3213
|
-
output(f"\nNo generation data available")
|
3214
|
-
output_close()
|
3215
|
-
return None
|
3216
3231
|
else:
|
3232
|
+
# no forecast, use generation history
|
3233
|
+
generation = None
|
3234
|
+
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
3235
|
+
gen_days = charge_config['generation_days']
|
3236
|
+
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
3237
|
+
pv_history = {}
|
3238
|
+
if history is not None and len(history) > 0:
|
3239
|
+
for day in history:
|
3240
|
+
date = day['date']
|
3241
|
+
if pv_history.get(date) is None:
|
3242
|
+
pv_history[date] = 0.0
|
3243
|
+
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
3244
|
+
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
3245
|
+
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
3246
|
+
output(f"\nGeneration (kWh):")
|
3247
|
+
s = ""
|
3248
|
+
for d in sorted(pv_history.keys())[-gen_days:]:
|
3249
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
3250
|
+
output(' ' + s[:-1])
|
3251
|
+
generation = pv_sum / gen_days
|
3252
|
+
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
3253
|
+
if generation is None or generation == 0.0:
|
3254
|
+
output(f"\nNo generation data available")
|
3255
|
+
output_close()
|
3256
|
+
return None
|
3217
3257
|
expected = generation
|
3218
3258
|
generation_timed = [expected * x / sun_sum for x in sun_timed]
|
3219
3259
|
if charge_config['forecast_selection'] == 1 and update_settings > 0:
|
@@ -3221,7 +3261,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3221
3261
|
update_settings = 0
|
3222
3262
|
# produce time lines for charge, discharge and work mode
|
3223
3263
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3224
|
-
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) +
|
3264
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
|
3225
3265
|
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3226
3266
|
for i in range(0, len(work_mode_timed)):
|
3227
3267
|
# get work mode
|
@@ -3233,19 +3273,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3233
3273
|
work_mode_timed[i]['charge'] = charge_power * duration
|
3234
3274
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
3235
3275
|
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
3236
|
-
|
3237
|
-
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
3276
|
+
work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
|
3238
3277
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
3239
|
-
discharge_timed[i] =
|
3240
|
-
|
3241
|
-
work_mode_timed[i]['hold'] = 1
|
3278
|
+
discharge_timed[i] = operating_loss
|
3279
|
+
work_mode_timed[i]['hold'] = 1
|
3242
3280
|
elif timed_mode > 0 and work_mode == 'Backup':
|
3243
|
-
discharge_timed[i] =
|
3281
|
+
discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
|
3244
3282
|
elif timed_mode > 0 and work_mode == 'Feedin':
|
3245
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3283
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3246
3284
|
0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
|
3247
3285
|
else: # work_mode == 'SelfUse'
|
3248
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3286
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3249
3287
|
0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
|
3250
3288
|
work_mode_timed[i]['pv'] = charge_timed[i]
|
3251
3289
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
@@ -3258,8 +3296,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3258
3296
|
kwh_contingency = consumption * contingency / 100
|
3259
3297
|
kwh_needed = reserve + kwh_contingency - kwh_min
|
3260
3298
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
3261
|
-
|
3262
|
-
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
3299
|
+
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends (without charging)
|
3263
3300
|
target_soc = charge_config.get('target_soc')
|
3264
3301
|
target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
3265
3302
|
if target_kwh > (end_residual + kwh_needed):
|
@@ -3278,7 +3315,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3278
3315
|
hours = 0.0
|
3279
3316
|
start_timed = time_to_end
|
3280
3317
|
end_timed = time_to_end
|
3281
|
-
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3282
3318
|
else:
|
3283
3319
|
# work out time to add kwh_needed to battery
|
3284
3320
|
charge_rate = charge_power * charge_loss
|
@@ -3289,30 +3325,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3289
3325
|
charge_message = "with charge added"
|
3290
3326
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3291
3327
|
# check if charge time exceeded or charge needed exceeds capacity
|
3292
|
-
hours_to_full = (capacity -
|
3293
|
-
if hours > charge_time:
|
3328
|
+
hours_to_full = (capacity - end_residual) / charge_rate
|
3329
|
+
if hours > charge_time or bat_hold == 2:
|
3294
3330
|
hours = charge_time
|
3295
3331
|
elif hours > hours_to_full:
|
3296
|
-
kwh_shortfall = kwh_needed - (capacity -
|
3332
|
+
kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
|
3297
3333
|
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
3298
3334
|
hours = required if required > hours and required < charge_time else charge_time
|
3299
|
-
# round charge time
|
3335
|
+
# round charge time
|
3300
3336
|
min_hours = charge_config['min_hours']
|
3301
3337
|
hours = int(hours / min_hours + 0.99) * min_hours
|
3302
|
-
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3303
|
-
kwh_added += discharge_rate * hours # discharge saved by charging
|
3304
|
-
kwh_spare = kwh_min - reserve + kwh_added
|
3305
3338
|
# rework charge and discharge
|
3306
3339
|
charge_period = get_best_charge_period(start_at, hours)
|
3307
|
-
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else
|
3340
|
+
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
|
3308
3341
|
price = charge_period.get('price') if charge_period is not None else None
|
3309
3342
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3310
3343
|
end_timed = start_timed + hours * steps_per_hour
|
3311
3344
|
start_residual = interpolate(start_timed, bat_timed)
|
3312
|
-
|
3313
|
-
|
3314
|
-
|
3315
|
-
|
3345
|
+
start_soc = start_residual / capacity * 100
|
3346
|
+
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3347
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3348
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3349
|
+
output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3350
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
|
3351
|
+
+ (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
|
3316
3352
|
for i in range(int(time_to_start), int(time_to_end)):
|
3317
3353
|
j = i + 1
|
3318
3354
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3335,8 +3371,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3335
3371
|
# rebuild the battery residual with the charge added and min_soc
|
3336
3372
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3337
3373
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3374
|
+
end_soc = end_residual / capacity * 100
|
3338
3375
|
# show the results
|
3339
|
-
output(f" End SoC: {
|
3376
|
+
output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3340
3377
|
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3341
3378
|
if not charge_today:
|
3342
3379
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
@@ -4304,17 +4341,9 @@ class Solcast :
|
|
4304
4341
|
total_actual = None
|
4305
4342
|
self.actual = get_history('day', d=day, v=v)
|
4306
4343
|
plots = {}
|
4344
|
+
times = [i/2 for i in range(0, 48)]
|
4307
4345
|
for v in self.actual:
|
4308
|
-
|
4309
|
-
actual_values = []
|
4310
|
-
average = 0.0
|
4311
|
-
for i in range(0, len(v.get('data'))):
|
4312
|
-
average += v['data'][i]['value'] / 6
|
4313
|
-
if i % 6 == 5:
|
4314
|
-
times.append(round_time((i - 5) / 12))
|
4315
|
-
actual_values.append(average)
|
4316
|
-
average = 0
|
4317
|
-
plots[v['variable']] = actual_values
|
4346
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4318
4347
|
if v['variable'] == 'pvPower':
|
4319
4348
|
total_actual = v.get('kwh')
|
4320
4349
|
if total_actual is None:
|
@@ -4340,16 +4369,16 @@ class Solcast :
|
|
4340
4369
|
estimate_values = [self.estimate[r][hours_time(t)] for t in times]
|
4341
4370
|
plots[r] = estimate_values
|
4342
4371
|
total_forecast = 0.0
|
4343
|
-
if self.daily.get(day) is not None:
|
4372
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4344
4373
|
sun_times = get_suntimes(day)
|
4345
4374
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4346
4375
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
4347
4376
|
total_forecast = sum(forecast_values) / 2
|
4348
4377
|
plots['forecast'] = forecast_values
|
4349
|
-
if total_actual is not None:
|
4350
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4351
4378
|
if total_forecast is not None:
|
4352
4379
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4380
|
+
if total_actual is not None:
|
4381
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4353
4382
|
print()
|
4354
4383
|
title = f"Forecast / Actual PV Power on {day}"
|
4355
4384
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4637,17 +4666,9 @@ class Solar :
|
|
4637
4666
|
total_actual = None
|
4638
4667
|
self.actual = get_history('day', d=day, v=v)
|
4639
4668
|
plots = {}
|
4669
|
+
times = [i/2 for i in range(0, 48)]
|
4640
4670
|
for v in self.actual:
|
4641
|
-
|
4642
|
-
actual_values = []
|
4643
|
-
average = 0.0
|
4644
|
-
for i in range(0, len(v.get('data'))):
|
4645
|
-
average += v['data'][i]['value'] / 6
|
4646
|
-
if i % 6 == 5:
|
4647
|
-
times.append(round_time((i - 5) / 12))
|
4648
|
-
actual_values.append(average)
|
4649
|
-
average = 0
|
4650
|
-
plots[v['variable']] = actual_values
|
4671
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4651
4672
|
if v['variable'] == 'pvPower':
|
4652
4673
|
total_actual = v.get('kwh')
|
4653
4674
|
if total_actual is None:
|
@@ -4671,16 +4692,16 @@ class Solar :
|
|
4671
4692
|
estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
|
4672
4693
|
plots[r] = estimate_values
|
4673
4694
|
total_forecast = 0.0
|
4674
|
-
if self.daily.get(day) is not None:
|
4695
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4675
4696
|
sun_times = get_suntimes(day)
|
4676
4697
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4677
4698
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4678
4699
|
total_forecast = sum(forecast_values) / 2
|
4679
4700
|
plots['forecast'] = forecast_values
|
4680
|
-
if total_actual is not None:
|
4681
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4682
4701
|
if total_forecast is not None:
|
4683
4702
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4703
|
+
if total_actual is not None:
|
4704
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4684
4705
|
print()
|
4685
4706
|
title = f"Forecast / Actual PV Power on {day}"
|
4686
4707
|
plt.figure(figsize=(figure_width, figure_width/3))
|
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: 07 November 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.
|
13
|
+
version = "2.7.1"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -671,10 +671,10 @@ def time_period(t, n):
|
|
671
671
|
(enable, start, end) = (t['enable1'], t['startTime1'], t['endTime1']) if n == 1 else (t['enable2'], t['startTime2'], t['endTime2'])
|
672
672
|
result = f"{start['hour']:02d}:{start['minute']:02d}-{end['hour']:02d}:{end['minute']:02d}"
|
673
673
|
if start['hour'] != end['hour'] or start['minute'] != end['minute']:
|
674
|
-
result += f" Charge from grid" if enable else f"
|
674
|
+
result += f" Charge from grid" if enable else f" Battery Hold"
|
675
675
|
return result
|
676
676
|
|
677
|
-
def set_charge(ch1=
|
677
|
+
def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force = 0, enable=1):
|
678
678
|
global device_sn, battery_settings, debug_setting, time_period_vars
|
679
679
|
if get_device() is None:
|
680
680
|
return None
|
@@ -1448,9 +1448,7 @@ get_raw = get_history
|
|
1448
1448
|
def report_value_profile(result):
|
1449
1449
|
if type(result) is not list or result[0]['type'] != 'day':
|
1450
1450
|
return (None, None)
|
1451
|
-
data = []
|
1452
|
-
for h in range(0,24):
|
1453
|
-
data.append((0.0, 0)) # value sum, count of values
|
1451
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1454
1452
|
totals = 0
|
1455
1453
|
n = 0
|
1456
1454
|
for day in result:
|
@@ -1477,6 +1475,30 @@ def report_value_profile(result):
|
|
1477
1475
|
result.append(by_hour[t] * daily_average / current_total)
|
1478
1476
|
return (daily_average, result)
|
1479
1477
|
|
1478
|
+
# rescale history data based on time and steps
|
1479
|
+
def rescale_history(data, steps):
|
1480
|
+
if data is None:
|
1481
|
+
return None
|
1482
|
+
result = [None for i in range(0, 24 * steps)]
|
1483
|
+
bst = 1 if 'BST' in data[0]['time'] else 0
|
1484
|
+
average = 0.0
|
1485
|
+
n = 0
|
1486
|
+
i = 0
|
1487
|
+
for d in data:
|
1488
|
+
h = round_time(time_hours(d['time'][11:]) + bst)
|
1489
|
+
new_i = int(h * steps)
|
1490
|
+
if new_i != i and i < len(result):
|
1491
|
+
result[i] = average / n if n > 0 else None
|
1492
|
+
average = 0.0
|
1493
|
+
n = 0
|
1494
|
+
i = new_i
|
1495
|
+
if d['value'] is not None:
|
1496
|
+
average += d['value']
|
1497
|
+
n += 1
|
1498
|
+
if n > 0 and i < len(result):
|
1499
|
+
result[i] = average / n
|
1500
|
+
return result
|
1501
|
+
|
1480
1502
|
|
1481
1503
|
##################################################################################################
|
1482
1504
|
# get production report in kWh
|
@@ -2417,8 +2439,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2417
2439
|
strategy = get_strategy(timed_mode=timed_mode)
|
2418
2440
|
for i in range(0, run_time):
|
2419
2441
|
h = time_line[i]
|
2420
|
-
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2421
|
-
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2442
|
+
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2443
|
+
'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2422
2444
|
if strategy is not None:
|
2423
2445
|
period['mode'] = 'SelfUse'
|
2424
2446
|
for d in strategy:
|
@@ -2444,41 +2466,55 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2444
2466
|
global charge_config, steps_per_hour
|
2445
2467
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2446
2468
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2447
|
-
charge_loss = charge_config['
|
2448
|
-
discharge_loss = charge_config['
|
2469
|
+
charge_loss = charge_config['_charge_loss']
|
2470
|
+
discharge_loss = charge_config['_discharge_loss']
|
2449
2471
|
charge_limit = charge_config['charge_limit']
|
2450
2472
|
float_charge = charge_config['float_charge']
|
2451
2473
|
run_time = len(work_mode_timed)
|
2452
2474
|
for i in range(0, run_time):
|
2453
2475
|
w = work_mode_timed[i]
|
2454
2476
|
w['kwh'] = kwh_current
|
2477
|
+
kwh_next = kwh_current
|
2455
2478
|
max_now = w['max_soc'] * capacity / 100
|
2456
|
-
|
2457
|
-
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2458
|
-
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2459
|
-
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2460
|
-
if kwh_current > capacity:
|
2461
|
-
# battery is full
|
2462
|
-
kwh_current = capacity
|
2463
|
-
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2464
|
-
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2479
|
+
min_soc_now = w['min_soc']
|
2465
2480
|
reserve_now = capacity * min_soc_now / 100
|
2466
|
-
|
2481
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2482
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2483
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2484
|
+
# charge from grid or force charge
|
2485
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2486
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2487
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2488
|
+
# force discharge
|
2489
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2490
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2491
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2492
|
+
else:
|
2493
|
+
# normal discharge
|
2494
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2495
|
+
if kwh_next > capacity:
|
2496
|
+
# battery is full
|
2497
|
+
kwh_next = capacity
|
2498
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2467
2499
|
# battery is empty, check if charge is needed
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2500
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2501
|
+
kwh_next = reserve_now
|
2502
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2471
2503
|
if reserve_drain <= reserve_limit:
|
2504
|
+
# float charge
|
2472
2505
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2506
|
+
kwh_next = reserve_drain
|
2473
2507
|
else:
|
2474
2508
|
# BMS power drain
|
2509
|
+
kwh_next = reserve_drain
|
2475
2510
|
reserve_drain -= bms_loss / steps_per_hour
|
2476
2511
|
else:
|
2477
2512
|
# reset drain level
|
2478
2513
|
reserve_drain = reserve_now
|
2479
|
-
if kwh_min is not None and
|
2480
|
-
kwh_min =
|
2481
|
-
|
2514
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2515
|
+
kwh_min = kwh_next
|
2516
|
+
kwh_current = kwh_next
|
2517
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2482
2518
|
|
2483
2519
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2484
2520
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -2538,6 +2574,8 @@ charge_config = {
|
|
2538
2574
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2539
2575
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2540
2576
|
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2577
|
+
'charge_loss': None, # loss converting charge energy to stored energy
|
2578
|
+
'discharge_loss': None, # loss converting stored energy to discharge energy
|
2541
2579
|
'inverter_power': 101, # Inverter power consumption in W
|
2542
2580
|
'bms_power': 50, # BMS power consumption in W
|
2543
2581
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2684,14 +2722,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2684
2722
|
output(f"full_charge = {full_charge}")
|
2685
2723
|
if test_soc is not None:
|
2686
2724
|
current_soc = test_soc
|
2687
|
-
capacity = 14.
|
2725
|
+
capacity = 14.46
|
2688
2726
|
residual = test_soc * capacity / 100
|
2689
2727
|
bat_volt = 317.4
|
2690
2728
|
bat_power = 0.0
|
2691
2729
|
temperature = 30
|
2692
2730
|
bms_charge_current = 15
|
2693
|
-
charge_loss = battery_params[2]['charge_loss']
|
2694
|
-
discharge_loss = battery_params[2]['discharge_loss']
|
2731
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
|
2732
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
|
2695
2733
|
bat_current = 0.0
|
2696
2734
|
device_power = 6.0
|
2697
2735
|
device_current = 35
|
@@ -2713,8 +2751,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2713
2751
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2714
2752
|
return None
|
2715
2753
|
bms_charge_current = battery.get('charge_rate')
|
2716
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2717
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2754
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2755
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2718
2756
|
device_power = device.get('power')
|
2719
2757
|
device_current = device.get('max_charge_current')
|
2720
2758
|
model = device.get('deviceType')
|
@@ -2743,6 +2781,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2743
2781
|
output(f" Max Charge: {charge_current:.1f}A")
|
2744
2782
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2745
2783
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2784
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
|
2746
2785
|
# charge current may be derated based on temperature
|
2747
2786
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2748
2787
|
if charge_current > bms_charge_current:
|
@@ -2775,8 +2814,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2775
2814
|
charge_config['charge_limit'] = charge_limit
|
2776
2815
|
charge_config['charge_power'] = charge_power
|
2777
2816
|
charge_config['float_charge'] = float_charge
|
2778
|
-
charge_config['
|
2779
|
-
charge_config['
|
2817
|
+
charge_config['_charge_loss'] = charge_loss
|
2818
|
+
charge_config['_discharge_loss'] = discharge_loss
|
2780
2819
|
# display what we have
|
2781
2820
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2782
2821
|
output(f"\nDevice Info:")
|
@@ -2837,27 +2876,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2837
2876
|
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
2838
2877
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2839
2878
|
output(f"\nNo forecasts available at this time")
|
2840
|
-
# get generation data
|
2841
|
-
generation = None
|
2842
|
-
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
2843
|
-
gen_days = charge_config['generation_days']
|
2844
|
-
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
2845
|
-
pv_history = {}
|
2846
|
-
if history is not None and len(history) > 0:
|
2847
|
-
for day in history:
|
2848
|
-
date = day['date']
|
2849
|
-
if pv_history.get(date) is None:
|
2850
|
-
pv_history[date] = 0.0
|
2851
|
-
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
2852
|
-
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
2853
|
-
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
2854
|
-
output(f"\nGeneration (kWh):")
|
2855
|
-
s = ""
|
2856
|
-
for d in sorted(pv_history.keys())[-gen_days:]:
|
2857
|
-
s += f" {d}: {pv_history[d]:4.1f},"
|
2858
|
-
output(' ' + s[:-1])
|
2859
|
-
generation = pv_sum / gen_days
|
2860
|
-
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2861
2879
|
# choose expected value and produce generation time line
|
2862
2880
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
2863
2881
|
sun_name = seasonal_sun[quarter]['name']
|
@@ -2875,11 +2893,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2875
2893
|
elif solar_value is not None:
|
2876
2894
|
expected = solar_value
|
2877
2895
|
generation_timed = solar_timed
|
2878
|
-
elif generation is None or generation == 0.0:
|
2879
|
-
output(f"\nNo generation data available")
|
2880
|
-
output_close()
|
2881
|
-
return None
|
2882
2896
|
else:
|
2897
|
+
# no forecast, use generation data
|
2898
|
+
generation = None
|
2899
|
+
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
2900
|
+
gen_days = charge_config['generation_days']
|
2901
|
+
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
2902
|
+
pv_history = {}
|
2903
|
+
if history is not None and len(history) > 0:
|
2904
|
+
for day in history:
|
2905
|
+
date = day['date']
|
2906
|
+
if pv_history.get(date) is None:
|
2907
|
+
pv_history[date] = 0.0
|
2908
|
+
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
2909
|
+
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
2910
|
+
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
2911
|
+
output(f"\nGeneration (kWh):")
|
2912
|
+
s = ""
|
2913
|
+
for d in sorted(pv_history.keys())[-gen_days:]:
|
2914
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
2915
|
+
output(' ' + s[:-1])
|
2916
|
+
generation = pv_sum / gen_days
|
2917
|
+
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2918
|
+
if generation is None or generation == 0.0:
|
2919
|
+
output(f"\nNo generation data available")
|
2920
|
+
output_close()
|
2921
|
+
return None
|
2883
2922
|
expected = generation
|
2884
2923
|
generation_timed = [expected * x / sun_sum for x in sun_timed]
|
2885
2924
|
if charge_config['forecast_selection'] == 1 and update_settings > 0:
|
@@ -2887,7 +2926,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2887
2926
|
update_settings = 0
|
2888
2927
|
# produce time lines for charge, discharge and work mode
|
2889
2928
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2890
|
-
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) +
|
2929
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
|
2891
2930
|
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2892
2931
|
for i in range(0, len(work_mode_timed)):
|
2893
2932
|
# get work mode
|
@@ -2899,19 +2938,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2899
2938
|
work_mode_timed[i]['charge'] = charge_power * duration
|
2900
2939
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
2901
2940
|
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
2902
|
-
|
2903
|
-
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
2941
|
+
work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
|
2904
2942
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
2905
|
-
discharge_timed[i] =
|
2906
|
-
|
2907
|
-
work_mode_timed[i]['hold'] = 1
|
2943
|
+
discharge_timed[i] = operating_loss
|
2944
|
+
work_mode_timed[i]['hold'] = 1
|
2908
2945
|
elif timed_mode > 0 and work_mode == 'Backup':
|
2909
|
-
discharge_timed[i] =
|
2946
|
+
discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
|
2910
2947
|
elif timed_mode > 0 and work_mode == 'Feedin':
|
2911
|
-
(discharge_timed[i], charge_timed[i]) = (
|
2948
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
2912
2949
|
0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
|
2913
2950
|
else: # work_mode == 'SelfUse'
|
2914
|
-
(discharge_timed[i], charge_timed[i]) = (
|
2951
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
2915
2952
|
0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
|
2916
2953
|
work_mode_timed[i]['pv'] = charge_timed[i]
|
2917
2954
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
@@ -2924,7 +2961,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2924
2961
|
kwh_contingency = consumption * contingency / 100
|
2925
2962
|
kwh_needed = reserve + kwh_contingency - kwh_min
|
2926
2963
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
2927
|
-
start_soc = int(start_residual / capacity * 100 + 0.5)
|
2928
2964
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
2929
2965
|
target_soc = charge_config.get('target_soc')
|
2930
2966
|
target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
@@ -2944,7 +2980,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2944
2980
|
hours = 0.0
|
2945
2981
|
start_timed = time_to_end
|
2946
2982
|
end_timed = time_to_end
|
2947
|
-
end_soc = int(end_residual / capacity * 100 + 0.5)
|
2948
2983
|
else:
|
2949
2984
|
# work out time to add kwh_needed to battery
|
2950
2985
|
charge_rate = charge_power * charge_loss
|
@@ -2955,30 +2990,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2955
2990
|
charge_message = "with charge added"
|
2956
2991
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2957
2992
|
# check if charge time exceeded or charge needed exceeds capacity
|
2958
|
-
hours_to_full = (capacity -
|
2959
|
-
if hours > charge_time:
|
2993
|
+
hours_to_full = (capacity - end_residual) / charge_rate
|
2994
|
+
if hours > charge_time or bat_hold == 2:
|
2960
2995
|
hours = charge_time
|
2961
2996
|
elif hours > hours_to_full:
|
2962
|
-
kwh_shortfall = kwh_needed - (capacity -
|
2997
|
+
kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
|
2963
2998
|
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
2964
2999
|
hours = required if required > hours and required < charge_time else charge_time
|
2965
|
-
# round charge time
|
3000
|
+
# round charge time
|
2966
3001
|
min_hours = charge_config['min_hours']
|
2967
3002
|
hours = int(hours / min_hours + 0.99) * min_hours
|
2968
|
-
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
2969
|
-
kwh_added += discharge_rate * hours # discharge saved during charging
|
2970
|
-
kwh_spare = kwh_min - reserve + kwh_added
|
2971
3003
|
# rework charge and discharge
|
2972
3004
|
charge_period = get_best_charge_period(start_at, hours)
|
2973
|
-
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else
|
3005
|
+
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
|
2974
3006
|
price = charge_period.get('price') if charge_period is not None else None
|
2975
3007
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
2976
3008
|
end_timed = start_timed + hours * steps_per_hour
|
2977
3009
|
start_residual = interpolate(start_timed, bat_timed)
|
2978
|
-
|
2979
|
-
|
2980
|
-
|
2981
|
-
|
3010
|
+
start_soc = start_residual / capacity * 100
|
3011
|
+
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3012
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3013
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3014
|
+
output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3015
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
|
3016
|
+
+ (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
|
2982
3017
|
for i in range(int(time_to_start), int(time_to_end)):
|
2983
3018
|
j = i + 1
|
2984
3019
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3001,8 +3036,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3001
3036
|
# rebuild the battery residual with any charge added and min_soc
|
3002
3037
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3003
3038
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3039
|
+
end_soc = end_residual / capacity * 100
|
3004
3040
|
# show the results
|
3005
|
-
output(f" End SoC: {
|
3041
|
+
output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3006
3042
|
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3007
3043
|
if not charge_today:
|
3008
3044
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
@@ -3967,17 +4003,9 @@ class Solcast :
|
|
3967
4003
|
total_actual = None
|
3968
4004
|
self.actual = get_history('day', d=day, v=v)
|
3969
4005
|
plots = {}
|
4006
|
+
times = [i/2 for i in range(0, 48)]
|
3970
4007
|
for v in self.actual:
|
3971
|
-
|
3972
|
-
actual_values = []
|
3973
|
-
average = 0.0
|
3974
|
-
for i in range(0, len(v.get('data'))):
|
3975
|
-
average += v['data'][i]['value'] / 6
|
3976
|
-
if i % 6 == 5:
|
3977
|
-
times.append(round_time((i - 5) / 12))
|
3978
|
-
actual_values.append(average)
|
3979
|
-
average = 0
|
3980
|
-
plots[v['variable']] = actual_values
|
4008
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
3981
4009
|
if v['variable'] == 'pvPower':
|
3982
4010
|
total_actual = v.get('kwh')
|
3983
4011
|
if total_actual is None:
|
@@ -4003,16 +4031,16 @@ class Solcast :
|
|
4003
4031
|
estimate_values = [self.estimate[r][hours_time(t)] for t in times]
|
4004
4032
|
plots[r] = estimate_values
|
4005
4033
|
total_forecast = 0.0
|
4006
|
-
if self.daily.get(day) is not None:
|
4034
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4007
4035
|
sun_times = get_suntimes(day)
|
4008
4036
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4009
4037
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
4010
4038
|
total_forecast = sum(forecast_values) / 2
|
4011
4039
|
plots['forecast'] = forecast_values
|
4012
|
-
if total_actual is not None:
|
4013
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4014
4040
|
if total_forecast is not None:
|
4015
4041
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4042
|
+
if total_actual is not None:
|
4043
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4016
4044
|
print()
|
4017
4045
|
title = f"Forecast / Actual PV Power on {day}"
|
4018
4046
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4300,17 +4328,9 @@ class Solar :
|
|
4300
4328
|
total_actual = None
|
4301
4329
|
self.actual = get_history('day', d=day, v=v)
|
4302
4330
|
plots = {}
|
4331
|
+
times = [i/2 for i in range(0, 48)]
|
4303
4332
|
for v in self.actual:
|
4304
|
-
|
4305
|
-
actual_values = []
|
4306
|
-
average = 0.0
|
4307
|
-
for i in range(0, len(v.get('data'))):
|
4308
|
-
average += v['data'][i]['value'] / 6
|
4309
|
-
if i % 6 == 5:
|
4310
|
-
times.append(round_time((i - 5) / 12))
|
4311
|
-
actual_values.append(average)
|
4312
|
-
average = 0
|
4313
|
-
plots[v['variable']] = actual_values
|
4333
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4314
4334
|
if v['variable'] == 'pvPower':
|
4315
4335
|
total_actual = v.get('kwh')
|
4316
4336
|
if total_actual is None:
|
@@ -4334,16 +4354,16 @@ class Solar :
|
|
4334
4354
|
estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
|
4335
4355
|
plots[r] = estimate_values
|
4336
4356
|
total_forecast = 0.0
|
4337
|
-
if self.daily.get(day) is not None:
|
4357
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4338
4358
|
sun_times = get_suntimes(day)
|
4339
4359
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4340
4360
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4341
4361
|
total_forecast = sum(forecast_values) / 2
|
4342
4362
|
plots['forecast'] = forecast_values
|
4343
|
-
if total_actual is not None:
|
4344
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4345
4363
|
if total_forecast is not None:
|
4346
4364
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4365
|
+
if total_actual is not None:
|
4366
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4347
4367
|
print()
|
4348
4368
|
title = f"Forecast / Actual PV Power on {day}"
|
4349
4369
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.1
|
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
|
@@ -158,12 +158,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
|
|
158
158
|
+ minSoc: min Soc setting e.g. 10 = 10%
|
159
159
|
|
160
160
|
set_charge() takes the charge times from the battery_settings and applies these to the inverter. The parameters are optional and will update battery_settings. You should specify all 3 parameter for a time period:
|
161
|
-
+ ch1: enable charge from grid for period 1 (True
|
162
|
-
+ st1: the start time for period 1
|
163
|
-
+ en1: the end time for period 1
|
164
|
-
+ ch2: enable charge from grid for period 2 (True
|
165
|
-
+ st2: the start time for period 2
|
166
|
-
+ en2: the end time for period 2
|
161
|
+
+ ch1: enable charge from grid for period 1 (default True)
|
162
|
+
+ st1: the start time for period 1 (default 0)
|
163
|
+
+ en1: the end time for period 1 (default 0)
|
164
|
+
+ ch2: enable charge from grid for period 2 (default True)
|
165
|
+
+ st2: the start time for period 2 (default 0)
|
166
|
+
+ en2: the end time for period 2 (default 0)
|
167
167
|
+ enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
|
168
168
|
|
169
169
|
set_period() returns a period structure that can be used to build a list for set_schedule()
|
@@ -341,7 +341,7 @@ The previous section provides functions that can be used to access and control y
|
|
341
341
|
Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
|
342
342
|
|
343
343
|
```
|
344
|
-
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
|
344
|
+
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
|
345
345
|
```
|
346
346
|
|
347
347
|
All the parameters are optional:
|
@@ -352,6 +352,7 @@ All the parameters are optional:
|
|
352
352
|
+ update_settings: 0 no changes, 1 update charge settings. The default is 0
|
353
353
|
+ show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
|
354
354
|
+ show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
|
355
|
+
+ timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
|
355
356
|
|
356
357
|
### Modelling
|
357
358
|
|
@@ -394,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
394
395
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
395
396
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
396
397
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
398
|
+
charge_loss: None # loss converting charge energy to stored energy
|
399
|
+
discharge_loss: None # loss converting stored energy to discharge energy
|
397
400
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
398
401
|
bms_power: 50 # BMS power consumption in W
|
399
402
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -804,6 +807,21 @@ This setting can be:
|
|
804
807
|
|
805
808
|
# Version Info
|
806
809
|
|
810
|
+
2.7.1<br>
|
811
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
812
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
813
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
814
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
815
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
816
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
817
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
818
|
+
|
819
|
+
|
820
|
+
2.7.0<br>
|
821
|
+
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
822
|
+
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
823
|
+
Correct problem with missing periods of actual data in forecast.compare()
|
824
|
+
|
807
825
|
2.6.9<br>
|
808
826
|
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
809
827
|
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=rV8qX1sJyXEvmyie8kvJSFVd3sU-a-9Zcy-i-ag_gTQ,222996
|
2
|
+
foxesscloud/openapi.py,sha256=0oxiSNedH_9UnSDQmNaeXgPIaTvwqzg3fa1Dupdi7yQ,206923
|
3
|
+
foxesscloud-2.7.1.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.7.1.dist-info/METADATA,sha256=Bl4xmInDDpLjnt1pp-Zn_N8dkBzxncEPlq-HPB2ZHts,61833
|
5
|
+
foxesscloud-2.7.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.7.1.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.7.1.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=2rBhstr0ZH9PxvCipZhxf79JN7xQ_CqQc_xRyy-0Yd4,221818
|
2
|
-
foxesscloud/openapi.py,sha256=NncnwOSohoTTVWLELZWfkXnD5Peav6gl8aq2tRGsizk,205719
|
3
|
-
foxesscloud-2.6.9.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.6.9.dist-info/METADATA,sha256=NfNht53QGiO5BHMY-SMM6zo49qPS1HQHE4yKo3O_mLI,60502
|
5
|
-
foxesscloud-2.6.9.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.6.9.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.6.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|