foxesscloud 2.5.9__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.5.9 → foxesscloud-2.6.1}/PKG-INFO +18 -7
- foxesscloud-2.5.9/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.6.1/README.md +17 -20
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/pyproject.toml +1 -1
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/src/foxesscloud/foxesscloud.py +164 -82
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/src/foxesscloud/openapi.py +117 -79
- foxesscloud-2.5.9/README.md → foxesscloud-2.6.1/src/foxesscloud.egg-info/PKG-INFO +31 -6
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/LICENCE +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/setup.cfg +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.5.9 → foxesscloud-2.6.1}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.5.9 → 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.
|
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,7 +117,12 @@ 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
|
+
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:
|
122
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
123
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
124
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
125
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
120
126
|
|
121
127
|
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
128
|
|
@@ -371,8 +377,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
371
377
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
372
378
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
373
379
|
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
380
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
377
381
|
bms_power: 50 # BMS power consumption in W
|
378
382
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -395,9 +399,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
395
399
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
396
400
|
special_days: ['12-25', '12-26', '01-01']
|
397
401
|
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
402
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
402
403
|
data_wrap: 6 # data items to show per line
|
403
404
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -786,6 +787,16 @@ 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
|
+
|
797
|
+
2.6.0<br>
|
798
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
799
|
+
|
789
800
|
2.5.9<br>
|
790
801
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
791
802
|
Update charge calibration for new BMS firmware.
|
@@ -1,17 +1,3 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: foxesscloud
|
3
|
-
Version: 2.5.9
|
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,7 +103,12 @@ 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.
|
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:
|
108
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
109
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
110
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
111
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
120
112
|
|
121
113
|
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
114
|
|
@@ -371,8 +363,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
371
363
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
372
364
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
373
365
|
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
366
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
377
367
|
bms_power: 50 # BMS power consumption in W
|
378
368
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -395,9 +385,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
395
385
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
396
386
|
special_days: ['12-25', '12-26', '01-01']
|
397
387
|
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
388
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
402
389
|
data_wrap: 6 # data items to show per line
|
403
390
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -786,6 +773,16 @@ This setting can be:
|
|
786
773
|
|
787
774
|
# Version Info
|
788
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
|
+
|
783
|
+
2.6.0<br>
|
784
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
785
|
+
|
789
786
|
2.5.9<br>
|
790
787
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
791
788
|
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: 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,13 +576,30 @@ def get_firmware():
|
|
575
576
|
##################################################################################################
|
576
577
|
|
577
578
|
battery = None
|
579
|
+
batteries = None
|
578
580
|
battery_settings = None
|
579
581
|
|
580
|
-
# 1 =
|
582
|
+
# 1 = Residual Energy, 2 = Residual Capacity
|
581
583
|
residual_handling = 1
|
582
584
|
|
583
|
-
|
584
|
-
|
585
|
+
# charge rates based on residual_handling
|
586
|
+
battery_params = {
|
587
|
+
# cell temp -5 0 5 10 15 20 25 30 35 40 45 50 55
|
588
|
+
# bms temp 5 10 15 20 25 30 35 40 45 50 55 60 65
|
589
|
+
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
590
|
+
'step': 5,
|
591
|
+
'offset': 5,
|
592
|
+
'charge_loss': 0.975,
|
593
|
+
'discharge_loss': 0.975},
|
594
|
+
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
595
|
+
'step': 5,
|
596
|
+
'offset': 5,
|
597
|
+
'charge_loss': 1.080,
|
598
|
+
'discharge_loss': 0.975},
|
599
|
+
}
|
600
|
+
|
601
|
+
def get_battery(info=1):
|
602
|
+
global token, device_id, battery, debug_setting, messages, residual_handling, battery_params
|
585
603
|
if get_device() is None:
|
586
604
|
return None
|
587
605
|
output(f"getting battery", 2)
|
@@ -595,13 +613,59 @@ def get_battery(info=0):
|
|
595
613
|
errno = response.json().get('errno')
|
596
614
|
output(f"** get_battery(), no result data, {errno_message(errno)}")
|
597
615
|
return None
|
616
|
+
saved_info = battery['info'] if battery is not None and battery.get('info') is not None else None
|
598
617
|
battery = result
|
618
|
+
if saved_info is not None:
|
619
|
+
battery['info'] = saved_info
|
620
|
+
elif info == 1:
|
621
|
+
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
622
|
+
if response.status_code != 200:
|
623
|
+
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
624
|
+
else:
|
625
|
+
result = response.json().get('result')
|
626
|
+
if result is None:
|
627
|
+
errno = response.json().get('errno')
|
628
|
+
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
629
|
+
else:
|
630
|
+
battery['info'] = result['batteries'][0]
|
631
|
+
if battery['info']['masterVersion'] >= '1.014':
|
632
|
+
residual_handling = 2
|
599
633
|
if battery.get('residual') is not None:
|
600
|
-
battery['residual'] /=1000
|
634
|
+
battery['residual'] /= 1000
|
601
635
|
if residual_handling == 2:
|
602
636
|
capacity = battery.get('residual')
|
603
637
|
soc = battery.get('soc')
|
604
|
-
|
638
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
639
|
+
else:
|
640
|
+
residual = battery.get('residual')
|
641
|
+
soc = battery.get('soc')
|
642
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
643
|
+
battery['capacity'] = round(capacity, 3)
|
644
|
+
battery['residual'] = round(residual, 3)
|
645
|
+
battery['charge_rate'] = 50
|
646
|
+
params = battery_params[residual_handling]
|
647
|
+
battery['charge_loss'] = params['charge_loss']
|
648
|
+
battery['discharge_loss'] = params['discharge_loss']
|
649
|
+
if battery.get('temperature') is not None:
|
650
|
+
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
651
|
+
return battery
|
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']
|
605
669
|
if info == 1:
|
606
670
|
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
607
671
|
if response.status_code != 200:
|
@@ -612,8 +676,24 @@ def get_battery(info=0):
|
|
612
676
|
errno = response.json().get('errno')
|
613
677
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
614
678
|
else:
|
615
|
-
|
616
|
-
|
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
|
617
697
|
|
618
698
|
##################################################################################################
|
619
699
|
# get charge times and save to battery_settings
|
@@ -2545,7 +2625,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2545
2625
|
profile = []
|
2546
2626
|
h = base_hour - time_offset
|
2547
2627
|
while h < 0:
|
2548
|
-
profile.append(
|
2628
|
+
profile.append(None)
|
2549
2629
|
h += 1 / steps_per_hour
|
2550
2630
|
while h < 48:
|
2551
2631
|
day = today if h < 24 else tomorrow
|
@@ -2555,10 +2635,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2555
2635
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2556
2636
|
else:
|
2557
2637
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2558
|
-
profile.append(
|
2638
|
+
profile.append(value)
|
2559
2639
|
h += 1 / steps_per_hour
|
2560
2640
|
while len(profile) < run_time:
|
2561
|
-
profile.append(
|
2641
|
+
profile.append(None)
|
2562
2642
|
return profile[:run_time]
|
2563
2643
|
|
2564
2644
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2596,11 +2676,11 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2596
2676
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2597
2677
|
# all power values are as measured at the inverter battery connection
|
2598
2678
|
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
|
2679
|
+
global charge_config, steps_per_hour
|
2600
2680
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2601
2681
|
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']
|
2682
|
+
charge_loss = charge_config['charge_loss']
|
2683
|
+
discharge_loss = charge_config['discharge_loss']
|
2604
2684
|
charge_limit = charge_config['charge_limit']
|
2605
2685
|
float_charge = charge_config['float_charge']
|
2606
2686
|
for i in range(0, len(work_mode_timed)):
|
@@ -2690,9 +2770,7 @@ charge_config = {
|
|
2690
2770
|
'export_limit': None, # maximum export power in kW
|
2691
2771
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2692
2772
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2693
|
-
'ac_dc_loss': 0.
|
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)
|
2773
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2696
2774
|
'inverter_power': 101, # Inverter power consumption in W
|
2697
2775
|
'bms_power': 50, # BMS power consumption in W
|
2698
2776
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2713,9 +2791,6 @@ charge_config = {
|
|
2713
2791
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2714
2792
|
'special_days': ['12-25', '12-26', '01-01'],
|
2715
2793
|
'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
2794
|
'data_wrap': 6, # data items to show per line
|
2720
2795
|
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2721
2796
|
'shading': { # effect of shading on Solcast / forecast.solar
|
@@ -2740,7 +2815,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2740
2815
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2741
2816
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2742
2817
|
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,
|
2818
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2744
2819
|
print(f"\n---------------- charge_needed ----------------")
|
2745
2820
|
# validate parameters
|
2746
2821
|
args = locals()
|
@@ -2820,7 +2895,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2820
2895
|
time_to_end = times[0]['time_to_end']
|
2821
2896
|
charge_time = times[0]['charge_time']
|
2822
2897
|
# work out time window and times with clock changes
|
2823
|
-
time_to_next = int(time_to_start)
|
2824
2898
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2825
2899
|
forecast_day = today if charge_today else tomorrow
|
2826
2900
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2839,9 +2913,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2839
2913
|
output(f"start_at = {start_at}, end_by = {end_by}, bat_hold = {bat_hold}")
|
2840
2914
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2841
2915
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2842
|
-
output(f"
|
2916
|
+
output(f"full_charge = {full_charge}")
|
2917
|
+
if test_soc is not None:
|
2918
|
+
current_soc = test_soc
|
2919
|
+
capacity = 14.54
|
2920
|
+
residual = test_soc * capacity / 100
|
2921
|
+
bat_volt = 317.4
|
2922
|
+
bat_power = 0.0
|
2923
|
+
temperature = 30
|
2924
|
+
bms_charge_current = 15
|
2925
|
+
charge_loss = 1.080
|
2926
|
+
discharge_loss = 0.975
|
2927
|
+
bat_current = 0.0
|
2928
|
+
device_power = 6.0
|
2929
|
+
device_current = 35
|
2930
|
+
model = 'H1-6.0-E'
|
2931
|
+
else:
|
2843
2932
|
# get device and battery info from inverter
|
2844
|
-
if test_soc is None:
|
2845
2933
|
get_battery()
|
2846
2934
|
if battery is None or battery['status'] != 1:
|
2847
2935
|
output(f"\nBattery status is not available")
|
@@ -2852,27 +2940,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2852
2940
|
bat_current = battery['current']
|
2853
2941
|
temperature = battery['temperature']
|
2854
2942
|
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:
|
2943
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2944
|
+
if capacity is None:
|
2860
2945
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2861
2946
|
return None
|
2947
|
+
bms_charge_current = battery.get('charge_rate')
|
2948
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 1.0
|
2949
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 1.0
|
2862
2950
|
device_power = device.get('power')
|
2863
2951
|
device_current = device.get('max_charge_current')
|
2864
2952
|
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
2953
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2877
2954
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2878
2955
|
volt_curve = charge_config['volt_curve']
|
@@ -2890,27 +2967,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2890
2967
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
2891
2968
|
output(f" Current SoC: {current_soc}%")
|
2892
2969
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2970
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2893
2971
|
output(f" Temperature: {temperature:.1f}°C")
|
2894
2972
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2895
2973
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2896
|
-
# charge
|
2974
|
+
# charge current may be derated based on temperature
|
2897
2975
|
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")
|
2976
|
+
if charge_current > bms_charge_current:
|
2977
|
+
charge_current = bms_charge_current
|
2914
2978
|
# inverter losses
|
2915
2979
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2916
2980
|
operating_loss = inverter_power / 1000
|
@@ -2924,21 +2988,24 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2924
2988
|
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
2989
|
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2926
2990
|
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]
|
2991
|
+
pv_loss = charge_config['pv_loss']
|
2931
2992
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2932
2993
|
dc_ac_loss = charge_config['dc_ac_loss']
|
2933
2994
|
discharge_limit = device_power / dc_ac_loss
|
2934
2995
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2935
2996
|
discharge_power = discharge_current * bat_ocv / 1000
|
2936
2997
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2937
|
-
discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
|
2938
2998
|
# charging happens if generation exceeds export limit in feedin work mode
|
2939
2999
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2940
3000
|
export_limit = export_power / dc_ac_loss
|
2941
3001
|
current_mode = get_work_mode()
|
3002
|
+
# set parameters for battery_timed()
|
3003
|
+
charge_config['charge_limit'] = charge_limit
|
3004
|
+
charge_config['charge_power'] = charge_power
|
3005
|
+
charge_config['float_charge'] = float_charge
|
3006
|
+
charge_config['charge_loss'] = charge_loss
|
3007
|
+
charge_config['discharge_loss'] = discharge_loss
|
3008
|
+
# display what we have
|
2942
3009
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2943
3010
|
output(f"\nDevice Info:")
|
2944
3011
|
output(f" Model: {model}")
|
@@ -3048,8 +3115,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3048
3115
|
output(f"\nSettings will not be updated when forecast is not available")
|
3049
3116
|
update_settings = 0
|
3050
3117
|
# 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]
|
3118
|
+
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3119
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
3053
3120
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3054
3121
|
for i in range(0, len(work_mode_timed)):
|
3055
3122
|
# get work mode
|
@@ -3079,7 +3146,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3079
3146
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
3080
3147
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
3081
3148
|
kwh_current = residual - (charge_timed[0] - discharge_timed[0]) * (hour_now % 1)
|
3082
|
-
(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)
|
3083
3150
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
3084
3151
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
3085
3152
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3157,7 +3224,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3157
3224
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3158
3225
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3159
3226
|
# rebuild the battery residual with the charge added and min_soc
|
3160
|
-
(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)
|
3161
3228
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3162
3229
|
# show the results
|
3163
3230
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3240,10 +3307,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3240
3307
|
# CHARGE_COMPARE - load saved data and compare with actual
|
3241
3308
|
##################################################################################################
|
3242
3309
|
|
3243
|
-
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3310
|
+
def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
3244
3311
|
global charge_config, storage
|
3312
|
+
now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
|
3313
|
+
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3245
3314
|
if save is None and charge_config.get('save') is not None:
|
3246
|
-
save = charge_config.get('save').replace('###',
|
3315
|
+
save = charge_config.get('save').replace('###', yesterday)
|
3316
|
+
if not os.path.exists(storage + save):
|
3317
|
+
save = None
|
3247
3318
|
if save is None:
|
3248
3319
|
print(f"** charge_compare(): please provide a saved file to load")
|
3249
3320
|
return
|
@@ -3386,28 +3457,35 @@ def bat_count(cell_count):
|
|
3386
3457
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3387
3458
|
|
3388
3459
|
# show information about the current state of the batteries
|
3389
|
-
def battery_info(log=0, plot=1, count=None, info=1):
|
3460
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3390
3461
|
global debug_setting, battery_info_app_key
|
3391
|
-
output_spool(battery_info_app_key)
|
3392
|
-
bat = get_battery(info=info)
|
3393
3462
|
if bat is None:
|
3394
|
-
|
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])
|
3395
3469
|
return None
|
3470
|
+
output_spool(battery_info_app_key)
|
3396
3471
|
nbat = None
|
3397
|
-
if bat.get('info') is not None:
|
3398
|
-
|
3399
|
-
|
3400
|
-
|
3401
|
-
|
3402
|
-
|
3403
|
-
|
3472
|
+
if info == 1 and bat.get('info') is not None:
|
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')
|
3404
3482
|
bat_volt = bat['volt']
|
3405
3483
|
current_soc = bat['soc']
|
3406
3484
|
residual = bat['residual']
|
3407
3485
|
bat_current = bat['current']
|
3408
3486
|
bat_power = bat['power']
|
3409
3487
|
bms_temperature = bat['temperature']
|
3410
|
-
capacity =
|
3488
|
+
capacity = bat['capacity']
|
3411
3489
|
cell_volts = get_cell_volts()
|
3412
3490
|
if cell_volts is None:
|
3413
3491
|
output_close()
|
@@ -3451,7 +3529,10 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3451
3529
|
for v in cell_temps:
|
3452
3530
|
s +=f",{v:.0f}"
|
3453
3531
|
return s
|
3454
|
-
|
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}%")
|
3455
3536
|
output(f"Capacity: {capacity:.2f}kWh")
|
3456
3537
|
output(f"Residual: {residual:.2f}kWh")
|
3457
3538
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
@@ -3462,6 +3543,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3462
3543
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3463
3544
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3464
3545
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3546
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3465
3547
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3466
3548
|
output(f"\nInfo by battery:")
|
3467
3549
|
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: 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.
|
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']
|
@@ -545,8 +547,24 @@ battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
|
545
547
|
# 1 = returns Residual Energy. 2 = resturns Residual Capacity
|
546
548
|
residual_handling = 1
|
547
549
|
|
548
|
-
|
549
|
-
|
550
|
+
# charge rates based on residual_handling
|
551
|
+
battery_params = {
|
552
|
+
# cell temp -5 0 5 10 15 20 25 30 35 40 45 50 55
|
553
|
+
# bms temp 5 10 15 20 25 30 35 40 45 50 55 60 65
|
554
|
+
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
555
|
+
'step': 5,
|
556
|
+
'offset': 5,
|
557
|
+
'charge_loss': 0.975,
|
558
|
+
'discharge_loss': 0.975},
|
559
|
+
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
560
|
+
'step': 5,
|
561
|
+
'offset': 5,
|
562
|
+
'charge_loss': 1.080,
|
563
|
+
'discharge_loss': 0.975},
|
564
|
+
}
|
565
|
+
|
566
|
+
def get_battery(info=0, v=None):
|
567
|
+
global device_sn, battery, debug_setting, residual_handling, battery_params
|
550
568
|
if get_device() is None:
|
551
569
|
return None
|
552
570
|
output(f"getting battery", 2)
|
@@ -560,12 +578,30 @@ def get_battery(v = None, info=0):
|
|
560
578
|
if residual_handling == 2:
|
561
579
|
capacity = battery.get('residual')
|
562
580
|
soc = battery.get('soc')
|
563
|
-
|
564
|
-
|
565
|
-
|
581
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
582
|
+
else:
|
583
|
+
residual = battery.get('residual')
|
584
|
+
soc = battery.get('soc')
|
585
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
586
|
+
battery['capacity'] = round(capacity, 3)
|
587
|
+
battery['residual'] = round(residual, 3)
|
566
588
|
battery['status'] = 1
|
589
|
+
battery['charge_rate'] = 50
|
590
|
+
params = battery_params[residual_handling]
|
591
|
+
battery['charge_loss'] = params['charge_loss']
|
592
|
+
battery['discharge_loss'] = params['discharge_loss']
|
593
|
+
if battery.get('temperature') is not None:
|
594
|
+
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
567
595
|
return battery
|
568
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
|
+
|
569
605
|
##################################################################################################
|
570
606
|
# get charge times and save to battery_settings
|
571
607
|
##################################################################################################
|
@@ -2408,7 +2444,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2408
2444
|
profile = []
|
2409
2445
|
h = base_hour - time_offset
|
2410
2446
|
while h < 0:
|
2411
|
-
profile.append(
|
2447
|
+
profile.append(None)
|
2412
2448
|
h += 1 / steps_per_hour
|
2413
2449
|
while h < 48:
|
2414
2450
|
day = today if h < 24 else tomorrow
|
@@ -2418,10 +2454,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2418
2454
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2419
2455
|
else:
|
2420
2456
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2421
|
-
profile.append(
|
2457
|
+
profile.append(value)
|
2422
2458
|
h += 1 / steps_per_hour
|
2423
2459
|
while len(profile) < run_time:
|
2424
|
-
profile.append(
|
2460
|
+
profile.append(None)
|
2425
2461
|
return profile[:run_time]
|
2426
2462
|
|
2427
2463
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2459,11 +2495,11 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2459
2495
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2460
2496
|
# note: all power values are as measured at the inverter battery connection
|
2461
2497
|
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
|
2498
|
+
global charge_config, steps_per_hour
|
2463
2499
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2464
2500
|
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']
|
2501
|
+
charge_loss = charge_config['charge_loss']
|
2502
|
+
discharge_loss = charge_config['discharge_loss']
|
2467
2503
|
charge_limit = charge_config['charge_limit']
|
2468
2504
|
float_charge = charge_config['float_charge']
|
2469
2505
|
for i in range(0, len(work_mode_timed)):
|
@@ -2553,9 +2589,7 @@ charge_config = {
|
|
2553
2589
|
'export_limit': None, # maximum export power in kW
|
2554
2590
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2555
2591
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2556
|
-
'ac_dc_loss': 0.
|
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
|
2592
|
+
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2559
2593
|
'inverter_power': 101, # Inverter power consumption in W
|
2560
2594
|
'bms_power': 50, # BMS power consumption in W
|
2561
2595
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2576,9 +2610,6 @@ charge_config = {
|
|
2576
2610
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2577
2611
|
'special_days': ['12-25', '12-26', '01-01'],
|
2578
2612
|
'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
2613
|
'data_wrap': 6, # data items to show per line
|
2583
2614
|
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2584
2615
|
'shading': { # effect of shading on Solcast / forecast.solar
|
@@ -2603,7 +2634,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2603
2634
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2604
2635
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2605
2636
|
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,
|
2637
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2607
2638
|
print(f"\n---------------- charge_needed ----------------")
|
2608
2639
|
# validate parameters
|
2609
2640
|
args = locals()
|
@@ -2683,7 +2714,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2683
2714
|
time_to_end = times[0]['time_to_end']
|
2684
2715
|
charge_time = times[0]['charge_time']
|
2685
2716
|
# work out time window and times with clock changes
|
2686
|
-
time_to_next = int(time_to_start)
|
2687
2717
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2688
2718
|
forecast_day = today if charge_today else tomorrow
|
2689
2719
|
run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
|
@@ -2702,9 +2732,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2702
2732
|
output(f"start_at = {start_at}, end_by = {end_by}, force_charge = {force_charge}")
|
2703
2733
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2704
2734
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2705
|
-
output(f"
|
2735
|
+
output(f"full_charge = {full_charge}")
|
2736
|
+
if test_soc is not None:
|
2737
|
+
current_soc = test_soc
|
2738
|
+
capacity = 14.54
|
2739
|
+
residual = test_soc * capacity / 100
|
2740
|
+
bat_volt = 317.4
|
2741
|
+
bat_power = 0.0
|
2742
|
+
temperature = 30
|
2743
|
+
bms_charge_current = 15
|
2744
|
+
charge_loss = 1.080
|
2745
|
+
discharge_loss = 0.975
|
2746
|
+
bat_current = 0.0
|
2747
|
+
device_power = 6.0
|
2748
|
+
device_current = 35
|
2749
|
+
model = 'H1-6.0-E'
|
2750
|
+
else:
|
2706
2751
|
# get device and battery info from inverter
|
2707
|
-
if test_soc is None:
|
2708
2752
|
get_battery()
|
2709
2753
|
if battery is None or battery['status'] != 1:
|
2710
2754
|
output(f"\nBattery status is not available")
|
@@ -2715,27 +2759,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2715
2759
|
bat_current = battery['current']
|
2716
2760
|
temperature = battery['temperature']
|
2717
2761
|
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:
|
2762
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2763
|
+
if capacity is None:
|
2723
2764
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2724
2765
|
return None
|
2766
|
+
bms_charge_current = battery.get('charge_rate')
|
2767
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 1.0
|
2768
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 1.0
|
2725
2769
|
device_power = device.get('power')
|
2726
2770
|
device_current = device.get('max_charge_current')
|
2727
2771
|
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
2772
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2740
2773
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2741
2774
|
volt_curve = charge_config['volt_curve']
|
@@ -2754,26 +2787,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2754
2787
|
output(f" Current SoC: {current_soc}%")
|
2755
2788
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2756
2789
|
output(f" Temperature: {temperature:.1f}°C")
|
2790
|
+
output(f" Max Charge: {bms_charge_current:.1f}A")
|
2757
2791
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2758
2792
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2759
|
-
# charge
|
2793
|
+
# charge current may be derated based on temperature
|
2760
2794
|
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")
|
2795
|
+
if charge_current > bms_charge_current:
|
2796
|
+
charge_current = bms_charge_current
|
2777
2797
|
# inverter losses
|
2778
2798
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2779
2799
|
operating_loss = inverter_power / 1000
|
@@ -2787,21 +2807,24 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2787
2807
|
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
2808
|
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2789
2809
|
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]
|
2810
|
+
pv_loss = charge_config['pv_loss']
|
2794
2811
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2795
2812
|
dc_ac_loss = charge_config['dc_ac_loss']
|
2796
2813
|
discharge_limit = device_power / dc_ac_loss
|
2797
2814
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2798
2815
|
discharge_power = discharge_current * bat_ocv / 1000
|
2799
2816
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2800
|
-
discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
|
2801
2817
|
# charging happens if generation exceeds export limit in feedin work mode
|
2802
2818
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2803
2819
|
export_limit = export_power / dc_ac_loss
|
2804
2820
|
current_mode = get_work_mode()
|
2821
|
+
# set parameters for battery_timed()
|
2822
|
+
charge_config['charge_limit'] = charge_limit
|
2823
|
+
charge_config['charge_power'] = charge_power
|
2824
|
+
charge_config['float_charge'] = float_charge
|
2825
|
+
charge_config['charge_loss'] = charge_loss
|
2826
|
+
charge_config['discharge_loss'] = discharge_loss
|
2827
|
+
# display what we have
|
2805
2828
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2806
2829
|
output(f"\nDevice Info:")
|
2807
2830
|
output(f" Model: {model}")
|
@@ -2910,8 +2933,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2910
2933
|
output(f"\nSettings will not be updated when forecast is not available")
|
2911
2934
|
update_settings = 0
|
2912
2935
|
# 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]
|
2936
|
+
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2937
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
2915
2938
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2916
2939
|
for i in range(0, len(work_mode_timed)):
|
2917
2940
|
# get work mode
|
@@ -2941,7 +2964,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2941
2964
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
2942
2965
|
# build the battery residual if we don't add any charge and don't limit discharge at min_soc
|
2943
2966
|
kwh_current = residual - (charge_timed[0] - discharge_timed[0]) * (hour_now % 1)
|
2944
|
-
(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)
|
2945
2968
|
# work out what we need to add to stay above reserve and provide contingency or to hit target_soc
|
2946
2969
|
contingency = charge_config['special_contingency'] if tomorrow[-5:] in charge_config['special_days'] else charge_config['contingency']
|
2947
2970
|
contingency = contingency[quarter] if type(contingency) is list else contingency
|
@@ -3019,7 +3042,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3019
3042
|
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3020
3043
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3021
3044
|
# rebuild the battery residual with any charge added and min_soc
|
3022
|
-
(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)
|
3023
3046
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3024
3047
|
# show the results
|
3025
3048
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
@@ -3103,8 +3126,12 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3103
3126
|
|
3104
3127
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3105
3128
|
global charge_config, storage
|
3129
|
+
now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
|
3130
|
+
yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3106
3131
|
if save is None and charge_config.get('save') is not None:
|
3107
|
-
save = charge_config.get('save').replace('###',
|
3132
|
+
save = charge_config.get('save').replace('###', yesterday)
|
3133
|
+
if not os.path.exists(storage + save):
|
3134
|
+
save = None
|
3108
3135
|
if save is None:
|
3109
3136
|
print(f"** charge_compare(): please provide a saved file to load")
|
3110
3137
|
return
|
@@ -3246,28 +3273,35 @@ def bat_count(cell_count):
|
|
3246
3273
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3247
3274
|
|
3248
3275
|
# show information about the current state of the batteries
|
3249
|
-
def battery_info(log=0, plot=1, count=None, info=1):
|
3276
|
+
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3250
3277
|
global debug_setting, battery_info_app_key
|
3251
|
-
output_spool(battery_info_app_key)
|
3252
|
-
bat = get_battery(info=info)
|
3253
3278
|
if bat is None:
|
3254
|
-
|
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])
|
3255
3285
|
return None
|
3286
|
+
output_spool(battery_info_app_key)
|
3256
3287
|
nbat = None
|
3257
|
-
if bat.get('info') is not None:
|
3258
|
-
|
3259
|
-
|
3260
|
-
|
3261
|
-
|
3262
|
-
|
3263
|
-
|
3288
|
+
if info == 1 and bat.get('info') is not None:
|
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')
|
3264
3298
|
bat_volt = bat['volt']
|
3265
3299
|
current_soc = bat['soc']
|
3266
3300
|
residual = bat['residual']
|
3267
3301
|
bat_current = bat['current']
|
3268
3302
|
bat_power = bat['power']
|
3269
3303
|
bms_temperature = bat['temperature']
|
3270
|
-
capacity =
|
3304
|
+
capacity = bat['capacity']
|
3271
3305
|
cell_volts = get_cell_volts()
|
3272
3306
|
if cell_volts is None:
|
3273
3307
|
output_close()
|
@@ -3311,7 +3345,10 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3311
3345
|
for v in cell_temps:
|
3312
3346
|
s +=f",{v:.0f}"
|
3313
3347
|
return s
|
3314
|
-
|
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}%")
|
3315
3352
|
output(f"Capacity: {capacity:.2f}kWh")
|
3316
3353
|
output(f"Residual: {residual:.2f}kWh")
|
3317
3354
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
@@ -3322,6 +3359,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3322
3359
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3323
3360
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3324
3361
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3362
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3325
3363
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3326
3364
|
output(f"\nInfo by battery:")
|
3327
3365
|
for i in range(0, nbat):
|
@@ -1,3 +1,17 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: foxesscloud
|
3
|
+
Version: 2.6.1
|
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,7 +117,12 @@ 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.
|
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:
|
122
|
+
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
123
|
+
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
124
|
+
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
125
|
+
+ 'discharge_loss': the ratio of the kWh available for each kWh removed from the battery during during discharging
|
106
126
|
|
107
127
|
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
128
|
|
@@ -357,8 +377,6 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
357
377
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
358
378
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
359
379
|
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
380
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
363
381
|
bms_power: 50 # BMS power consumption in W
|
364
382
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -381,9 +399,6 @@ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy
|
|
381
399
|
special_contingency: 30 # contingency for special days when consumption might be higher
|
382
400
|
special_days: ['12-25', '12-26', '01-01']
|
383
401
|
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
402
|
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
388
403
|
data_wrap: 6 # data items to show per line
|
389
404
|
target_soc: None # target soc for charging (over-rides calculated value)
|
@@ -772,6 +787,16 @@ This setting can be:
|
|
772
787
|
|
773
788
|
# Version Info
|
774
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
|
+
|
797
|
+
2.6.0<br>
|
798
|
+
Rework charge de-rating with temperature, losses and other info provided by get_battery() to take new BMS behaviour into account.
|
799
|
+
|
775
800
|
2.5.9<br>
|
776
801
|
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
777
802
|
Update charge calibration for new BMS firmware.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|