foxesscloud 2.6.1__py3-none-any.whl → 2.6.3__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 +67 -39
- foxesscloud/openapi.py +37 -29
- {foxesscloud-2.6.1.dist-info → foxesscloud-2.6.3.dist-info}/METADATA +10 -1
- foxesscloud-2.6.3.dist-info/RECORD +7 -0
- foxesscloud-2.6.1.dist-info/RECORD +0 -7
- {foxesscloud-2.6.1.dist-info → foxesscloud-2.6.3.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.1.dist-info → foxesscloud-2.6.3.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.1.dist-info → foxesscloud-2.6.3.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: 13 October 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.7.
|
13
|
+
version = "1.7.5"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -65,7 +65,7 @@ def plot_show():
|
|
65
65
|
##################################################################################################
|
66
66
|
##################################################################################################
|
67
67
|
|
68
|
-
def
|
68
|
+
def convert_date(d):
|
69
69
|
if d is not None and len(d) < 18:
|
70
70
|
if len(d) == 10:
|
71
71
|
d += ' 00:00:00'
|
@@ -76,8 +76,12 @@ def query_date(d, offset = None):
|
|
76
76
|
try:
|
77
77
|
t = datetime.now() if d is None else datetime.strptime(d, "%Y-%m-%d %H:%M:%S")
|
78
78
|
except Exception as e:
|
79
|
-
output(f"**
|
79
|
+
output(f"** convert_date(): {str(e)}")
|
80
80
|
return None
|
81
|
+
return t
|
82
|
+
|
83
|
+
def query_date(d, offset = None):
|
84
|
+
t = convert_date(d)
|
81
85
|
if offset is not None:
|
82
86
|
t += timedelta(days = offset)
|
83
87
|
return {'year': t.year, 'month': t.month, 'day': t.day, 'hour': t.hour, 'minute': t.minute, 'second': t.second}
|
@@ -594,8 +598,8 @@ battery_params = {
|
|
594
598
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
595
599
|
'step': 5,
|
596
600
|
'offset': 5,
|
597
|
-
'charge_loss': 1.
|
598
|
-
'discharge_loss': 0.
|
601
|
+
'charge_loss': 1.08,
|
602
|
+
'discharge_loss': 0.85},
|
599
603
|
}
|
600
604
|
|
601
605
|
def get_battery(info=1):
|
@@ -628,11 +632,15 @@ def get_battery(info=1):
|
|
628
632
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
629
633
|
else:
|
630
634
|
battery['info'] = result['batteries'][0]
|
631
|
-
if battery['info']['masterVersion'] >= '1.014':
|
635
|
+
if battery['info']['masterVersion'] >= '1.014' and battery['info']['masterSN'][:7] == '60BBHV2':
|
632
636
|
residual_handling = 2
|
637
|
+
battery['residual_handling'] = residual_handling
|
638
|
+
battery['rated_capacity'] = None
|
639
|
+
battery['soh'] = None
|
640
|
+
battery['soh_supported'] = False
|
633
641
|
if battery.get('residual') is not None:
|
634
642
|
battery['residual'] /= 1000
|
635
|
-
if residual_handling == 2:
|
643
|
+
if battery['residual_handling'] == 2:
|
636
644
|
capacity = battery.get('residual')
|
637
645
|
soc = battery.get('soc')
|
638
646
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
@@ -643,7 +651,7 @@ def get_battery(info=1):
|
|
643
651
|
battery['capacity'] = round(capacity, 3)
|
644
652
|
battery['residual'] = round(residual, 3)
|
645
653
|
battery['charge_rate'] = 50
|
646
|
-
params = battery_params[residual_handling]
|
654
|
+
params = battery_params[battery['residual_handling']]
|
647
655
|
battery['charge_loss'] = params['charge_loss']
|
648
656
|
battery['discharge_loss'] = params['discharge_loss']
|
649
657
|
if battery.get('temperature') is not None:
|
@@ -679,20 +687,38 @@ def get_batteries(info=1):
|
|
679
687
|
for i in range(0, len(batteries)):
|
680
688
|
batteries[i]['info'] = result['batteries'][i]
|
681
689
|
for b in batteries:
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
690
|
+
b['residual_handling'] = residual_handling
|
691
|
+
if b.get('info') is not None and b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
|
692
|
+
b['residual_handling'] = 2
|
693
|
+
if b.get('soh') is not None and b['soh'].isnumeric():
|
694
|
+
b['soh'] = int(b['soh'])
|
695
|
+
b['soh_supported'] = True
|
696
|
+
else:
|
697
|
+
b['rated_capacity'] = None
|
698
|
+
b['soh'] = None
|
699
|
+
b['soh_supported'] = False
|
700
|
+
for i, b in enumerate(batteries):
|
701
|
+
if i == 0:
|
702
|
+
residual_handling = b['residual_handling']
|
703
|
+
get_battery(info=0)
|
704
|
+
battery['ratedCapacity'] = b.get('ratedCapacity')
|
705
|
+
b['capacity'] = battery.get('capacity')
|
706
|
+
b['residual'] = battery.get('residual')
|
707
|
+
b['soc'] = battery.get('soc')
|
708
|
+
if b.get('capacity') is None and b.get('soh') is not None:
|
709
|
+
capacity = (b['ratedCapacity'] / 1000 * b['soh'] / 100)
|
710
|
+
soc = b.get('soc')
|
711
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
712
|
+
b['capacity'] = round(capacity, 3)
|
713
|
+
b['residual'] = round(residual, 3)
|
714
|
+
if b.get('capacity') is not None and b.get('ratedCapacity') is not None:
|
715
|
+
b['soh'] = round(b['capacity'] / b['ratedCapacity'] * 1000 * 100, 1)
|
689
716
|
b['charge_rate'] = 50
|
690
|
-
params = battery_params[residual_handling]
|
717
|
+
params = battery_params[b['residual_handling']]
|
691
718
|
b['charge_loss'] = params['charge_loss']
|
692
719
|
b['discharge_loss'] = params['discharge_loss']
|
693
720
|
if b.get('temperature') is not None:
|
694
721
|
b['charge_rate'] = params['table'][int((b['temperature'] - params['offset']) / params['step'])]
|
695
|
-
battery = batteries[0]
|
696
722
|
return batteries
|
697
723
|
|
698
724
|
##################################################################################################
|
@@ -2165,9 +2191,9 @@ def hours_difference(t1, t2):
|
|
2165
2191
|
if t1 == t2:
|
2166
2192
|
return 0.0
|
2167
2193
|
if type(t1) is str:
|
2168
|
-
t1 =
|
2194
|
+
t1 = convert_date(t1)
|
2169
2195
|
if type(t2) is str:
|
2170
|
-
t2 =
|
2196
|
+
t2 = convert_date(t2)
|
2171
2197
|
return round((t1 - t2).total_seconds() / 3600,1)
|
2172
2198
|
|
2173
2199
|
##################################################################################################
|
@@ -2331,7 +2357,7 @@ tariff_config = {
|
|
2331
2357
|
'update_time': 16.5, # time in hours when tomrow's data can be fetched
|
2332
2358
|
'weighting': None, # weights for weighted average
|
2333
2359
|
'plunge_price': [3, 3], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2334
|
-
'plunge_slots':
|
2360
|
+
'plunge_slots': 8, # number of 30 minute slots to use
|
2335
2361
|
'data_wrap': 6, # prices to show per line
|
2336
2362
|
'show_data': 1, # show pricing data
|
2337
2363
|
'show_plot': 1 # plot pricing data
|
@@ -2344,7 +2370,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2344
2370
|
if d is not None and len(d) < 11:
|
2345
2371
|
d += " 18:00"
|
2346
2372
|
# get dates and times
|
2347
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else
|
2373
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
|
2348
2374
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2349
2375
|
# adjust system to get local time now
|
2350
2376
|
now = system_time + timedelta(hours=time_offset)
|
@@ -2815,7 +2841,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2815
2841
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2816
2842
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2817
2843
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2818
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2844
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
|
2819
2845
|
print(f"\n---------------- charge_needed ----------------")
|
2820
2846
|
# validate parameters
|
2821
2847
|
args = locals()
|
@@ -2843,7 +2869,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2843
2869
|
if type(forecast_times) is not list:
|
2844
2870
|
forecast_times = [forecast_times]
|
2845
2871
|
# get dates and times
|
2846
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else
|
2872
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
|
2847
2873
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2848
2874
|
now = system_time + timedelta(hours=time_offset)
|
2849
2875
|
today = datetime.strftime(now, '%Y-%m-%d')
|
@@ -2916,14 +2942,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2916
2942
|
output(f"full_charge = {full_charge}")
|
2917
2943
|
if test_soc is not None:
|
2918
2944
|
current_soc = test_soc
|
2919
|
-
capacity = 14.
|
2945
|
+
capacity = 14.53
|
2920
2946
|
residual = test_soc * capacity / 100
|
2921
2947
|
bat_volt = 317.4
|
2922
2948
|
bat_power = 0.0
|
2923
2949
|
temperature = 30
|
2924
2950
|
bms_charge_current = 15
|
2925
|
-
charge_loss =
|
2926
|
-
discharge_loss =
|
2951
|
+
charge_loss = charge_rates[2]['charge_loss']
|
2952
|
+
discharge_loss = charge_rates[2]['discharge_loss']
|
2927
2953
|
bat_current = 0.0
|
2928
2954
|
device_power = 6.0
|
2929
2955
|
device_current = 35
|
@@ -2971,6 +2997,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2971
2997
|
output(f" Temperature: {temperature:.1f}°C")
|
2972
2998
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2973
2999
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
3000
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
|
2974
3001
|
# charge current may be derated based on temperature
|
2975
3002
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2976
3003
|
if charge_current > bms_charge_current:
|
@@ -3309,10 +3336,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3309
3336
|
|
3310
3337
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
3311
3338
|
global charge_config, storage
|
3312
|
-
now =
|
3339
|
+
now = convert_date(d)
|
3313
3340
|
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3314
3341
|
if save is None and charge_config.get('save') is not None:
|
3315
|
-
save = charge_config.get('save').replace('###', yesterday)
|
3342
|
+
save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
|
3316
3343
|
if not os.path.exists(storage + save):
|
3317
3344
|
save = None
|
3318
3345
|
if save is None:
|
@@ -3339,7 +3366,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3339
3366
|
base_hour = int(time_hours(base_time[11:16]))
|
3340
3367
|
start_day = base_time[:10]
|
3341
3368
|
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3342
|
-
now =
|
3369
|
+
now = convert_date(base_time)
|
3343
3370
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3344
3371
|
if v is None:
|
3345
3372
|
v = ['pvPower', 'loadsPower', 'SoC']
|
@@ -3372,13 +3399,14 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3372
3399
|
for i in range(0, run_time):
|
3373
3400
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3374
3401
|
if show_data > 0 and plots.get('SoC') is not None:
|
3375
|
-
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else
|
3376
|
-
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3402
|
+
data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
|
3403
|
+
s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
|
3377
3404
|
h = base_hour
|
3378
3405
|
t = 0
|
3379
3406
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3380
3407
|
col = h % data_wrap
|
3381
3408
|
s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
|
3409
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3382
3410
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3383
3411
|
h += 1
|
3384
3412
|
t += steps_per_hour
|
@@ -3529,12 +3557,12 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3529
3557
|
for v in cell_temps:
|
3530
3558
|
s +=f",{v:.0f}"
|
3531
3559
|
return s
|
3532
|
-
if rated_capacity is not None:
|
3533
|
-
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3534
|
-
output(f"SoH: {bat_soh}%")
|
3535
3560
|
output(f"Current SoC: {current_soc}%")
|
3536
|
-
output(f"Capacity: {capacity:.2f}kWh")
|
3537
|
-
output(f"Residual: {residual:.2f}kWh")
|
3561
|
+
output(f"Capacity: {capacity:.2f}kWh" + (" (Residual / SoC x 100)" if bat['residual_handling'] == 1 else ""))
|
3562
|
+
output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity / 100)" if bat['residual_handling'] == 2 else ""))
|
3563
|
+
if rated_capacity is not None and bat_soh is not None:
|
3564
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3565
|
+
output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
|
3538
3566
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
3539
3567
|
output(f"InvBatCurrent: {bat_current:.1f}A")
|
3540
3568
|
output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
|
@@ -3944,7 +3972,7 @@ class Solcast :
|
|
3944
3972
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3945
3973
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3946
3974
|
self.data = {}
|
3947
|
-
now =
|
3975
|
+
now = convert_date(d)
|
3948
3976
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3949
3977
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
3950
3978
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
@@ -4288,7 +4316,7 @@ class Solar :
|
|
4288
4316
|
def __init__(self, reload=0, quiet=False, shading=None, d=None):
|
4289
4317
|
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4290
4318
|
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4291
|
-
now =
|
4319
|
+
now = convert_date(d)
|
4292
4320
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
4293
4321
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
4294
4322
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
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: 13 October 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.6.
|
13
|
+
version = "2.6.3"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -66,8 +66,7 @@ def plot_show():
|
|
66
66
|
##################################################################################################
|
67
67
|
##################################################################################################
|
68
68
|
|
69
|
-
|
70
|
-
def query_date(d, offset = None):
|
69
|
+
def convert_date(d):
|
71
70
|
if d is not None and len(d) < 18:
|
72
71
|
if len(d) == 10:
|
73
72
|
d += ' 00:00:00'
|
@@ -78,8 +77,13 @@ def query_date(d, offset = None):
|
|
78
77
|
try:
|
79
78
|
t = datetime.now() if d is None else datetime.strptime(d, "%Y-%m-%d %H:%M:%S")
|
80
79
|
except Exception as e:
|
81
|
-
output(f"**
|
80
|
+
output(f"** convert_date(): {str(e)}")
|
82
81
|
return None
|
82
|
+
return t
|
83
|
+
|
84
|
+
# return query date as a dictionary with year, month, day, hour, minute, second
|
85
|
+
def query_date(d, offset = None):
|
86
|
+
t = convert_date(d)
|
83
87
|
if offset is not None:
|
84
88
|
t += timedelta(days = offset)
|
85
89
|
return {'year': t.year, 'month': t.month, 'day': t.day, 'hour': t.hour, 'minute': t.minute, 'second': t.second}
|
@@ -94,7 +98,7 @@ def query_time(d, time_span):
|
|
94
98
|
else:
|
95
99
|
d += ':00'
|
96
100
|
try:
|
97
|
-
t = datetime.now().replace(minute=0, second=0, microsecond=0) if d is None else
|
101
|
+
t = datetime.now().replace(minute=0, second=0, microsecond=0) if d is None else convert_date(d)
|
98
102
|
except Exception as e:
|
99
103
|
output(f"** query_time(): {str(e)}")
|
100
104
|
return (None, None)
|
@@ -560,7 +564,7 @@ battery_params = {
|
|
560
564
|
'step': 5,
|
561
565
|
'offset': 5,
|
562
566
|
'charge_loss': 1.080,
|
563
|
-
'discharge_loss': 0.
|
567
|
+
'discharge_loss': 0.85},
|
564
568
|
}
|
565
569
|
|
566
570
|
def get_battery(info=0, v=None):
|
@@ -575,7 +579,11 @@ def get_battery(info=0, v=None):
|
|
575
579
|
battery = {}
|
576
580
|
for i in range(0, len(battery_vars)):
|
577
581
|
battery[battery_data[i]] = result[i].get('value')
|
578
|
-
|
582
|
+
battery['residual_handling'] = residual_handling
|
583
|
+
battery['rated_capacity'] = None
|
584
|
+
battery['soh'] = None
|
585
|
+
battery['soh_supported'] = False
|
586
|
+
if battery['residual_handling'] == 2:
|
579
587
|
capacity = battery.get('residual')
|
580
588
|
soc = battery.get('soc')
|
581
589
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
@@ -587,7 +595,7 @@ def get_battery(info=0, v=None):
|
|
587
595
|
battery['residual'] = round(residual, 3)
|
588
596
|
battery['status'] = 1
|
589
597
|
battery['charge_rate'] = 50
|
590
|
-
params = battery_params[residual_handling]
|
598
|
+
params = battery_params[battery['residual_handling']]
|
591
599
|
battery['charge_loss'] = params['charge_loss']
|
592
600
|
battery['discharge_loss'] = params['discharge_loss']
|
593
601
|
if battery.get('temperature') is not None:
|
@@ -597,8 +605,8 @@ def get_battery(info=0, v=None):
|
|
597
605
|
def get_batteries(info=0):
|
598
606
|
global battery, batteries
|
599
607
|
get_battery(info=info)
|
600
|
-
battery
|
601
|
-
|
608
|
+
if battery is None:
|
609
|
+
return None
|
602
610
|
batteries = [battery]
|
603
611
|
return batteries
|
604
612
|
|
@@ -1984,9 +1992,9 @@ def hours_difference(t1, t2):
|
|
1984
1992
|
if t1 == t2:
|
1985
1993
|
return 0.0
|
1986
1994
|
if type(t1) is str:
|
1987
|
-
t1 =
|
1995
|
+
t1 = convert_date(t1)
|
1988
1996
|
if type(t2) is str:
|
1989
|
-
t2 =
|
1997
|
+
t2 = convert_date(t2)
|
1990
1998
|
return round((t1 - t2).total_seconds() / 3600,1)
|
1991
1999
|
|
1992
2000
|
##################################################################################################
|
@@ -2150,7 +2158,7 @@ tariff_config = {
|
|
2150
2158
|
'update_time': 16.5, # time in hours when tomrow's data can be fetched
|
2151
2159
|
'weighting': None, # weights for weighted average
|
2152
2160
|
'plunge_price': [3, 3], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2153
|
-
'plunge_slots':
|
2161
|
+
'plunge_slots': 8, # number of 30 minute slots to use
|
2154
2162
|
'data_wrap': 6, # prices to show per line
|
2155
2163
|
'show_data': 1, # show pricing data
|
2156
2164
|
'show_plot': 1 # plot pricing data
|
@@ -2163,7 +2171,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2163
2171
|
if d is not None and len(d) < 11:
|
2164
2172
|
d += " 18:00"
|
2165
2173
|
# get dates and times
|
2166
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else
|
2174
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
|
2167
2175
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2168
2176
|
# adjust system to get local time now
|
2169
2177
|
now = system_time + timedelta(hours=time_offset)
|
@@ -2634,7 +2642,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2634
2642
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2635
2643
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2636
2644
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2637
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2645
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
|
2638
2646
|
print(f"\n---------------- charge_needed ----------------")
|
2639
2647
|
# validate parameters
|
2640
2648
|
args = locals()
|
@@ -2662,7 +2670,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2662
2670
|
if type(forecast_times) is not list:
|
2663
2671
|
forecast_times = [forecast_times]
|
2664
2672
|
# get dates and times
|
2665
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else
|
2673
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
|
2666
2674
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2667
2675
|
now = system_time + timedelta(hours=time_offset)
|
2668
2676
|
today = datetime.strftime(now, '%Y-%m-%d')
|
@@ -2741,8 +2749,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2741
2749
|
bat_power = 0.0
|
2742
2750
|
temperature = 30
|
2743
2751
|
bms_charge_current = 15
|
2744
|
-
charge_loss =
|
2745
|
-
discharge_loss =
|
2752
|
+
charge_loss = charge_rates[2]['charge_loss']
|
2753
|
+
discharge_loss = charge_rates[2]['discharge_loss']
|
2746
2754
|
bat_current = 0.0
|
2747
2755
|
device_power = 6.0
|
2748
2756
|
device_current = 35
|
@@ -3126,10 +3134,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3126
3134
|
|
3127
3135
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3128
3136
|
global charge_config, storage
|
3129
|
-
now =
|
3137
|
+
now = convert_date(d)
|
3130
3138
|
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3131
3139
|
if save is None and charge_config.get('save') is not None:
|
3132
|
-
save = charge_config.get('save').replace('###', yesterday)
|
3140
|
+
save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
|
3133
3141
|
if not os.path.exists(storage + save):
|
3134
3142
|
save = None
|
3135
3143
|
if save is None:
|
@@ -3156,7 +3164,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3156
3164
|
base_hour = int(time_hours(base_time[11:16]))
|
3157
3165
|
start_day = base_time[:10]
|
3158
3166
|
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3159
|
-
now =
|
3167
|
+
now = convert_date(base_time)
|
3160
3168
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3161
3169
|
if v is None:
|
3162
3170
|
v = ['pvPower', 'loadsPower', 'SoC']
|
@@ -3345,12 +3353,12 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3345
3353
|
for v in cell_temps:
|
3346
3354
|
s +=f",{v:.0f}"
|
3347
3355
|
return s
|
3348
|
-
if rated_capacity is not None:
|
3349
|
-
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3350
|
-
output(f"SoH: {bat_soh}%")
|
3351
3356
|
output(f"Current SoC: {current_soc}%")
|
3352
|
-
output(f"Capacity: {capacity:.2f}kWh")
|
3353
|
-
output(f"Residual: {residual:.2f}kWh")
|
3357
|
+
output(f"Capacity: {capacity:.2f}kWh" + (" (Residual / SoC x 100)" if bat['residual_handling'] == 1 else ""))
|
3358
|
+
output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity / 100)" if bat['residual_handling'] == 2 else ""))
|
3359
|
+
if rated_capacity is not None and bat_soh is not None:
|
3360
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3361
|
+
output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
|
3354
3362
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
3355
3363
|
output(f"InvBatCurrent: {bat_current:.1f}A")
|
3356
3364
|
output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
|
@@ -3760,7 +3768,7 @@ class Solcast :
|
|
3760
3768
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3761
3769
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3762
3770
|
self.data = {}
|
3763
|
-
now =
|
3771
|
+
now = convert_date(d)
|
3764
3772
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3765
3773
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
3766
3774
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
@@ -4104,7 +4112,7 @@ class Solar :
|
|
4104
4112
|
def __init__(self, reload=0, quiet=False, shading=None, d=None):
|
4105
4113
|
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4106
4114
|
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4107
|
-
now =
|
4115
|
+
now = convert_date(d)
|
4108
4116
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
4109
4117
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
4110
4118
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.3
|
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
|
@@ -787,6 +787,15 @@ This setting can be:
|
|
787
787
|
|
788
788
|
# Version Info
|
789
789
|
|
790
|
+
2.6.3<br>
|
791
|
+
Increase default plungs_slots from 6 to 8.
|
792
|
+
Correct battery capacity in get_batteries().
|
793
|
+
|
794
|
+
2.6.2<br>
|
795
|
+
Update battery calibration for charge_needed() when residual_handling is 2.
|
796
|
+
Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
|
797
|
+
Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
|
798
|
+
|
790
799
|
2.6.1<br>
|
791
800
|
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
792
801
|
Update calibration for Force Charge with BMS 1.014 and later.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=qhra8fiKsM3xoQCbzQYsPoY0D4mNPbpOwXP_AgpXQ9E,217489
|
2
|
+
foxesscloud/openapi.py,sha256=YwFHpTH4CyTMyf9f5ZXD0-umFL-lWnq2aQHA0XV7YuM,207518
|
3
|
+
foxesscloud-2.6.3.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.6.3.dist-info/METADATA,sha256=j3UCMUoCfv10iOf5_8VcDPA8MbCO5CpHllwYi6jSRzw,57854
|
5
|
+
foxesscloud-2.6.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.6.3.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.6.3.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=DHP0NqeFXj6rFIdwyTp7U935YBhJcFG2YMwaHGRBaF4,215964
|
2
|
-
foxesscloud/openapi.py,sha256=Rp1HdtBLpMt1iK5psUkfnCSzw7vmmABs_SkOZ4G2V0M,207271
|
3
|
-
foxesscloud-2.6.1.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.6.1.dist-info/METADATA,sha256=aYpWZtJnpnzmEjkB5ip7fCW4Qx9Ra5WOL-5uFCYHemQ,57452
|
5
|
-
foxesscloud-2.6.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.6.1.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|