foxesscloud 2.6.0__tar.gz → 2.6.1__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/src/foxesscloud.egg-info → foxesscloud-2.6.1}/PKG-INFO +11 -3
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/README.md +10 -2
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/pyproject.toml +1 -1
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/src/foxesscloud/foxesscloud.py +82 -26
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/src/foxesscloud/openapi.py +44 -25
- {foxesscloud-2.6.0 → foxesscloud-2.6.1/src/foxesscloud.egg-info}/PKG-INFO +11 -3
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/LICENCE +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/setup.cfg +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.6.0 → foxesscloud-2.6.1}/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.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
|
@@ -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,13 @@ This setting can be:
|
|
786
787
|
|
787
788
|
# Version Info
|
788
789
|
|
790
|
+
2.6.1<br>
|
791
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
792
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
793
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
794
|
+
Update battery_info() to support multiple BMS.
|
795
|
+
Add rated capacity and SoH to battery info if available.
|
796
|
+
|
789
797
|
2.6.0<br>
|
790
798
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
799
|
|
@@ -91,6 +91,7 @@ Once an inverter is selected, you can make other calls to get information:
|
|
91
91
|
```
|
92
92
|
f.get_generation()
|
93
93
|
f.get_battery()
|
94
|
+
f.get_batteries()
|
94
95
|
f.get_settings()
|
95
96
|
f.get_charge()
|
96
97
|
f.get_min()
|
@@ -102,8 +103,8 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
102
103
|
|
103
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'.
|
104
105
|
|
105
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery
|
106
|
-
|
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:
|
107
108
|
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
108
109
|
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
109
110
|
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
@@ -772,6 +773,13 @@ This setting can be:
|
|
772
773
|
|
773
774
|
# Version Info
|
774
775
|
|
776
|
+
2.6.1<br>
|
777
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
778
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
779
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
780
|
+
Update battery_info() to support multiple BMS.
|
781
|
+
Add rated capacity and SoH to battery info if available.
|
782
|
+
|
775
783
|
2.6.0<br>
|
776
784
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
777
785
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 09 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.3"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -477,6 +477,7 @@ def get_device(sn=None):
|
|
477
477
|
device_id = device.get('deviceID')
|
478
478
|
device_sn = device.get('deviceSN')
|
479
479
|
battery = None
|
480
|
+
batteries = None
|
480
481
|
battery_settings = None
|
481
482
|
schedule = None
|
482
483
|
templates = None
|
@@ -575,6 +576,7 @@ def get_firmware():
|
|
575
576
|
##################################################################################################
|
576
577
|
|
577
578
|
battery = None
|
579
|
+
batteries = None
|
578
580
|
battery_settings = None
|
579
581
|
|
580
582
|
# 1 = Residual Energy, 2 = Residual Capacity
|
@@ -592,7 +594,7 @@ battery_params = {
|
|
592
594
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
593
595
|
'step': 5,
|
594
596
|
'offset': 5,
|
595
|
-
'charge_loss': 1.
|
597
|
+
'charge_loss': 1.080,
|
596
598
|
'discharge_loss': 0.975},
|
597
599
|
}
|
598
600
|
|
@@ -625,8 +627,8 @@ def get_battery(info=1):
|
|
625
627
|
errno = response.json().get('errno')
|
626
628
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
627
629
|
else:
|
628
|
-
battery['info'] = result['batteries']
|
629
|
-
if battery['info'][
|
630
|
+
battery['info'] = result['batteries'][0]
|
631
|
+
if battery['info']['masterVersion'] >= '1.014':
|
630
632
|
residual_handling = 2
|
631
633
|
if battery.get('residual') is not None:
|
632
634
|
battery['residual'] /= 1000
|
@@ -648,6 +650,51 @@ def get_battery(info=1):
|
|
648
650
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
649
651
|
return battery
|
650
652
|
|
653
|
+
def get_batteries(info=1):
|
654
|
+
global token, device_id, battery, debug_setting, messages, batteries, battery_params, residual_handling
|
655
|
+
if get_device() is None:
|
656
|
+
return None
|
657
|
+
output(f"getting batteries", 2)
|
658
|
+
params = {'id': device_id}
|
659
|
+
response = signed_get(path="/generic/v0/device/battery/info", params=params)
|
660
|
+
if response.status_code != 200:
|
661
|
+
output(f"** get_batteries() got response code {response.status_code}: {response.reason}")
|
662
|
+
return None
|
663
|
+
result = response.json().get('result')
|
664
|
+
if result is None:
|
665
|
+
errno = response.json().get('errno')
|
666
|
+
output(f"** get_batteries(), no result data, {errno_message(errno)}")
|
667
|
+
return None
|
668
|
+
batteries = result['batterys']
|
669
|
+
if info == 1:
|
670
|
+
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
671
|
+
if response.status_code != 200:
|
672
|
+
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
673
|
+
else:
|
674
|
+
result = response.json().get('result')
|
675
|
+
if result is None:
|
676
|
+
errno = response.json().get('errno')
|
677
|
+
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
678
|
+
else:
|
679
|
+
for i in range(0, len(batteries)):
|
680
|
+
batteries[i]['info'] = result['batteries'][i]
|
681
|
+
for b in batteries:
|
682
|
+
if b.get('info') is not None and b['info']['masterVersion'] >= '1.014':
|
683
|
+
residual_handling = 2
|
684
|
+
capacity = b['ratedCapacity'] / 1000 * int(b['soh']) / 100
|
685
|
+
soc = b.get('soc')
|
686
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
687
|
+
b['capacity'] = round(capacity, 3)
|
688
|
+
b['residual'] = round(residual, 3)
|
689
|
+
b['charge_rate'] = 50
|
690
|
+
params = battery_params[residual_handling]
|
691
|
+
b['charge_loss'] = params['charge_loss']
|
692
|
+
b['discharge_loss'] = params['discharge_loss']
|
693
|
+
if b.get('temperature') is not None:
|
694
|
+
b['charge_rate'] = params['table'][int((b['temperature'] - params['offset']) / params['step'])]
|
695
|
+
battery = batteries[0]
|
696
|
+
return batteries
|
697
|
+
|
651
698
|
##################################################################################################
|
652
699
|
# get charge times and save to battery_settings
|
653
700
|
##################################################################################################
|
@@ -2723,7 +2770,7 @@ charge_config = {
|
|
2723
2770
|
'export_limit': None, # maximum export power in kW
|
2724
2771
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2725
2772
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2726
|
-
'ac_dc_loss': 0.
|
2773
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2727
2774
|
'inverter_power': 101, # Inverter power consumption in W
|
2728
2775
|
'bms_power': 50, # BMS power consumption in W
|
2729
2776
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2848,7 +2895,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2848
2895
|
time_to_end = times[0]['time_to_end']
|
2849
2896
|
charge_time = times[0]['charge_time']
|
2850
2897
|
# work out time window and times with clock changes
|
2851
|
-
time_to_next = int(time_to_start)
|
2852
2898
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2853
2899
|
forecast_day = today if charge_today else tomorrow
|
2854
2900
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2867,7 +2913,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2867
2913
|
output(f"start_at = {start_at}, end_by = {end_by}, bat_hold = {bat_hold}")
|
2868
2914
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2869
2915
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2870
|
-
output(f"
|
2916
|
+
output(f"full_charge = {full_charge}")
|
2871
2917
|
if test_soc is not None:
|
2872
2918
|
current_soc = test_soc
|
2873
2919
|
capacity = 14.54
|
@@ -2875,9 +2921,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2875
2921
|
bat_volt = 317.4
|
2876
2922
|
bat_power = 0.0
|
2877
2923
|
temperature = 30
|
2878
|
-
bms_charge_current =
|
2879
|
-
charge_loss = 1.
|
2880
|
-
discharge_loss = 0.
|
2924
|
+
bms_charge_current = 15
|
2925
|
+
charge_loss = 1.080
|
2926
|
+
discharge_loss = 0.975
|
2881
2927
|
bat_current = 0.0
|
2882
2928
|
device_power = 6.0
|
2883
2929
|
device_current = 35
|
@@ -2921,7 +2967,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2921
2967
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
2922
2968
|
output(f" Current SoC: {current_soc}%")
|
2923
2969
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2924
|
-
output(f" Charge
|
2970
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2925
2971
|
output(f" Temperature: {temperature:.1f}°C")
|
2926
2972
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2927
2973
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
@@ -3100,7 +3146,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3100
3146
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
3101
3147
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
3102
3148
|
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)
|
3149
|
+
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=time_to_end, kwh_min=capacity)
|
3104
3150
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
3105
3151
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
3106
3152
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3178,7 +3224,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3178
3224
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3179
3225
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3180
3226
|
# 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)
|
3227
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3182
3228
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3183
3229
|
# show the results
|
3184
3230
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3411,21 +3457,28 @@ def bat_count(cell_count):
|
|
3411
3457
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3412
3458
|
|
3413
3459
|
# 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)
|
3460
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3461
|
+
global debug_setting, battery_info_app_key
|
3418
3462
|
if bat is None:
|
3419
|
-
|
3463
|
+
bats = get_batteries(info=info)
|
3464
|
+
if bats is None:
|
3465
|
+
return None
|
3466
|
+
for i in range(0, len(bats)):
|
3467
|
+
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3468
|
+
battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
|
3420
3469
|
return None
|
3470
|
+
output_spool(battery_info_app_key)
|
3421
3471
|
nbat = None
|
3422
3472
|
if info == 1 and bat.get('info') is not None:
|
3423
|
-
|
3424
|
-
|
3425
|
-
|
3426
|
-
|
3427
|
-
|
3428
|
-
|
3473
|
+
b = bat['info']
|
3474
|
+
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3475
|
+
nbat = 0
|
3476
|
+
for s in b['slaveBatteries']:
|
3477
|
+
nbat += 1
|
3478
|
+
output(f"SN {s['sn']}, {s['batType']}, Version {s['version']} (Battery {nbat})")
|
3479
|
+
output()
|
3480
|
+
rated_capacity = bat.get('ratedCapacity')
|
3481
|
+
bat_soh = bat.get('soh')
|
3429
3482
|
bat_volt = bat['volt']
|
3430
3483
|
current_soc = bat['soc']
|
3431
3484
|
residual = bat['residual']
|
@@ -3476,7 +3529,10 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3476
3529
|
for v in cell_temps:
|
3477
3530
|
s +=f",{v:.0f}"
|
3478
3531
|
return s
|
3479
|
-
|
3532
|
+
if rated_capacity is not None:
|
3533
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3534
|
+
output(f"SoH: {bat_soh}%")
|
3535
|
+
output(f"Current SoC: {current_soc}%")
|
3480
3536
|
output(f"Capacity: {capacity:.2f}kWh")
|
3481
3537
|
output(f"Residual: {residual:.2f}kWh")
|
3482
3538
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 09 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.1"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -469,6 +469,7 @@ def get_device(sn=None):
|
|
469
469
|
return None
|
470
470
|
device = result
|
471
471
|
battery = None
|
472
|
+
batteries = None
|
472
473
|
battery_settings = None
|
473
474
|
schedule = None
|
474
475
|
get_flag()
|
@@ -538,6 +539,7 @@ def get_generation(update=1):
|
|
538
539
|
##################################################################################################
|
539
540
|
|
540
541
|
battery = None
|
542
|
+
batteries = None
|
541
543
|
battery_settings = None
|
542
544
|
battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy' ]
|
543
545
|
battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
@@ -557,11 +559,11 @@ battery_params = {
|
|
557
559
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
558
560
|
'step': 5,
|
559
561
|
'offset': 5,
|
560
|
-
'charge_loss': 1.
|
562
|
+
'charge_loss': 1.080,
|
561
563
|
'discharge_loss': 0.975},
|
562
564
|
}
|
563
565
|
|
564
|
-
def get_battery(
|
566
|
+
def get_battery(info=0, v=None):
|
565
567
|
global device_sn, battery, debug_setting, residual_handling, battery_params
|
566
568
|
if get_device() is None:
|
567
569
|
return None
|
@@ -592,6 +594,14 @@ def get_battery(v = None, info=0):
|
|
592
594
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
593
595
|
return battery
|
594
596
|
|
597
|
+
def get_batteries(info=0):
|
598
|
+
global battery, batteries
|
599
|
+
get_battery(info=info)
|
600
|
+
battery['ratedCapacity'] = None
|
601
|
+
battery['soh'] = None
|
602
|
+
batteries = [battery]
|
603
|
+
return batteries
|
604
|
+
|
595
605
|
##################################################################################################
|
596
606
|
# get charge times and save to battery_settings
|
597
607
|
##################################################################################################
|
@@ -2579,7 +2589,7 @@ charge_config = {
|
|
2579
2589
|
'export_limit': None, # maximum export power in kW
|
2580
2590
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2581
2591
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2582
|
-
'ac_dc_loss': 0.
|
2592
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2583
2593
|
'inverter_power': 101, # Inverter power consumption in W
|
2584
2594
|
'bms_power': 50, # BMS power consumption in W
|
2585
2595
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2704,7 +2714,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2704
2714
|
time_to_end = times[0]['time_to_end']
|
2705
2715
|
charge_time = times[0]['charge_time']
|
2706
2716
|
# work out time window and times with clock changes
|
2707
|
-
time_to_next = int(time_to_start)
|
2708
2717
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2709
2718
|
forecast_day = today if charge_today else tomorrow
|
2710
2719
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2723,7 +2732,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2723
2732
|
output(f"start_at = {start_at}, end_by = {end_by}, force_charge = {force_charge}")
|
2724
2733
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2725
2734
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2726
|
-
output(f"
|
2735
|
+
output(f"full_charge = {full_charge}")
|
2727
2736
|
if test_soc is not None:
|
2728
2737
|
current_soc = test_soc
|
2729
2738
|
capacity = 14.54
|
@@ -2731,9 +2740,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2731
2740
|
bat_volt = 317.4
|
2732
2741
|
bat_power = 0.0
|
2733
2742
|
temperature = 30
|
2734
|
-
bms_charge_current =
|
2735
|
-
charge_loss = 1.
|
2736
|
-
discharge_loss = 0.
|
2743
|
+
bms_charge_current = 15
|
2744
|
+
charge_loss = 1.080
|
2745
|
+
discharge_loss = 0.975
|
2737
2746
|
bat_current = 0.0
|
2738
2747
|
device_power = 6.0
|
2739
2748
|
device_current = 35
|
@@ -2778,7 +2787,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2778
2787
|
output(f" Current SoC: {current_soc}%")
|
2779
2788
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2780
2789
|
output(f" Temperature: {temperature:.1f}°C")
|
2781
|
-
output(f" Charge
|
2790
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2782
2791
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2783
2792
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2784
2793
|
# charge current may be derated based on temperature
|
@@ -2955,7 +2964,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2955
2964
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
2956
2965
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
2957
2966
|
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)
|
2967
|
+
(bat_timed, kwh_min) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=time_to_end, kwh_min=capacity)
|
2959
2968
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
2960
2969
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
2961
2970
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3033,7 +3042,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3033
3042
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3034
3043
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3035
3044
|
# 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)
|
3045
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3037
3046
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3038
3047
|
# show the results
|
3039
3048
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3264,21 +3273,28 @@ def bat_count(cell_count):
|
|
3264
3273
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3265
3274
|
|
3266
3275
|
# 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)
|
3276
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3277
|
+
global debug_setting, battery_info_app_key
|
3271
3278
|
if bat is None:
|
3272
|
-
|
3279
|
+
bats = get_batteries(info=info)
|
3280
|
+
if bats is None:
|
3281
|
+
return None
|
3282
|
+
for i in range(0, len(bats)):
|
3283
|
+
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3284
|
+
battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
|
3273
3285
|
return None
|
3286
|
+
output_spool(battery_info_app_key)
|
3274
3287
|
nbat = None
|
3275
3288
|
if info == 1 and bat.get('info') is not None:
|
3276
|
-
|
3277
|
-
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
3289
|
+
b = bat['info']
|
3290
|
+
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3291
|
+
nbat = 0
|
3292
|
+
for s in b['slaveBatteries']:
|
3293
|
+
nbat += 1
|
3294
|
+
output(f"SN {s['sn']}, {s['batType']}, Version {s['version']} (Battery {nbat})")
|
3295
|
+
output()
|
3296
|
+
rated_capacity = bat.get('ratedCapacity')
|
3297
|
+
bat_soh = bat.get('soh')
|
3282
3298
|
bat_volt = bat['volt']
|
3283
3299
|
current_soc = bat['soc']
|
3284
3300
|
residual = bat['residual']
|
@@ -3329,7 +3345,10 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3329
3345
|
for v in cell_temps:
|
3330
3346
|
s +=f",{v:.0f}"
|
3331
3347
|
return s
|
3332
|
-
|
3348
|
+
if rated_capacity is not None:
|
3349
|
+
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3350
|
+
output(f"SoH: {bat_soh}%")
|
3351
|
+
output(f"Current SoC: {current_soc}%")
|
3333
3352
|
output(f"Capacity: {capacity:.2f}kWh")
|
3334
3353
|
output(f"Residual: {residual:.2f}kWh")
|
3335
3354
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.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
|
@@ -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,13 @@ This setting can be:
|
|
786
787
|
|
787
788
|
# Version Info
|
788
789
|
|
790
|
+
2.6.1<br>
|
791
|
+
Fix problem where battery discharges below min_soc while waiting for charging to start.
|
792
|
+
Update calibration for Force Charge with BMS 1.014 and later.
|
793
|
+
Add get_batteries() to return a list of BMS and batteries where inverters support more than 1 BMS.
|
794
|
+
Update battery_info() to support multiple BMS.
|
795
|
+
Add rated capacity and SoH to battery info if available.
|
796
|
+
|
789
797
|
2.6.0<br>
|
790
798
|
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
799
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|