foxesscloud 2.5.9__tar.gz → 2.6.0__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.5.9/src/foxesscloud.egg-info → foxesscloud-2.6.0}/PKG-INFO +10 -7
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/README.md +9 -6
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/pyproject.toml +1 -1
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/src/foxesscloud/foxesscloud.py +96 -70
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/src/foxesscloud/openapi.py +82 -63
- {foxesscloud-2.5.9 → foxesscloud-2.6.0/src/foxesscloud.egg-info}/PKG-INFO +10 -7
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/LICENCE +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/setup.cfg +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.0}/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.
|
3
|
+
Version: 2.6.0
|
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
|
@@ -116,7 +116,12 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
116
116
|
|
117
117
|
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
118
|
|
119
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery.
|
119
|
+
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery. Additional battery attributes include:
|
120
|
+
+ 'info': a list of BMS and battery serial numbers and firmware versions
|
121
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
122
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
123
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
124
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
120
125
|
|
121
126
|
get_settings() will return the battery settings and is equivalent to get_charge() and get_min(). The results are stored in f.battery_settings. The settings include minSoc, minSocOnGrid, enable charge from grid and the charge times.
|
122
127
|
|
@@ -371,8 +376,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
371
376
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
372
377
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
373
378
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
374
|
-
charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
|
375
|
-
discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
|
376
379
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
377
380
|
bms_power: 50 # BMS power consumption in W
|
378
381
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -395,9 +398,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
395
398
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
396
399
|
special_days: ['12-25', '12-26', '01-01']
|
397
400
|
full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
|
398
|
-
derate_temp: 28 # battery temperature in C when derating charge current is applied
|
399
|
-
derate_step: 5 # step size for derating e.g. 21, 16, 11
|
400
|
-
derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
|
401
401
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
402
402
|
data_wrap: 6 # data items to show per line
|
403
403
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -786,6 +786,9 @@ This setting can be:
|
|
786
786
|
|
787
787
|
# Version Info
|
788
788
|
|
789
|
+
2.6.0<br>
|
790
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
|
+
|
789
792
|
2.5.9<br>
|
790
793
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
791
794
|
Update charge calibration for new BMS firmware.
|
@@ -102,7 +102,12 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
102
102
|
|
103
103
|
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
104
|
|
105
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery.
|
105
|
+
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery. Additional battery attributes include:
|
106
|
+
+ 'info': a list of BMS and battery serial numbers and firmware versions
|
107
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
108
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
109
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
110
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
106
111
|
|
107
112
|
get_settings() will return the battery settings and is equivalent to get_charge() and get_min(). The results are stored in f.battery_settings. The settings include minSoc, minSocOnGrid, enable charge from grid and the charge times.
|
108
113
|
|
@@ -357,8 +362,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
357
362
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
358
363
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
359
364
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
360
|
-
charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
|
361
|
-
discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
|
362
365
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
363
366
|
bms_power: 50 # BMS power consumption in W
|
364
367
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -381,9 +384,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
381
384
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
382
385
|
special_days: ['12-25', '12-26', '01-01']
|
383
386
|
full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
|
384
|
-
derate_temp: 28 # battery temperature in C when derating charge current is applied
|
385
|
-
derate_step: 5 # step size for derating e.g. 21, 16, 11
|
386
|
-
derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
|
387
387
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
388
388
|
data_wrap: 6 # data items to show per line
|
389
389
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -772,6 +772,9 @@ This setting can be:
|
|
772
772
|
|
773
773
|
# Version Info
|
774
774
|
|
775
|
+
2.6.0<br>
|
776
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
777
|
+
|
775
778
|
2.5.9<br>
|
776
779
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
777
780
|
Update charge calibration for new BMS firmware.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 05 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.2"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -577,11 +577,27 @@ def get_firmware():
|
|
577
577
|
battery = None
|
578
578
|
battery_settings = None
|
579
579
|
|
580
|
-
# 1 =
|
580
|
+
# 1 = Residual Energy, 2 = Residual Capacity
|
581
581
|
residual_handling = 1
|
582
582
|
|
583
|
-
|
584
|
-
|
583
|
+
# charge rates based on residual_handling
|
584
|
+
battery_params = {
|
585
|
+
# cell temp -5 0 5 10 15 20 25 30 35 40 45 50 55
|
586
|
+
# bms temp 5 10 15 20 25 30 35 40 45 50 55 60 65
|
587
|
+
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
588
|
+
'step': 5,
|
589
|
+
'offset': 5,
|
590
|
+
'charge_loss': 0.975,
|
591
|
+
'discharge_loss': 0.975},
|
592
|
+
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
593
|
+
'step': 5,
|
594
|
+
'offset': 5,
|
595
|
+
'charge_loss': 1.040,
|
596
|
+
'discharge_loss': 0.975},
|
597
|
+
}
|
598
|
+
|
599
|
+
def get_battery(info=1):
|
600
|
+
global token, device_id, battery, debug_setting, messages, residual_handling, battery_params
|
585
601
|
if get_device() is None:
|
586
602
|
return None
|
587
603
|
output(f"getting battery", 2)
|
@@ -595,14 +611,11 @@ def get_battery(info=0):
|
|
595
611
|
errno = response.json().get('errno')
|
596
612
|
output(f"** get_battery(), no result data, {errno_message(errno)}")
|
597
613
|
return None
|
614
|
+
saved_info = battery['info'] if battery is not None and battery.get('info') is not None else None
|
598
615
|
battery = result
|
599
|
-
if
|
600
|
-
battery['
|
601
|
-
|
602
|
-
capacity = battery.get('residual')
|
603
|
-
soc = battery.get('soc')
|
604
|
-
battery['residual'] = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
605
|
-
if info == 1:
|
616
|
+
if saved_info is not None:
|
617
|
+
battery['info'] = saved_info
|
618
|
+
elif info == 1:
|
606
619
|
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
607
620
|
if response.status_code != 200:
|
608
621
|
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
@@ -613,6 +626,26 @@ def get_battery(info=0):
|
|
613
626
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
614
627
|
else:
|
615
628
|
battery['info'] = result['batteries']
|
629
|
+
if battery['info'][0]['masterVersion'] >= '1.014':
|
630
|
+
residual_handling = 2
|
631
|
+
if battery.get('residual') is not None:
|
632
|
+
battery['residual'] /= 1000
|
633
|
+
if residual_handling == 2:
|
634
|
+
capacity = battery.get('residual')
|
635
|
+
soc = battery.get('soc')
|
636
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
637
|
+
else:
|
638
|
+
residual = battery.get('residual')
|
639
|
+
soc = battery.get('soc')
|
640
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
641
|
+
battery['capacity'] = round(capacity, 3)
|
642
|
+
battery['residual'] = round(residual, 3)
|
643
|
+
battery['charge_rate'] = 50
|
644
|
+
params = battery_params[residual_handling]
|
645
|
+
battery['charge_loss'] = params['charge_loss']
|
646
|
+
battery['discharge_loss'] = params['discharge_loss']
|
647
|
+
if battery.get('temperature') is not None:
|
648
|
+
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
616
649
|
return battery
|
617
650
|
|
618
651
|
##################################################################################################
|
@@ -2545,7 +2578,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2545
2578
|
profile = []
|
2546
2579
|
h = base_hour - time_offset
|
2547
2580
|
while h < 0:
|
2548
|
-
profile.append(
|
2581
|
+
profile.append(None)
|
2549
2582
|
h += 1 / steps_per_hour
|
2550
2583
|
while h < 48:
|
2551
2584
|
day = today if h < 24 else tomorrow
|
@@ -2555,10 +2588,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2555
2588
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2556
2589
|
else:
|
2557
2590
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2558
|
-
profile.append(
|
2591
|
+
profile.append(value)
|
2559
2592
|
h += 1 / steps_per_hour
|
2560
2593
|
while len(profile) < run_time:
|
2561
|
-
profile.append(
|
2594
|
+
profile.append(None)
|
2562
2595
|
return profile[:run_time]
|
2563
2596
|
|
2564
2597
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2596,11 +2629,11 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2596
2629
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2597
2630
|
# all power values are as measured at the inverter battery connection
|
2598
2631
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2599
|
-
global charge_config, steps_per_hour
|
2632
|
+
global charge_config, steps_per_hour
|
2600
2633
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2601
2634
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2602
|
-
charge_loss = charge_config['charge_loss']
|
2603
|
-
discharge_loss = charge_config['discharge_loss']
|
2635
|
+
charge_loss = charge_config['charge_loss']
|
2636
|
+
discharge_loss = charge_config['discharge_loss']
|
2604
2637
|
charge_limit = charge_config['charge_limit']
|
2605
2638
|
float_charge = charge_config['float_charge']
|
2606
2639
|
for i in range(0, len(work_mode_timed)):
|
@@ -2691,8 +2724,6 @@ charge_config = {
|
|
2691
2724
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2692
2725
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2693
2726
|
'ac_dc_loss': 0.960, # loss converting AC grid power to DC battery charge power
|
2694
|
-
'charge_loss': [0.975, 1.040], # loss in battery energy for each kWh added (based on residual_handling)
|
2695
|
-
'discharge_loss': [0.975, 0.975], # loss in battery energy for each kWh removed (based on residual_handling)
|
2696
2727
|
'inverter_power': 101, # Inverter power consumption in W
|
2697
2728
|
'bms_power': 50, # BMS power consumption in W
|
2698
2729
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2713,9 +2744,6 @@ charge_config = {
|
|
2713
2744
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2714
2745
|
'special_days': ['12-25', '12-26', '01-01'],
|
2715
2746
|
'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
|
2716
|
-
'derate_temp': 28, # BMS temperature when cold derating starts to be applied
|
2717
|
-
'derate_step': 5, # scale for derating factors in C
|
2718
|
-
'derating': [24, 15, 10, 2], # max charge current de-rating
|
2719
2747
|
'data_wrap': 6, # data items to show per line
|
2720
2748
|
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2721
2749
|
'shading': { # effect of shading on Solcast / forecast.solar
|
@@ -2740,7 +2768,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2740
2768
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2741
2769
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2742
2770
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2743
|
-
global timed_strategy, steps_per_hour, base_time, storage,
|
2771
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2744
2772
|
print(f"\n---------------- charge_needed ----------------")
|
2745
2773
|
# validate parameters
|
2746
2774
|
args = locals()
|
@@ -2840,8 +2868,22 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2840
2868
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2841
2869
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2842
2870
|
output(f"time_to_next = {time_to_next}, full_charge = {full_charge}")
|
2871
|
+
if test_soc is not None:
|
2872
|
+
current_soc = test_soc
|
2873
|
+
capacity = 14.54
|
2874
|
+
residual = test_soc * capacity / 100
|
2875
|
+
bat_volt = 317.4
|
2876
|
+
bat_power = 0.0
|
2877
|
+
temperature = 30
|
2878
|
+
bms_charge_current = 25
|
2879
|
+
charge_loss = 1.040
|
2880
|
+
discharge_loss = 0.974
|
2881
|
+
bat_current = 0.0
|
2882
|
+
device_power = 6.0
|
2883
|
+
device_current = 35
|
2884
|
+
model = 'H1-6.0-E'
|
2885
|
+
else:
|
2843
2886
|
# get device and battery info from inverter
|
2844
|
-
if test_soc is None:
|
2845
2887
|
get_battery()
|
2846
2888
|
if battery is None or battery['status'] != 1:
|
2847
2889
|
output(f"\nBattery status is not available")
|
@@ -2852,27 +2894,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2852
2894
|
bat_current = battery['current']
|
2853
2895
|
temperature = battery['temperature']
|
2854
2896
|
residual = battery['residual']
|
2855
|
-
if charge_config.get('capacity') is not None
|
2856
|
-
|
2857
|
-
elif residual is not None and residual > 0.2 and current_soc is not None and current_soc > 1:
|
2858
|
-
capacity = residual * 100 / current_soc
|
2859
|
-
else:
|
2897
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2898
|
+
if capacity is None:
|
2860
2899
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2861
2900
|
return None
|
2901
|
+
bms_charge_current = battery.get('charge_rate')
|
2902
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 1.0
|
2903
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 1.0
|
2862
2904
|
device_power = device.get('power')
|
2863
2905
|
device_current = device.get('max_charge_current')
|
2864
2906
|
model = device.get('deviceType')
|
2865
|
-
else:
|
2866
|
-
current_soc = test_soc
|
2867
|
-
capacity = 14.54
|
2868
|
-
residual = test_soc * capacity / 100
|
2869
|
-
bat_volt = 317.4
|
2870
|
-
bat_power = 0.0
|
2871
|
-
temperature = 30
|
2872
|
-
bat_current = 0.0
|
2873
|
-
device_power = 6.0
|
2874
|
-
device_current = 25
|
2875
|
-
model = 'H1-6.0-E'
|
2876
2907
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2877
2908
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2878
2909
|
volt_curve = charge_config['volt_curve']
|
@@ -2890,27 +2921,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2890
2921
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
2891
2922
|
output(f" Current SoC: {current_soc}%")
|
2892
2923
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2924
|
+
output(f" Charge Rate: {bms_charge_current:.1f}A")
|
2893
2925
|
output(f" Temperature: {temperature:.1f}°C")
|
2894
2926
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2895
2927
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2896
|
-
# charge
|
2928
|
+
# charge current may be derated based on temperature
|
2897
2929
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2898
|
-
|
2899
|
-
|
2900
|
-
output(f"\nHigh battery temperature may affect the charge rate")
|
2901
|
-
elif round(temperature, 0) <= derate_temp:
|
2902
|
-
output(f"\nLow battery temperature may affect the charge rate")
|
2903
|
-
derating = charge_config['derating']
|
2904
|
-
derate_step = charge_config['derate_step']
|
2905
|
-
i = int((derate_temp - temperature) / (derate_step if derate_step is not None and derate_step > 0 else 1))
|
2906
|
-
if derating is not None and type(derating) is list and i < len(derating):
|
2907
|
-
derated_current = derating[i]
|
2908
|
-
if derated_current < charge_current:
|
2909
|
-
output(f" Charge current reduced from {charge_current:.0f}A to {derated_current:.0f}A" )
|
2910
|
-
charge_current = derated_current
|
2911
|
-
else:
|
2912
|
-
bat_hold = 2
|
2913
|
-
output(f" Full charge set")
|
2930
|
+
if charge_current > bms_charge_current:
|
2931
|
+
charge_current = bms_charge_current
|
2914
2932
|
# inverter losses
|
2915
2933
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2916
2934
|
operating_loss = inverter_power / 1000
|
@@ -2924,21 +2942,24 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2924
2942
|
force_charge_power = charge_config['force_charge_power'] if timed_mode > 1 and charge_config.get('force_charge_power') is not None else 100
|
2925
2943
|
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2926
2944
|
float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
|
2927
|
-
charge_config['
|
2928
|
-
charge_config['charge_power'] = charge_power
|
2929
|
-
charge_config['float_charge'] = float_charge
|
2930
|
-
charge_loss = charge_config['charge_loss'][residual_handling - 1]
|
2945
|
+
pv_loss = charge_config['pv_loss']
|
2931
2946
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2932
2947
|
dc_ac_loss = charge_config['dc_ac_loss']
|
2933
2948
|
discharge_limit = device_power / dc_ac_loss
|
2934
2949
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2935
2950
|
discharge_power = discharge_current * bat_ocv / 1000
|
2936
2951
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2937
|
-
discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
|
2938
2952
|
# charging happens if generation exceeds export limit in feedin work mode
|
2939
2953
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2940
2954
|
export_limit = export_power / dc_ac_loss
|
2941
2955
|
current_mode = get_work_mode()
|
2956
|
+
# set parameters for battery_timed()
|
2957
|
+
charge_config['charge_limit'] = charge_limit
|
2958
|
+
charge_config['charge_power'] = charge_power
|
2959
|
+
charge_config['float_charge'] = float_charge
|
2960
|
+
charge_config['charge_loss'] = charge_loss
|
2961
|
+
charge_config['discharge_loss'] = discharge_loss
|
2962
|
+
# display what we have
|
2942
2963
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2943
2964
|
output(f"\nDevice Info:")
|
2944
2965
|
output(f" Model: {model}")
|
@@ -3048,8 +3069,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3048
3069
|
output(f"\nSettings will not be updated when forecast is not available")
|
3049
3070
|
update_settings = 0
|
3050
3071
|
# produce time lines for charge, discharge and work mode
|
3051
|
-
charge_timed = [min([charge_limit, x *
|
3052
|
-
discharge_timed = [min([discharge_limit, x / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
3072
|
+
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3073
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
3053
3074
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3054
3075
|
for i in range(0, len(work_mode_timed)):
|
3055
3076
|
# get work mode
|
@@ -3240,10 +3261,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3240
3261
|
# CHARGE_COMPARE - load saved data and compare with actual
|
3241
3262
|
##################################################################################################
|
3242
3263
|
|
3243
|
-
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3264
|
+
def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
3244
3265
|
global charge_config, storage
|
3266
|
+
now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
|
3267
|
+
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3245
3268
|
if save is None and charge_config.get('save') is not None:
|
3246
|
-
save = charge_config.get('save').replace('###',
|
3269
|
+
save = charge_config.get('save').replace('###', yesterday)
|
3270
|
+
if not os.path.exists(storage + save):
|
3271
|
+
save = None
|
3247
3272
|
if save is None:
|
3248
3273
|
print(f"** charge_compare(): please provide a saved file to load")
|
3249
3274
|
return
|
@@ -3387,14 +3412,14 @@ battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
|
3387
3412
|
|
3388
3413
|
# show information about the current state of the batteries
|
3389
3414
|
def battery_info(log=0, plot=1, count=None, info=1):
|
3390
|
-
global debug_setting, battery_info_app_key
|
3415
|
+
global debug_setting, battery_info_app_key, residual_handling
|
3391
3416
|
output_spool(battery_info_app_key)
|
3392
3417
|
bat = get_battery(info=info)
|
3393
3418
|
if bat is None:
|
3394
3419
|
output_close()
|
3395
3420
|
return None
|
3396
3421
|
nbat = None
|
3397
|
-
if bat.get('info') is not None:
|
3422
|
+
if info == 1 and bat.get('info') is not None:
|
3398
3423
|
for b in bat['info']:
|
3399
3424
|
output(f"\nSN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3400
3425
|
nbat = 0
|
@@ -3407,7 +3432,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3407
3432
|
bat_current = bat['current']
|
3408
3433
|
bat_power = bat['power']
|
3409
3434
|
bms_temperature = bat['temperature']
|
3410
|
-
capacity =
|
3435
|
+
capacity = bat['capacity']
|
3411
3436
|
cell_volts = get_cell_volts()
|
3412
3437
|
if cell_volts is None:
|
3413
3438
|
output_close()
|
@@ -3462,6 +3487,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3462
3487
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3463
3488
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3464
3489
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3490
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3465
3491
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3466
3492
|
output(f"\nInfo by battery:")
|
3467
3493
|
for i in range(0, nbat):
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 05 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.
|
13
|
+
version = "2.6.0"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -545,8 +545,24 @@ battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
|
545
545
|
# 1 = returns Residual Energy. 2 = resturns Residual Capacity
|
546
546
|
residual_handling = 1
|
547
547
|
|
548
|
+
# charge rates based on residual_handling
|
549
|
+
battery_params = {
|
550
|
+
# cell temp -5 0 5 10 15 20 25 30 35 40 45 50 55
|
551
|
+
# bms temp 5 10 15 20 25 30 35 40 45 50 55 60 65
|
552
|
+
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
553
|
+
'step': 5,
|
554
|
+
'offset': 5,
|
555
|
+
'charge_loss': 0.975,
|
556
|
+
'discharge_loss': 0.975},
|
557
|
+
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
558
|
+
'step': 5,
|
559
|
+
'offset': 5,
|
560
|
+
'charge_loss': 1.040,
|
561
|
+
'discharge_loss': 0.975},
|
562
|
+
}
|
563
|
+
|
548
564
|
def get_battery(v = None, info=0):
|
549
|
-
global device_sn, battery, debug_setting, residual_handling
|
565
|
+
global device_sn, battery, debug_setting, residual_handling, battery_params
|
550
566
|
if get_device() is None:
|
551
567
|
return None
|
552
568
|
output(f"getting battery", 2)
|
@@ -560,10 +576,20 @@ def get_battery(v = None, info=0):
|
|
560
576
|
if residual_handling == 2:
|
561
577
|
capacity = battery.get('residual')
|
562
578
|
soc = battery.get('soc')
|
563
|
-
|
564
|
-
|
565
|
-
|
579
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
580
|
+
else:
|
581
|
+
residual = battery.get('residual')
|
582
|
+
soc = battery.get('soc')
|
583
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
584
|
+
battery['capacity'] = round(capacity, 3)
|
585
|
+
battery['residual'] = round(residual, 3)
|
566
586
|
battery['status'] = 1
|
587
|
+
battery['charge_rate'] = 50
|
588
|
+
params = battery_params[residual_handling]
|
589
|
+
battery['charge_loss'] = params['charge_loss']
|
590
|
+
battery['discharge_loss'] = params['discharge_loss']
|
591
|
+
if battery.get('temperature') is not None:
|
592
|
+
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
567
593
|
return battery
|
568
594
|
|
569
595
|
##################################################################################################
|
@@ -2408,7 +2434,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2408
2434
|
profile = []
|
2409
2435
|
h = base_hour - time_offset
|
2410
2436
|
while h < 0:
|
2411
|
-
profile.append(
|
2437
|
+
profile.append(None)
|
2412
2438
|
h += 1 / steps_per_hour
|
2413
2439
|
while h < 48:
|
2414
2440
|
day = today if h < 24 else tomorrow
|
@@ -2418,10 +2444,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2418
2444
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2419
2445
|
else:
|
2420
2446
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2421
|
-
profile.append(
|
2447
|
+
profile.append(value)
|
2422
2448
|
h += 1 / steps_per_hour
|
2423
2449
|
while len(profile) < run_time:
|
2424
|
-
profile.append(
|
2450
|
+
profile.append(None)
|
2425
2451
|
return profile[:run_time]
|
2426
2452
|
|
2427
2453
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2459,11 +2485,11 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2459
2485
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2460
2486
|
# note: all power values are as measured at the inverter battery connection
|
2461
2487
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2462
|
-
global charge_config, steps_per_hour
|
2488
|
+
global charge_config, steps_per_hour
|
2463
2489
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2464
2490
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2465
|
-
charge_loss = charge_config['charge_loss']
|
2466
|
-
discharge_loss = charge_config['discharge_loss']
|
2491
|
+
charge_loss = charge_config['charge_loss']
|
2492
|
+
discharge_loss = charge_config['discharge_loss']
|
2467
2493
|
charge_limit = charge_config['charge_limit']
|
2468
2494
|
float_charge = charge_config['float_charge']
|
2469
2495
|
for i in range(0, len(work_mode_timed)):
|
@@ -2554,8 +2580,6 @@ charge_config = {
|
|
2554
2580
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2555
2581
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2556
2582
|
'ac_dc_loss': 0.962, # loss converting AC grid power to DC battery charge power
|
2557
|
-
'charge_loss': [0.975, 1.040], # loss in battery energy for each kWh added based on residual_handling
|
2558
|
-
'discharge_loss': [0.975, 0.975], # loss in battery energy for each kWh removed based on residual_handling
|
2559
2583
|
'inverter_power': 101, # Inverter power consumption in W
|
2560
2584
|
'bms_power': 50, # BMS power consumption in W
|
2561
2585
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2576,9 +2600,6 @@ charge_config = {
|
|
2576
2600
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2577
2601
|
'special_days': ['12-25', '12-26', '01-01'],
|
2578
2602
|
'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
|
2579
|
-
'derate_temp': 25, # BMS temperature when cold derating starts to be applied
|
2580
|
-
'derate_step': 5, # scale for derating factors in C
|
2581
|
-
'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 25C, 20C, 15C, 10C
|
2582
2603
|
'data_wrap': 6, # data items to show per line
|
2583
2604
|
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2584
2605
|
'shading': { # effect of shading on Solcast / forecast.solar
|
@@ -2603,7 +2624,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2603
2624
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2604
2625
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2605
2626
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2606
|
-
global timed_strategy, steps_per_hour, base_time, storage,
|
2627
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2607
2628
|
print(f"\n---------------- charge_needed ----------------")
|
2608
2629
|
# validate parameters
|
2609
2630
|
args = locals()
|
@@ -2703,8 +2724,22 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2703
2724
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2704
2725
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2705
2726
|
output(f"time_to_next = {time_to_next}, full_charge = {full_charge}")
|
2727
|
+
if test_soc is not None:
|
2728
|
+
current_soc = test_soc
|
2729
|
+
capacity = 14.54
|
2730
|
+
residual = test_soc * capacity / 100
|
2731
|
+
bat_volt = 317.4
|
2732
|
+
bat_power = 0.0
|
2733
|
+
temperature = 30
|
2734
|
+
bms_charge_current = 25
|
2735
|
+
charge_loss = 1.040
|
2736
|
+
discharge_loss = 0.974
|
2737
|
+
bat_current = 0.0
|
2738
|
+
device_power = 6.0
|
2739
|
+
device_current = 35
|
2740
|
+
model = 'H1-6.0-E'
|
2741
|
+
else:
|
2706
2742
|
# get device and battery info from inverter
|
2707
|
-
if test_soc is None:
|
2708
2743
|
get_battery()
|
2709
2744
|
if battery is None or battery['status'] != 1:
|
2710
2745
|
output(f"\nBattery status is not available")
|
@@ -2715,27 +2750,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2715
2750
|
bat_current = battery['current']
|
2716
2751
|
temperature = battery['temperature']
|
2717
2752
|
residual = battery['residual']
|
2718
|
-
if charge_config.get('capacity') is not None
|
2719
|
-
|
2720
|
-
elif residual is not None and residual > 0.2 and current_soc is not None and current_soc > 1:
|
2721
|
-
capacity = residual * 100 / current_soc
|
2722
|
-
else:
|
2753
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2754
|
+
if capacity is None:
|
2723
2755
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2724
2756
|
return None
|
2757
|
+
bms_charge_current = battery.get('charge_rate')
|
2758
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 1.0
|
2759
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 1.0
|
2725
2760
|
device_power = device.get('power')
|
2726
2761
|
device_current = device.get('max_charge_current')
|
2727
2762
|
model = device.get('deviceType')
|
2728
|
-
else:
|
2729
|
-
current_soc = test_soc
|
2730
|
-
capacity = 14.54
|
2731
|
-
residual = test_soc * capacity / 100
|
2732
|
-
bat_volt = 317.4
|
2733
|
-
bat_power = 0.0
|
2734
|
-
temperature = 30
|
2735
|
-
bat_current = 0.0
|
2736
|
-
device_power = 6.0
|
2737
|
-
device_current = 25
|
2738
|
-
model = 'H1-6.0-E'
|
2739
2763
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2740
2764
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2741
2765
|
volt_curve = charge_config['volt_curve']
|
@@ -2754,26 +2778,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2754
2778
|
output(f" Current SoC: {current_soc}%")
|
2755
2779
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2756
2780
|
output(f" Temperature: {temperature:.1f}°C")
|
2781
|
+
output(f" Charge Rate: {bms_charge_current:.1f}A")
|
2757
2782
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2758
2783
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2759
|
-
# charge
|
2784
|
+
# charge current may be derated based on temperature
|
2760
2785
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2761
|
-
|
2762
|
-
|
2763
|
-
output(f"\nHigh battery temperature may affect the charge rate")
|
2764
|
-
elif round(temperature, 0) <= derate_temp:
|
2765
|
-
output(f"\nLow battery temperature may affect the charge rate")
|
2766
|
-
derating = charge_config['derating']
|
2767
|
-
derate_step = charge_config['derate_step']
|
2768
|
-
i = int((derate_temp - temperature) / (derate_step if derate_step is not None and derate_step > 0 else 1))
|
2769
|
-
if derating is not None and type(derating) is list and i < len(derating):
|
2770
|
-
derated_current = derating[i]
|
2771
|
-
if derated_current < charge_current:
|
2772
|
-
output(f" Charge current reduced from {charge_current:.0f}A to {derated_current:.0f}A" )
|
2773
|
-
charge_current = derated_current
|
2774
|
-
else:
|
2775
|
-
bat_hold = 2
|
2776
|
-
output(f" Full charge set")
|
2786
|
+
if charge_current > bms_charge_current:
|
2787
|
+
charge_current = bms_charge_current
|
2777
2788
|
# inverter losses
|
2778
2789
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2779
2790
|
operating_loss = inverter_power / 1000
|
@@ -2787,21 +2798,24 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2787
2798
|
force_charge_power = charge_config['force_charge_power'] if timed_mode > 1 and charge_config.get('force_charge_power') is not None else 100
|
2788
2799
|
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2789
2800
|
float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
|
2790
|
-
charge_config['
|
2791
|
-
charge_config['charge_power'] = charge_power
|
2792
|
-
charge_config['float_charge'] = float_charge
|
2793
|
-
charge_loss = charge_config['charge_loss'][residual_handling - 1]
|
2801
|
+
pv_loss = charge_config['pv_loss']
|
2794
2802
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2795
2803
|
dc_ac_loss = charge_config['dc_ac_loss']
|
2796
2804
|
discharge_limit = device_power / dc_ac_loss
|
2797
2805
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2798
2806
|
discharge_power = discharge_current * bat_ocv / 1000
|
2799
2807
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2800
|
-
discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
|
2801
2808
|
# charging happens if generation exceeds export limit in feedin work mode
|
2802
2809
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2803
2810
|
export_limit = export_power / dc_ac_loss
|
2804
2811
|
current_mode = get_work_mode()
|
2812
|
+
# set parameters for battery_timed()
|
2813
|
+
charge_config['charge_limit'] = charge_limit
|
2814
|
+
charge_config['charge_power'] = charge_power
|
2815
|
+
charge_config['float_charge'] = float_charge
|
2816
|
+
charge_config['charge_loss'] = charge_loss
|
2817
|
+
charge_config['discharge_loss'] = discharge_loss
|
2818
|
+
# display what we have
|
2805
2819
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2806
2820
|
output(f"\nDevice Info:")
|
2807
2821
|
output(f" Model: {model}")
|
@@ -2910,8 +2924,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2910
2924
|
output(f"\nSettings will not be updated when forecast is not available")
|
2911
2925
|
update_settings = 0
|
2912
2926
|
# produce time lines for charge, discharge and work mode
|
2913
|
-
charge_timed = [min([charge_limit, x *
|
2914
|
-
discharge_timed = [min([discharge_limit, x / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
2927
|
+
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2928
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
2915
2929
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2916
2930
|
for i in range(0, len(work_mode_timed)):
|
2917
2931
|
# get work mode
|
@@ -3103,8 +3117,12 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3103
3117
|
|
3104
3118
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3105
3119
|
global charge_config, storage
|
3120
|
+
now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
|
3121
|
+
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3106
3122
|
if save is None and charge_config.get('save') is not None:
|
3107
|
-
save = charge_config.get('save').replace('###',
|
3123
|
+
save = charge_config.get('save').replace('###', yesterday)
|
3124
|
+
if not os.path.exists(storage + save):
|
3125
|
+
save = None
|
3108
3126
|
if save is None:
|
3109
3127
|
print(f"** charge_compare(): please provide a saved file to load")
|
3110
3128
|
return
|
@@ -3247,14 +3265,14 @@ battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
|
3247
3265
|
|
3248
3266
|
# show information about the current state of the batteries
|
3249
3267
|
def battery_info(log=0, plot=1, count=None, info=1):
|
3250
|
-
global debug_setting, battery_info_app_key
|
3268
|
+
global debug_setting, battery_info_app_key, residual_handling
|
3251
3269
|
output_spool(battery_info_app_key)
|
3252
3270
|
bat = get_battery(info=info)
|
3253
3271
|
if bat is None:
|
3254
3272
|
output_close()
|
3255
3273
|
return None
|
3256
3274
|
nbat = None
|
3257
|
-
if bat.get('info') is not None:
|
3275
|
+
if info == 1 and bat.get('info') is not None:
|
3258
3276
|
for b in bat['info']:
|
3259
3277
|
output(f"\nSN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3260
3278
|
nbat = 0
|
@@ -3267,7 +3285,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3267
3285
|
bat_current = bat['current']
|
3268
3286
|
bat_power = bat['power']
|
3269
3287
|
bms_temperature = bat['temperature']
|
3270
|
-
capacity =
|
3288
|
+
capacity = bat['capacity']
|
3271
3289
|
cell_volts = get_cell_volts()
|
3272
3290
|
if cell_volts is None:
|
3273
3291
|
output_close()
|
@@ -3322,6 +3340,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3322
3340
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3323
3341
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3324
3342
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3343
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3325
3344
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3326
3345
|
output(f"\nInfo by battery:")
|
3327
3346
|
for i in range(0, nbat):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.6.0
|
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
|
@@ -116,7 +116,12 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
116
116
|
|
117
117
|
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
118
|
|
119
|
-
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery.
|
119
|
+
get_battery() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery. Additional battery attributes include:
|
120
|
+
+ 'info': a list of BMS and battery serial numbers and firmware versions
|
121
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
122
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
123
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
124
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
120
125
|
|
121
126
|
get_settings() will return the battery settings and is equivalent to get_charge() and get_min(). The results are stored in f.battery_settings. The settings include minSoc, minSocOnGrid, enable charge from grid and the charge times.
|
122
127
|
|
@@ -371,8 +376,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
371
376
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
372
377
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
373
378
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
374
|
-
charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
|
375
|
-
discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
|
376
379
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
377
380
|
bms_power: 50 # BMS power consumption in W
|
378
381
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -395,9 +398,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
395
398
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
396
399
|
special_days: ['12-25', '12-26', '01-01']
|
397
400
|
full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
|
398
|
-
derate_temp: 28 # battery temperature in C when derating charge current is applied
|
399
|
-
derate_step: 5 # step size for derating e.g. 21, 16, 11
|
400
|
-
derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
|
401
401
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
402
402
|
data_wrap: 6 # data items to show per line
|
403
403
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -786,6 +786,9 @@ This setting can be:
|
|
786
786
|
|
787
787
|
# Version Info
|
788
788
|
|
789
|
+
2.6.0<br>
|
790
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
791
|
+
|
789
792
|
2.5.9<br>
|
790
793
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
791
794
|
Update charge calibration for new BMS firmware.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|