foxesscloud 2.6.0__tar.gz → 2.6.2__tar.gz
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-2.6.0 → foxesscloud-2.6.2}/PKG-INFO +16 -3
- foxesscloud-2.6.0/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.6.2/README.md +15 -16
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/pyproject.toml +1 -1
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/src/foxesscloud/foxesscloud.py +123 -45
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/src/foxesscloud/openapi.py +67 -42
- foxesscloud-2.6.0/README.md → foxesscloud-2.6.2/src/foxesscloud.egg-info/PKG-INFO +29 -2
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/LICENCE +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/setup.cfg +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.2}/src/foxesscloud.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.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
|
@@ -105,6 +105,7 @@ Once an inverter is selected, you can make other calls to get information:
|
|
105
105
|
```
|
106
106
|
f.get_generation()
|
107
107
|
f.get_battery()
|
108
|
+
f.get_batteries()
|
108
109
|
f.get_settings()
|
109
110
|
f.get_charge()
|
110
111
|
f.get_min()
|
@@ -116,8 +117,8 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
116
117
|
|
117
118
|
get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
|
118
119
|
|
119
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery
|
120
|
-
|
120
|
+
get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery / f.batteries.
|
121
|
+
get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Additional battery attributes include:
|
121
122
|
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
122
123
|
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
123
124
|
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
@@ -786,6 +787,18 @@ This setting can be:
|
|
786
787
|
|
787
788
|
# Version Info
|
788
789
|
|
790
|
+
2.6.2<br>
|
791
|
+
Update battery calibration for charge_needed() when residual_handling is 2.
|
792
|
+
Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
|
793
|
+
Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
|
794
|
+
|
795
|
+
2.6.1<br>
|
796
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
797
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
798
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
799
|
+
Update battery_info() to support multiple BMS.
|
800
|
+
Add rated capacity and SoH to battery info if available.
|
801
|
+
|
789
802
|
2.6.0<br>
|
790
803
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
804
|
|
@@ -1,17 +1,3 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: foxesscloud
|
3
|
-
Version: 2.6.0
|
4
|
-
Summary: library for accessing Fox ESS cloud data using Open API
|
5
|
-
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
|
-
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
7
|
-
Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Requires-Python: >=3.7
|
12
|
-
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENCE
|
14
|
-
|
15
1
|
# FoxESS-Cloud
|
16
2
|
|
17
3
|
<a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
|
@@ -105,6 +91,7 @@ Once an inverter is selected, you can make other calls to get information:
|
|
105
91
|
```
|
106
92
|
f.get_generation()
|
107
93
|
f.get_battery()
|
94
|
+
f.get_batteries()
|
108
95
|
f.get_settings()
|
109
96
|
f.get_charge()
|
110
97
|
f.get_min()
|
@@ -116,8 +103,8 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
116
103
|
|
117
104
|
get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
|
118
105
|
|
119
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery
|
120
|
-
|
106
|
+
get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery / f.batteries.
|
107
|
+
get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Additional battery attributes include:
|
121
108
|
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
122
109
|
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
123
110
|
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
@@ -786,6 +773,18 @@ This setting can be:
|
|
786
773
|
|
787
774
|
# Version Info
|
788
775
|
|
776
|
+
2.6.2<br>
|
777
|
+
Update battery calibration for charge_needed() when residual_handling is 2.
|
778
|
+
Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
|
779
|
+
Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
|
780
|
+
|
781
|
+
2.6.1<br>
|
782
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
783
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
784
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
785
|
+
Update battery_info() to support multiple BMS.
|
786
|
+
Add rated capacity and SoH to battery info if available.
|
787
|
+
|
789
788
|
2.6.0<br>
|
790
789
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
790
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 12 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.4"
|
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}
|
@@ -477,6 +481,7 @@ def get_device(sn=None):
|
|
477
481
|
device_id = device.get('deviceID')
|
478
482
|
device_sn = device.get('deviceSN')
|
479
483
|
battery = None
|
484
|
+
batteries = None
|
480
485
|
battery_settings = None
|
481
486
|
schedule = None
|
482
487
|
templates = None
|
@@ -575,6 +580,7 @@ def get_firmware():
|
|
575
580
|
##################################################################################################
|
576
581
|
|
577
582
|
battery = None
|
583
|
+
batteries = None
|
578
584
|
battery_settings = None
|
579
585
|
|
580
586
|
# 1 = Residual Energy, 2 = Residual Capacity
|
@@ -592,8 +598,8 @@ battery_params = {
|
|
592
598
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
593
599
|
'step': 5,
|
594
600
|
'offset': 5,
|
595
|
-
'charge_loss': 1.
|
596
|
-
'discharge_loss': 0.
|
601
|
+
'charge_loss': 1.08,
|
602
|
+
'discharge_loss': 0.85},
|
597
603
|
}
|
598
604
|
|
599
605
|
def get_battery(info=1):
|
@@ -625,12 +631,16 @@ def get_battery(info=1):
|
|
625
631
|
errno = response.json().get('errno')
|
626
632
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
627
633
|
else:
|
628
|
-
battery['info'] = result['batteries']
|
629
|
-
if battery['info'][
|
634
|
+
battery['info'] = result['batteries'][0]
|
635
|
+
if battery['info']['masterVersion'] >= '1.014':
|
630
636
|
residual_handling = 2
|
637
|
+
battery['residual_handling'] = residual_handling
|
638
|
+
battery['rated_capacity'] = None
|
639
|
+
battery['soh'] = None
|
640
|
+
battery['soh_supported'] = False
|
631
641
|
if battery.get('residual') is not None:
|
632
642
|
battery['residual'] /= 1000
|
633
|
-
if residual_handling == 2:
|
643
|
+
if battery['residual_handling'] == 2:
|
634
644
|
capacity = battery.get('residual')
|
635
645
|
soc = battery.get('soc')
|
636
646
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
@@ -641,13 +651,70 @@ def get_battery(info=1):
|
|
641
651
|
battery['capacity'] = round(capacity, 3)
|
642
652
|
battery['residual'] = round(residual, 3)
|
643
653
|
battery['charge_rate'] = 50
|
644
|
-
params = battery_params[residual_handling]
|
654
|
+
params = battery_params[battery['residual_handling']]
|
645
655
|
battery['charge_loss'] = params['charge_loss']
|
646
656
|
battery['discharge_loss'] = params['discharge_loss']
|
647
657
|
if battery.get('temperature') is not None:
|
648
658
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
649
659
|
return battery
|
650
660
|
|
661
|
+
def get_batteries(info=1):
|
662
|
+
global token, device_id, battery, debug_setting, messages, batteries, battery_params, residual_handling
|
663
|
+
if get_device() is None:
|
664
|
+
return None
|
665
|
+
output(f"getting batteries", 2)
|
666
|
+
params = {'id': device_id}
|
667
|
+
response = signed_get(path="/generic/v0/device/battery/info", params=params)
|
668
|
+
if response.status_code != 200:
|
669
|
+
output(f"** get_batteries() got response code {response.status_code}: {response.reason}")
|
670
|
+
return None
|
671
|
+
result = response.json().get('result')
|
672
|
+
if result is None:
|
673
|
+
errno = response.json().get('errno')
|
674
|
+
output(f"** get_batteries(), no result data, {errno_message(errno)}")
|
675
|
+
return None
|
676
|
+
batteries = result['batterys']
|
677
|
+
if info == 1:
|
678
|
+
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
679
|
+
if response.status_code != 200:
|
680
|
+
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
681
|
+
else:
|
682
|
+
result = response.json().get('result')
|
683
|
+
if result is None:
|
684
|
+
errno = response.json().get('errno')
|
685
|
+
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
686
|
+
else:
|
687
|
+
for i in range(0, len(batteries)):
|
688
|
+
batteries[i]['info'] = result['batteries'][i]
|
689
|
+
for b in batteries:
|
690
|
+
if b.get('info') is not None and b['info']['masterVersion'] >= '1.014':
|
691
|
+
b['residual_handling'] = 2
|
692
|
+
if b.get('soh') is not None and b['soh'].isnumeric():
|
693
|
+
b['soh_supported'] = True
|
694
|
+
else:
|
695
|
+
b['rated_capacity'] = None
|
696
|
+
b['soh'] = None
|
697
|
+
b['soh_supported'] = False
|
698
|
+
for b in batteries:
|
699
|
+
if b.get('soh_supported') is not None and b['soh_supported']:
|
700
|
+
capacity = (b['ratedCapacity'] / 1000 * int(b['soh']) / 100)
|
701
|
+
soc = b.get('soc')
|
702
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
703
|
+
b['capacity'] = round(capacity, 3)
|
704
|
+
b['residual'] = round(residual, 3)
|
705
|
+
b['charge_rate'] = 50
|
706
|
+
params = battery_params[residual_handling]
|
707
|
+
b['charge_loss'] = params['charge_loss']
|
708
|
+
b['discharge_loss'] = params['discharge_loss']
|
709
|
+
if b.get('temperature') is not None:
|
710
|
+
b['charge_rate'] = params['table'][int((b['temperature'] - params['offset']) / params['step'])]
|
711
|
+
else:
|
712
|
+
get_battery(info=info)
|
713
|
+
batteries = [battery]
|
714
|
+
break
|
715
|
+
battery = batteries[0]
|
716
|
+
return batteries
|
717
|
+
|
651
718
|
##################################################################################################
|
652
719
|
# get charge times and save to battery_settings
|
653
720
|
##################################################################################################
|
@@ -2118,9 +2185,9 @@ def hours_difference(t1, t2):
|
|
2118
2185
|
if t1 == t2:
|
2119
2186
|
return 0.0
|
2120
2187
|
if type(t1) is str:
|
2121
|
-
t1 =
|
2188
|
+
t1 = convert_date(t1)
|
2122
2189
|
if type(t2) is str:
|
2123
|
-
t2 =
|
2190
|
+
t2 = convert_date(t2)
|
2124
2191
|
return round((t1 - t2).total_seconds() / 3600,1)
|
2125
2192
|
|
2126
2193
|
##################################################################################################
|
@@ -2297,7 +2364,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2297
2364
|
if d is not None and len(d) < 11:
|
2298
2365
|
d += " 18:00"
|
2299
2366
|
# get dates and times
|
2300
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else
|
2367
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
|
2301
2368
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2302
2369
|
# adjust system to get local time now
|
2303
2370
|
now = system_time + timedelta(hours=time_offset)
|
@@ -2723,7 +2790,7 @@ charge_config = {
|
|
2723
2790
|
'export_limit': None, # maximum export power in kW
|
2724
2791
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2725
2792
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2726
|
-
'ac_dc_loss': 0.
|
2793
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2727
2794
|
'inverter_power': 101, # Inverter power consumption in W
|
2728
2795
|
'bms_power': 50, # BMS power consumption in W
|
2729
2796
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2768,7 +2835,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2768
2835
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2769
2836
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2770
2837
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2771
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2838
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
|
2772
2839
|
print(f"\n---------------- charge_needed ----------------")
|
2773
2840
|
# validate parameters
|
2774
2841
|
args = locals()
|
@@ -2796,7 +2863,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2796
2863
|
if type(forecast_times) is not list:
|
2797
2864
|
forecast_times = [forecast_times]
|
2798
2865
|
# get dates and times
|
2799
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else
|
2866
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
|
2800
2867
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2801
2868
|
now = system_time + timedelta(hours=time_offset)
|
2802
2869
|
today = datetime.strftime(now, '%Y-%m-%d')
|
@@ -2848,7 +2915,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2848
2915
|
time_to_end = times[0]['time_to_end']
|
2849
2916
|
charge_time = times[0]['charge_time']
|
2850
2917
|
# work out time window and times with clock changes
|
2851
|
-
time_to_next = int(time_to_start)
|
2852
2918
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2853
2919
|
forecast_day = today if charge_today else tomorrow
|
2854
2920
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2867,17 +2933,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2867
2933
|
output(f"start_at = {start_at}, end_by = {end_by}, bat_hold = {bat_hold}")
|
2868
2934
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2869
2935
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2870
|
-
output(f"
|
2936
|
+
output(f"full_charge = {full_charge}")
|
2871
2937
|
if test_soc is not None:
|
2872
2938
|
current_soc = test_soc
|
2873
|
-
capacity = 14.
|
2939
|
+
capacity = 14.53
|
2874
2940
|
residual = test_soc * capacity / 100
|
2875
2941
|
bat_volt = 317.4
|
2876
2942
|
bat_power = 0.0
|
2877
2943
|
temperature = 30
|
2878
|
-
bms_charge_current =
|
2879
|
-
charge_loss =
|
2880
|
-
discharge_loss =
|
2944
|
+
bms_charge_current = 15
|
2945
|
+
charge_loss = charge_rates[2]['charge_loss']
|
2946
|
+
discharge_loss = charge_rates[2]['discharge_loss']
|
2881
2947
|
bat_current = 0.0
|
2882
2948
|
device_power = 6.0
|
2883
2949
|
device_current = 35
|
@@ -2921,10 +2987,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2921
2987
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
2922
2988
|
output(f" Current SoC: {current_soc}%")
|
2923
2989
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2924
|
-
output(f" Charge
|
2990
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2925
2991
|
output(f" Temperature: {temperature:.1f}°C")
|
2926
2992
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2927
2993
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2994
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
|
2928
2995
|
# charge current may be derated based on temperature
|
2929
2996
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2930
2997
|
if charge_current > bms_charge_current:
|
@@ -3100,7 +3167,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3100
3167
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
3101
3168
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
3102
3169
|
kwh_current = residual - (charge_timed[0] - discharge_timed[0]) * (hour_now % 1)
|
3103
|
-
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=capacity)
|
3170
|
+
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=time_to_end, kwh_min=capacity)
|
3104
3171
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
3105
3172
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
3106
3173
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3178,7 +3245,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3178
3245
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3179
3246
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3180
3247
|
# rebuild the battery residual with the charge added and min_soc
|
3181
|
-
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3248
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3182
3249
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3183
3250
|
# show the results
|
3184
3251
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3263,10 +3330,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3263
3330
|
|
3264
3331
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
3265
3332
|
global charge_config, storage
|
3266
|
-
now =
|
3333
|
+
now = convert_date(d)
|
3267
3334
|
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3268
3335
|
if save is None and charge_config.get('save') is not None:
|
3269
|
-
save = charge_config.get('save').replace('###', yesterday)
|
3336
|
+
save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
|
3270
3337
|
if not os.path.exists(storage + save):
|
3271
3338
|
save = None
|
3272
3339
|
if save is None:
|
@@ -3293,7 +3360,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3293
3360
|
base_hour = int(time_hours(base_time[11:16]))
|
3294
3361
|
start_day = base_time[:10]
|
3295
3362
|
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3296
|
-
now =
|
3363
|
+
now = convert_date(base_time)
|
3297
3364
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3298
3365
|
if v is None:
|
3299
3366
|
v = ['pvPower', 'loadsPower', 'SoC']
|
@@ -3326,13 +3393,14 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3326
3393
|
for i in range(0, run_time):
|
3327
3394
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3328
3395
|
if show_data > 0 and plots.get('SoC') is not None:
|
3329
|
-
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else
|
3330
|
-
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3396
|
+
data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
|
3397
|
+
s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
|
3331
3398
|
h = base_hour
|
3332
3399
|
t = 0
|
3333
3400
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3334
3401
|
col = h % data_wrap
|
3335
3402
|
s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
|
3403
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3336
3404
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3337
3405
|
h += 1
|
3338
3406
|
t += steps_per_hour
|
@@ -3411,21 +3479,28 @@ def bat_count(cell_count):
|
|
3411
3479
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3412
3480
|
|
3413
3481
|
# show information about the current state of the batteries
|
3414
|
-
def battery_info(log=0, plot=1, count=None, info=1):
|
3415
|
-
global debug_setting, battery_info_app_key
|
3416
|
-
output_spool(battery_info_app_key)
|
3417
|
-
bat = get_battery(info=info)
|
3482
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3483
|
+
global debug_setting, battery_info_app_key
|
3418
3484
|
if bat is None:
|
3419
|
-
|
3485
|
+
bats = get_batteries(info=info)
|
3486
|
+
if bats is None:
|
3487
|
+
return None
|
3488
|
+
for i in range(0, len(bats)):
|
3489
|
+
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3490
|
+
battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
|
3420
3491
|
return None
|
3492
|
+
output_spool(battery_info_app_key)
|
3421
3493
|
nbat = None
|
3422
3494
|
if info == 1 and bat.get('info') is not None:
|
3423
|
-
|
3424
|
-
|
3425
|
-
|
3426
|
-
|
3427
|
-
|
3428
|
-
|
3495
|
+
b = bat['info']
|
3496
|
+
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3497
|
+
nbat = 0
|
3498
|
+
for s in b['slaveBatteries']:
|
3499
|
+
nbat += 1
|
3500
|
+
output(f"SN {s['sn']}, {s['batType']}, Version {s['version']} (Battery {nbat})")
|
3501
|
+
output()
|
3502
|
+
rated_capacity = bat.get('ratedCapacity')
|
3503
|
+
bat_soh = bat.get('soh')
|
3429
3504
|
bat_volt = bat['volt']
|
3430
3505
|
current_soc = bat['soc']
|
3431
3506
|
residual = bat['residual']
|
@@ -3476,9 +3551,12 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3476
3551
|
for v in cell_temps:
|
3477
3552
|
s +=f",{v:.0f}"
|
3478
3553
|
return s
|
3479
|
-
|
3554
|
+
if rated_capacity is not None:
|
3555
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3556
|
+
output(f"SoH: {bat_soh}%")
|
3557
|
+
output(f"Current SoC: {current_soc}%")
|
3480
3558
|
output(f"Capacity: {capacity:.2f}kWh")
|
3481
|
-
output(f"Residual: {residual:.2f}kWh")
|
3559
|
+
output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity)" if bat['residual_handling'] == 2 else ""))
|
3482
3560
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
3483
3561
|
output(f"InvBatCurrent: {bat_current:.1f}A")
|
3484
3562
|
output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
|
@@ -3888,7 +3966,7 @@ class Solcast :
|
|
3888
3966
|
# 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
|
3889
3967
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3890
3968
|
self.data = {}
|
3891
|
-
now =
|
3969
|
+
now = convert_date(d)
|
3892
3970
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3893
3971
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
3894
3972
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
@@ -4232,7 +4310,7 @@ class Solar :
|
|
4232
4310
|
def __init__(self, reload=0, quiet=False, shading=None, d=None):
|
4233
4311
|
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4234
4312
|
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4235
|
-
now =
|
4313
|
+
now = convert_date(d)
|
4236
4314
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
4237
4315
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
4238
4316
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 12 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.2"
|
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)
|
@@ -469,6 +473,7 @@ def get_device(sn=None):
|
|
469
473
|
return None
|
470
474
|
device = result
|
471
475
|
battery = None
|
476
|
+
batteries = None
|
472
477
|
battery_settings = None
|
473
478
|
schedule = None
|
474
479
|
get_flag()
|
@@ -538,6 +543,7 @@ def get_generation(update=1):
|
|
538
543
|
##################################################################################################
|
539
544
|
|
540
545
|
battery = None
|
546
|
+
batteries = None
|
541
547
|
battery_settings = None
|
542
548
|
battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy' ]
|
543
549
|
battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
@@ -557,11 +563,11 @@ battery_params = {
|
|
557
563
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
558
564
|
'step': 5,
|
559
565
|
'offset': 5,
|
560
|
-
'charge_loss': 1.
|
561
|
-
'discharge_loss': 0.
|
566
|
+
'charge_loss': 1.080,
|
567
|
+
'discharge_loss': 0.85},
|
562
568
|
}
|
563
569
|
|
564
|
-
def get_battery(
|
570
|
+
def get_battery(info=0, v=None):
|
565
571
|
global device_sn, battery, debug_setting, residual_handling, battery_params
|
566
572
|
if get_device() is None:
|
567
573
|
return None
|
@@ -573,7 +579,11 @@ def get_battery(v = None, info=0):
|
|
573
579
|
battery = {}
|
574
580
|
for i in range(0, len(battery_vars)):
|
575
581
|
battery[battery_data[i]] = result[i].get('value')
|
576
|
-
|
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:
|
577
587
|
capacity = battery.get('residual')
|
578
588
|
soc = battery.get('soc')
|
579
589
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
@@ -585,13 +595,19 @@ def get_battery(v = None, info=0):
|
|
585
595
|
battery['residual'] = round(residual, 3)
|
586
596
|
battery['status'] = 1
|
587
597
|
battery['charge_rate'] = 50
|
588
|
-
params = battery_params[residual_handling]
|
598
|
+
params = battery_params[battery['residual_handling']]
|
589
599
|
battery['charge_loss'] = params['charge_loss']
|
590
600
|
battery['discharge_loss'] = params['discharge_loss']
|
591
601
|
if battery.get('temperature') is not None:
|
592
602
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
593
603
|
return battery
|
594
604
|
|
605
|
+
def get_batteries(info=0):
|
606
|
+
global battery, batteries
|
607
|
+
get_battery(info=info)
|
608
|
+
batteries = [battery]
|
609
|
+
return batteries
|
610
|
+
|
595
611
|
##################################################################################################
|
596
612
|
# get charge times and save to battery_settings
|
597
613
|
##################################################################################################
|
@@ -1974,9 +1990,9 @@ def hours_difference(t1, t2):
|
|
1974
1990
|
if t1 == t2:
|
1975
1991
|
return 0.0
|
1976
1992
|
if type(t1) is str:
|
1977
|
-
t1 =
|
1993
|
+
t1 = convert_date(t1)
|
1978
1994
|
if type(t2) is str:
|
1979
|
-
t2 =
|
1995
|
+
t2 = convert_date(t2)
|
1980
1996
|
return round((t1 - t2).total_seconds() / 3600,1)
|
1981
1997
|
|
1982
1998
|
##################################################################################################
|
@@ -2153,7 +2169,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2153
2169
|
if d is not None and len(d) < 11:
|
2154
2170
|
d += " 18:00"
|
2155
2171
|
# get dates and times
|
2156
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else
|
2172
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
|
2157
2173
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2158
2174
|
# adjust system to get local time now
|
2159
2175
|
now = system_time + timedelta(hours=time_offset)
|
@@ -2579,7 +2595,7 @@ charge_config = {
|
|
2579
2595
|
'export_limit': None, # maximum export power in kW
|
2580
2596
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2581
2597
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2582
|
-
'ac_dc_loss': 0.
|
2598
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2583
2599
|
'inverter_power': 101, # Inverter power consumption in W
|
2584
2600
|
'bms_power': 50, # BMS power consumption in W
|
2585
2601
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2624,7 +2640,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2624
2640
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2625
2641
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2626
2642
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2627
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2643
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
|
2628
2644
|
print(f"\n---------------- charge_needed ----------------")
|
2629
2645
|
# validate parameters
|
2630
2646
|
args = locals()
|
@@ -2652,7 +2668,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2652
2668
|
if type(forecast_times) is not list:
|
2653
2669
|
forecast_times = [forecast_times]
|
2654
2670
|
# get dates and times
|
2655
|
-
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else
|
2671
|
+
system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
|
2656
2672
|
time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
|
2657
2673
|
now = system_time + timedelta(hours=time_offset)
|
2658
2674
|
today = datetime.strftime(now, '%Y-%m-%d')
|
@@ -2704,7 +2720,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2704
2720
|
time_to_end = times[0]['time_to_end']
|
2705
2721
|
charge_time = times[0]['charge_time']
|
2706
2722
|
# work out time window and times with clock changes
|
2707
|
-
time_to_next = int(time_to_start)
|
2708
2723
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2709
2724
|
forecast_day = today if charge_today else tomorrow
|
2710
2725
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2723,7 +2738,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2723
2738
|
output(f"start_at = {start_at}, end_by = {end_by}, force_charge = {force_charge}")
|
2724
2739
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2725
2740
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2726
|
-
output(f"
|
2741
|
+
output(f"full_charge = {full_charge}")
|
2727
2742
|
if test_soc is not None:
|
2728
2743
|
current_soc = test_soc
|
2729
2744
|
capacity = 14.54
|
@@ -2731,9 +2746,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2731
2746
|
bat_volt = 317.4
|
2732
2747
|
bat_power = 0.0
|
2733
2748
|
temperature = 30
|
2734
|
-
bms_charge_current =
|
2735
|
-
charge_loss =
|
2736
|
-
discharge_loss =
|
2749
|
+
bms_charge_current = 15
|
2750
|
+
charge_loss = charge_rates[2]['charge_loss']
|
2751
|
+
discharge_loss = charge_rates[2]['discharge_loss']
|
2737
2752
|
bat_current = 0.0
|
2738
2753
|
device_power = 6.0
|
2739
2754
|
device_current = 35
|
@@ -2778,7 +2793,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2778
2793
|
output(f" Current SoC: {current_soc}%")
|
2779
2794
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2780
2795
|
output(f" Temperature: {temperature:.1f}°C")
|
2781
|
-
output(f" Charge
|
2796
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2782
2797
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2783
2798
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2784
2799
|
# charge current may be derated based on temperature
|
@@ -2955,7 +2970,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2955
2970
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
2956
2971
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
2957
2972
|
kwh_current = residual - (charge_timed[0] - discharge_timed[0]) * (hour_now % 1)
|
2958
|
-
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=capacity)
|
2973
|
+
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=time_to_end, kwh_min=capacity)
|
2959
2974
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
2960
2975
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
2961
2976
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3033,7 +3048,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3033
3048
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3034
3049
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3035
3050
|
# rebuild the battery residual with any charge added and min_soc
|
3036
|
-
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3051
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3037
3052
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3038
3053
|
# show the results
|
3039
3054
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3117,10 +3132,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3117
3132
|
|
3118
3133
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3119
3134
|
global charge_config, storage
|
3120
|
-
now =
|
3135
|
+
now = convert_date(d)
|
3121
3136
|
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3122
3137
|
if save is None and charge_config.get('save') is not None:
|
3123
|
-
save = charge_config.get('save').replace('###', yesterday)
|
3138
|
+
save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
|
3124
3139
|
if not os.path.exists(storage + save):
|
3125
3140
|
save = None
|
3126
3141
|
if save is None:
|
@@ -3147,7 +3162,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3147
3162
|
base_hour = int(time_hours(base_time[11:16]))
|
3148
3163
|
start_day = base_time[:10]
|
3149
3164
|
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3150
|
-
now =
|
3165
|
+
now = convert_date(base_time)
|
3151
3166
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3152
3167
|
if v is None:
|
3153
3168
|
v = ['pvPower', 'loadsPower', 'SoC']
|
@@ -3264,21 +3279,28 @@ def bat_count(cell_count):
|
|
3264
3279
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3265
3280
|
|
3266
3281
|
# show information about the current state of the batteries
|
3267
|
-
def battery_info(log=0, plot=1, count=None, info=1):
|
3268
|
-
global debug_setting, battery_info_app_key
|
3269
|
-
output_spool(battery_info_app_key)
|
3270
|
-
bat = get_battery(info=info)
|
3282
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3283
|
+
global debug_setting, battery_info_app_key
|
3271
3284
|
if bat is None:
|
3272
|
-
|
3285
|
+
bats = get_batteries(info=info)
|
3286
|
+
if bats is None:
|
3287
|
+
return None
|
3288
|
+
for i in range(0, len(bats)):
|
3289
|
+
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3290
|
+
battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
|
3273
3291
|
return None
|
3292
|
+
output_spool(battery_info_app_key)
|
3274
3293
|
nbat = None
|
3275
3294
|
if info == 1 and bat.get('info') is not None:
|
3276
|
-
|
3277
|
-
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
3295
|
+
b = bat['info']
|
3296
|
+
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3297
|
+
nbat = 0
|
3298
|
+
for s in b['slaveBatteries']:
|
3299
|
+
nbat += 1
|
3300
|
+
output(f"SN {s['sn']}, {s['batType']}, Version {s['version']} (Battery {nbat})")
|
3301
|
+
output()
|
3302
|
+
rated_capacity = bat.get('ratedCapacity')
|
3303
|
+
bat_soh = bat.get('soh')
|
3282
3304
|
bat_volt = bat['volt']
|
3283
3305
|
current_soc = bat['soc']
|
3284
3306
|
residual = bat['residual']
|
@@ -3329,7 +3351,10 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3329
3351
|
for v in cell_temps:
|
3330
3352
|
s +=f",{v:.0f}"
|
3331
3353
|
return s
|
3332
|
-
|
3354
|
+
if rated_capacity is not None:
|
3355
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3356
|
+
output(f"SoH: {bat_soh}%")
|
3357
|
+
output(f"Current SoC: {current_soc}%")
|
3333
3358
|
output(f"Capacity: {capacity:.2f}kWh")
|
3334
3359
|
output(f"Residual: {residual:.2f}kWh")
|
3335
3360
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
@@ -3741,7 +3766,7 @@ class Solcast :
|
|
3741
3766
|
# 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
|
3742
3767
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3743
3768
|
self.data = {}
|
3744
|
-
now =
|
3769
|
+
now = convert_date(d)
|
3745
3770
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3746
3771
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
3747
3772
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
@@ -4085,7 +4110,7 @@ class Solar :
|
|
4085
4110
|
def __init__(self, reload=0, quiet=False, shading=None, d=None):
|
4086
4111
|
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4087
4112
|
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4088
|
-
now =
|
4113
|
+
now = convert_date(d)
|
4089
4114
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
4090
4115
|
self.quarter = int(self.today[5:7]) // 3 % 4
|
4091
4116
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
@@ -1,3 +1,17 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: foxesscloud
|
3
|
+
Version: 2.6.2
|
4
|
+
Summary: library for accessing Fox ESS cloud data using Open API
|
5
|
+
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
|
+
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
7
|
+
Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENCE
|
14
|
+
|
1
15
|
# FoxESS-Cloud
|
2
16
|
|
3
17
|
<a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
|
@@ -91,6 +105,7 @@ Once an inverter is selected, you can make other calls to get information:
|
|
91
105
|
```
|
92
106
|
f.get_generation()
|
93
107
|
f.get_battery()
|
108
|
+
f.get_batteries()
|
94
109
|
f.get_settings()
|
95
110
|
f.get_charge()
|
96
111
|
f.get_min()
|
@@ -102,8 +117,8 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
102
117
|
|
103
118
|
get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
|
104
119
|
|
105
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery
|
106
|
-
|
120
|
+
get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery / f.batteries.
|
121
|
+
get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Additional battery attributes include:
|
107
122
|
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
108
123
|
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
109
124
|
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
@@ -772,6 +787,18 @@ This setting can be:
|
|
772
787
|
|
773
788
|
# Version Info
|
774
789
|
|
790
|
+
2.6.2<br>
|
791
|
+
Update battery calibration for charge_needed() when residual_handling is 2.
|
792
|
+
Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
|
793
|
+
Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
|
794
|
+
|
795
|
+
2.6.1<br>
|
796
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
797
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
798
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
799
|
+
Update battery_info() to support multiple BMS.
|
800
|
+
Add rated capacity and SoH to battery info if available.
|
801
|
+
|
775
802
|
2.6.0<br>
|
776
803
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
777
804
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|