foxesscloud 2.5.8__py3-none-any.whl → 2.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxesscloud/foxesscloud.py +131 -98
- foxesscloud/openapi.py +118 -92
- {foxesscloud-2.5.8.dist-info → foxesscloud-2.6.0.dist-info}/METADATA +49 -39
- foxesscloud-2.6.0.dist-info/RECORD +7 -0
- foxesscloud-2.5.8.dist-info/RECORD +0 -7
- {foxesscloud-2.5.8.dist-info → foxesscloud-2.6.0.dist-info}/LICENCE +0 -0
- {foxesscloud-2.5.8.dist-info → foxesscloud-2.6.0.dist-info}/WHEEL +0 -0
- {foxesscloud-2.5.8.dist-info → foxesscloud-2.6.0.dist-info}/top_level.txt +0 -0
foxesscloud/foxesscloud.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 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
|
@@ -576,10 +576,28 @@ def get_firmware():
|
|
576
576
|
|
577
577
|
battery = None
|
578
578
|
battery_settings = None
|
579
|
-
residual_handling = 1 # set to 2 if Residual returns current capacity
|
580
579
|
|
581
|
-
|
582
|
-
|
580
|
+
# 1 = Residual Energy, 2 = Residual Capacity
|
581
|
+
residual_handling = 1
|
582
|
+
|
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
|
583
601
|
if get_device() is None:
|
584
602
|
return None
|
585
603
|
output(f"getting battery", 2)
|
@@ -593,14 +611,11 @@ def get_battery(info=0):
|
|
593
611
|
errno = response.json().get('errno')
|
594
612
|
output(f"** get_battery(), no result data, {errno_message(errno)}")
|
595
613
|
return None
|
614
|
+
saved_info = battery['info'] if battery is not None and battery.get('info') is not None else None
|
596
615
|
battery = result
|
597
|
-
if
|
598
|
-
battery['
|
599
|
-
|
600
|
-
capacity = battery.get('residual')
|
601
|
-
soc = battery.get('soc')
|
602
|
-
battery['residual'] = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
603
|
-
if info == 1:
|
616
|
+
if saved_info is not None:
|
617
|
+
battery['info'] = saved_info
|
618
|
+
elif info == 1:
|
604
619
|
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
605
620
|
if response.status_code != 200:
|
606
621
|
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
@@ -611,6 +626,26 @@ def get_battery(info=0):
|
|
611
626
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
612
627
|
else:
|
613
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'])]
|
614
649
|
return battery
|
615
650
|
|
616
651
|
##################################################################################################
|
@@ -2543,7 +2578,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2543
2578
|
profile = []
|
2544
2579
|
h = base_hour - time_offset
|
2545
2580
|
while h < 0:
|
2546
|
-
profile.append(
|
2581
|
+
profile.append(None)
|
2547
2582
|
h += 1 / steps_per_hour
|
2548
2583
|
while h < 48:
|
2549
2584
|
day = today if h < 24 else tomorrow
|
@@ -2553,10 +2588,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2553
2588
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2554
2589
|
else:
|
2555
2590
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2556
|
-
profile.append(
|
2591
|
+
profile.append(value)
|
2557
2592
|
h += 1 / steps_per_hour
|
2558
2593
|
while len(profile) < run_time:
|
2559
|
-
profile.append(
|
2594
|
+
profile.append(None)
|
2560
2595
|
return profile[:run_time]
|
2561
2596
|
|
2562
2597
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2592,23 +2627,23 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2592
2627
|
return work_mode_timed
|
2593
2628
|
|
2594
2629
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2630
|
+
# all power values are as measured at the inverter battery connection
|
2595
2631
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2596
2632
|
global charge_config, steps_per_hour
|
2597
|
-
bat_timed = []
|
2598
2633
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2599
2634
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2600
2635
|
charge_loss = charge_config['charge_loss']
|
2636
|
+
discharge_loss = charge_config['discharge_loss']
|
2601
2637
|
charge_limit = charge_config['charge_limit']
|
2602
2638
|
float_charge = charge_config['float_charge']
|
2603
2639
|
for i in range(0, len(work_mode_timed)):
|
2604
|
-
bat_timed.append(kwh_current)
|
2605
2640
|
w = work_mode_timed[i]
|
2606
2641
|
w['kwh'] = kwh_current
|
2607
2642
|
max_now = w['max_soc'] * capacity / 100
|
2608
2643
|
if kwh_current < max_now and w['charge'] > 0.0:
|
2609
2644
|
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2610
2645
|
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2611
|
-
kwh_current += (w['pv'] - w['discharge']
|
2646
|
+
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2612
2647
|
if kwh_current > capacity:
|
2613
2648
|
# battery is full
|
2614
2649
|
kwh_current = capacity
|
@@ -2629,16 +2664,16 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2629
2664
|
reserve_drain = reserve_now
|
2630
2665
|
if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
|
2631
2666
|
kwh_min = kwh_current
|
2632
|
-
return (
|
2667
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
|
2633
2668
|
|
2669
|
+
# use work_mode_timed to generate time periods for the inverter schedule
|
2634
2670
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
2635
2671
|
global steps_per_hour
|
2636
|
-
output(f"\nConfiguring schedule:",1)
|
2637
2672
|
strategy = []
|
2638
2673
|
start = base_hour
|
2639
|
-
|
2674
|
+
times = []
|
2640
2675
|
for t in range(0, min([24 * steps_per_hour, len(work_mode_timed)])):
|
2641
|
-
period =
|
2676
|
+
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2642
2677
|
next_period = work_mode_timed[t]
|
2643
2678
|
h = base_hour + t / steps_per_hour
|
2644
2679
|
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
|
@@ -2650,15 +2685,19 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2650
2685
|
s['max_soc'] = period.get('max_soc')
|
2651
2686
|
elif period['mode'] == 'SelfUse' and period['hold'] == 1:
|
2652
2687
|
s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
|
2653
|
-
|
2688
|
+
s['end'] = (start + 1 / steps_per_hour) % 24
|
2689
|
+
for p in times:
|
2654
2690
|
p['min_soc'] = s['min_soc']
|
2655
2691
|
if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
|
2656
2692
|
strategy.append(s)
|
2657
2693
|
start = h
|
2658
|
-
|
2659
|
-
|
2660
|
-
if len(strategy)
|
2694
|
+
times = []
|
2695
|
+
times.append(work_mode_timed[t])
|
2696
|
+
if len(strategy) == 0:
|
2697
|
+
return []
|
2698
|
+
if strategy[-1]['min_soc'] != min_soc:
|
2661
2699
|
strategy.append({'start': start %24, 'end': (start + 1 / steps_per_hour) % 24, 'mode': 'SelfUse', 'min_soc': min_soc})
|
2700
|
+
output(f"\nConfiguring schedule:",1)
|
2662
2701
|
periods = []
|
2663
2702
|
for s in strategy:
|
2664
2703
|
periods.append(set_period(segment = s, quiet=0))
|
@@ -2682,9 +2721,9 @@ charge_config = {
|
|
2682
2721
|
'charge_current': None, # max battery charge current setting in A
|
2683
2722
|
'discharge_current': None, # max battery discharge current setting in A
|
2684
2723
|
'export_limit': None, # maximum export power in kW
|
2685
|
-
'
|
2686
|
-
'pv_loss': 0.
|
2687
|
-
'
|
2724
|
+
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2725
|
+
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2726
|
+
'ac_dc_loss': 0.960, # loss converting AC grid power to DC battery charge power
|
2688
2727
|
'inverter_power': 101, # Inverter power consumption in W
|
2689
2728
|
'bms_power': 50, # BMS power consumption in W
|
2690
2729
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2705,11 +2744,8 @@ charge_config = {
|
|
2705
2744
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2706
2745
|
'special_days': ['12-25', '12-26', '01-01'],
|
2707
2746
|
'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
|
2708
|
-
'derate_temp': 22, # battery temperature where cold derating starts to be applied
|
2709
|
-
'derate_step': 5, # scale for derating factors in C
|
2710
|
-
'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 22C, 17C, 12C, 7C
|
2711
2747
|
'data_wrap': 6, # data items to show per line
|
2712
|
-
'target_soc': None, #
|
2748
|
+
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2713
2749
|
'shading': { # effect of shading on Solcast / forecast.solar
|
2714
2750
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2715
2751
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
@@ -2732,7 +2768,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2732
2768
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2733
2769
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2734
2770
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2735
|
-
global timed_strategy, steps_per_hour, base_time, storage
|
2771
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2736
2772
|
print(f"\n---------------- charge_needed ----------------")
|
2737
2773
|
# validate parameters
|
2738
2774
|
args = locals()
|
@@ -2832,8 +2868,22 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2832
2868
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2833
2869
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2834
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:
|
2835
2886
|
# get device and battery info from inverter
|
2836
|
-
if test_soc is None:
|
2837
2887
|
get_battery()
|
2838
2888
|
if battery is None or battery['status'] != 1:
|
2839
2889
|
output(f"\nBattery status is not available")
|
@@ -2844,27 +2894,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2844
2894
|
bat_current = battery['current']
|
2845
2895
|
temperature = battery['temperature']
|
2846
2896
|
residual = battery['residual']
|
2847
|
-
if charge_config.get('capacity') is not None
|
2848
|
-
|
2849
|
-
elif residual is not None and residual > 0.2 and current_soc is not None and current_soc > 1:
|
2850
|
-
capacity = residual * 100 / current_soc
|
2851
|
-
else:
|
2897
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2898
|
+
if capacity is None:
|
2852
2899
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2853
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
|
2854
2904
|
device_power = device.get('power')
|
2855
2905
|
device_current = device.get('max_charge_current')
|
2856
2906
|
model = device.get('deviceType')
|
2857
|
-
else:
|
2858
|
-
current_soc = test_soc
|
2859
|
-
capacity = 14.54
|
2860
|
-
residual = test_soc * capacity / 100
|
2861
|
-
bat_volt = 315.4
|
2862
|
-
bat_power = 0.0
|
2863
|
-
temperature = 30
|
2864
|
-
bat_current = 0.0
|
2865
|
-
device_power = 6.0
|
2866
|
-
device_current = 25
|
2867
|
-
model = 'H1-6.0-E'
|
2868
2907
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2869
2908
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2870
2909
|
volt_curve = charge_config['volt_curve']
|
@@ -2882,62 +2921,52 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2882
2921
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
2883
2922
|
output(f" Current SoC: {current_soc}%")
|
2884
2923
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2924
|
+
output(f" Charge Rate: {bms_charge_current:.1f}A")
|
2885
2925
|
output(f" Temperature: {temperature:.1f}°C")
|
2886
2926
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2887
2927
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2888
|
-
# charge
|
2928
|
+
# charge current may be derated based on temperature
|
2889
2929
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2890
|
-
|
2891
|
-
|
2892
|
-
output(f"\nHigh battery temperature may affect the charge rate")
|
2893
|
-
elif round(temperature, 0) <= derate_temp:
|
2894
|
-
output(f"\nLow battery temperature may affect the charge rate")
|
2895
|
-
derating = charge_config['derating']
|
2896
|
-
derate_step = charge_config['derate_step']
|
2897
|
-
i = int((derate_temp - temperature) / (derate_step if derate_step is not None and derate_step > 0 else 1))
|
2898
|
-
if derating is not None and type(derating) is list and i < len(derating):
|
2899
|
-
derated_current = derating[i]
|
2900
|
-
if derated_current < charge_current:
|
2901
|
-
output(f" Charge current reduced from {charge_current:.0f}A to {derated_current:.0f}A" )
|
2902
|
-
charge_current = derated_current
|
2903
|
-
else:
|
2904
|
-
bat_hold = 2
|
2905
|
-
output(f" Full charge set")
|
2930
|
+
if charge_current > bms_charge_current:
|
2931
|
+
charge_current = bms_charge_current
|
2906
2932
|
# inverter losses
|
2907
2933
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2908
2934
|
operating_loss = inverter_power / 1000
|
2909
2935
|
bms_power = charge_config['bms_power']
|
2910
2936
|
bms_loss = bms_power / 1000
|
2911
2937
|
# work out charge limit, power and losses. Max power going to the battery after ac conversion losses
|
2938
|
+
ac_dc_loss = charge_config['ac_dc_loss']
|
2912
2939
|
charge_limit = min([charge_current * (bat_ocv + charge_current * bat_resistance) / 1000, max([6, device_power])])
|
2913
2940
|
if charge_limit < 0.1:
|
2914
2941
|
output(f"** charge_current is too low ({charge_current:.1f}A)")
|
2915
|
-
charge_loss = 1.0 - charge_limit * 1000 * bat_resistance / bat_ocv ** 2
|
2916
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
|
2917
|
-
|
2918
|
-
charge_power = min([(device_power - operating_loss) * grid_loss, force_charge_power * grid_loss, charge_limit])
|
2943
|
+
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2919
2944
|
float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
|
2920
|
-
charge_config['
|
2921
|
-
charge_config['charge_limit'] = charge_limit
|
2922
|
-
charge_config['charge_power'] = charge_power
|
2923
|
-
charge_config['float_charge'] = float_charge
|
2945
|
+
pv_loss = charge_config['pv_loss']
|
2924
2946
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2925
|
-
|
2926
|
-
discharge_limit = device_power /
|
2947
|
+
dc_ac_loss = charge_config['dc_ac_loss']
|
2948
|
+
discharge_limit = device_power / dc_ac_loss
|
2927
2949
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2928
2950
|
discharge_power = discharge_current * bat_ocv / 1000
|
2929
2951
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2930
2952
|
# charging happens if generation exceeds export limit in feedin work mode
|
2931
2953
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2932
|
-
export_limit = export_power /
|
2954
|
+
export_limit = export_power / dc_ac_loss
|
2933
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
|
2934
2963
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2935
2964
|
output(f"\nDevice Info:")
|
2936
2965
|
output(f" Model: {model}")
|
2937
2966
|
output(f" Rating: {device_power:.2f}kW")
|
2938
2967
|
output(f" Export: {export_power:.2f}kW")
|
2939
|
-
output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {
|
2940
|
-
output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {
|
2968
|
+
output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {ac_dc_loss * 100:.1f}% efficient")
|
2969
|
+
output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {dc_ac_loss * 100:.1f}% efficient")
|
2941
2970
|
output(f" Inverter: {inverter_power:.0f}W power consumption")
|
2942
2971
|
output(f" BMS: {bms_power:.0f}W power consumption")
|
2943
2972
|
if current_mode is not None:
|
@@ -3040,8 +3069,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3040
3069
|
output(f"\nSettings will not be updated when forecast is not available")
|
3041
3070
|
update_settings = 0
|
3042
3071
|
# produce time lines for charge, discharge and work mode
|
3043
|
-
charge_timed = [min([charge_limit, x *
|
3044
|
-
discharge_timed = [min([discharge_limit, x /
|
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]
|
3045
3074
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3046
3075
|
for i in range(0, len(work_mode_timed)):
|
3047
3076
|
# get work mode
|
@@ -3052,9 +3081,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3052
3081
|
discharge_timed[i] = discharge_timed[i] * (1.0 - duration)
|
3053
3082
|
work_mode_timed[i]['charge'] = charge_power * duration
|
3054
3083
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
3055
|
-
fdpwr = work_mode_timed[i]['fdpwr'] /
|
3056
|
-
fdpwr = min([discharge_limit, export_limit + discharge_timed[i]
|
3057
|
-
discharge_timed[i] = fdpwr * duration
|
3084
|
+
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
3085
|
+
fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
|
3086
|
+
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
3058
3087
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
3059
3088
|
discharge_timed[i] = bms_loss
|
3060
3089
|
if timed_mode > 1:
|
@@ -3099,13 +3128,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3099
3128
|
end_timed = time_to_end
|
3100
3129
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3101
3130
|
else:
|
3102
|
-
if test_charge is None:
|
3103
|
-
output(f"\nCharge needed: {kwh_needed:.2f}kWh")
|
3104
|
-
charge_message = "with charge added"
|
3105
|
-
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3106
3131
|
# work out time to add kwh_needed to battery
|
3107
3132
|
charge_rate = charge_power * charge_loss
|
3108
3133
|
hours = kwh_needed / charge_rate
|
3134
|
+
if test_charge is None:
|
3135
|
+
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
3136
|
+
charge_message = "with charge added"
|
3137
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3109
3138
|
# check if charge time exceeded or charge needed exceeds capacity
|
3110
3139
|
hours_to_full = (capacity - start_residual) / charge_rate
|
3111
3140
|
if hours > charge_time:
|
@@ -3146,7 +3175,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3146
3175
|
if i >= start_timed and i < end_timed:
|
3147
3176
|
work_mode_timed[i]['mode'] = 'ForceCharge'
|
3148
3177
|
work_mode_timed[i]['charge'] = charge_power * t
|
3149
|
-
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else
|
3178
|
+
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3150
3179
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3151
3180
|
# rebuild the battery residual with the charge added and min_soc
|
3152
3181
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
@@ -3218,7 +3247,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3218
3247
|
data['capacity'] = capacity
|
3219
3248
|
data['config'] = charge_config
|
3220
3249
|
data['time'] = time_line
|
3221
|
-
data['bat'] = bat_timed
|
3222
3250
|
data['work_mode'] = work_mode_timed
|
3223
3251
|
data['generation'] = generation_timed
|
3224
3252
|
data['consumption'] = consumption_timed
|
@@ -3233,10 +3261,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3233
3261
|
# CHARGE_COMPARE - load saved data and compare with actual
|
3234
3262
|
##################################################################################################
|
3235
3263
|
|
3236
|
-
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):
|
3237
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')
|
3238
3268
|
if save is None and charge_config.get('save') is not None:
|
3239
|
-
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
|
3240
3272
|
if save is None:
|
3241
3273
|
print(f"** charge_compare(): please provide a saved file to load")
|
3242
3274
|
return
|
@@ -3253,10 +3285,10 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3253
3285
|
steps_per_hour = data.get('steps')
|
3254
3286
|
capacity = data.get('capacity')
|
3255
3287
|
time_line = data.get('time')
|
3256
|
-
bat_timed = data.get('bat')
|
3257
3288
|
generation_timed = data.get('generation')
|
3258
3289
|
consumption_timed = data.get('consumption')
|
3259
3290
|
work_mode_timed = data.get('work_mode')
|
3291
|
+
bat_timed = data['bat'] if data.get('bat') is not None else [work_mode_timed[t]['kwh'] for t in range(0, len(work_mode_timed))]
|
3260
3292
|
run_time = len(time_line)
|
3261
3293
|
base_hour = int(time_hours(base_time[11:16]))
|
3262
3294
|
start_day = base_time[:10]
|
@@ -3298,7 +3330,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3298
3330
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3299
3331
|
h = base_hour
|
3300
3332
|
t = 0
|
3301
|
-
while t < len(time_line) and bat_timed[t] is not None:
|
3333
|
+
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3302
3334
|
col = h % data_wrap
|
3303
3335
|
s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
|
3304
3336
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
@@ -3380,14 +3412,14 @@ battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
|
3380
3412
|
|
3381
3413
|
# show information about the current state of the batteries
|
3382
3414
|
def battery_info(log=0, plot=1, count=None, info=1):
|
3383
|
-
global debug_setting, battery_info_app_key
|
3415
|
+
global debug_setting, battery_info_app_key, residual_handling
|
3384
3416
|
output_spool(battery_info_app_key)
|
3385
3417
|
bat = get_battery(info=info)
|
3386
3418
|
if bat is None:
|
3387
3419
|
output_close()
|
3388
3420
|
return None
|
3389
3421
|
nbat = None
|
3390
|
-
if bat.get('info') is not None:
|
3422
|
+
if info == 1 and bat.get('info') is not None:
|
3391
3423
|
for b in bat['info']:
|
3392
3424
|
output(f"\nSN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3393
3425
|
nbat = 0
|
@@ -3400,7 +3432,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3400
3432
|
bat_current = bat['current']
|
3401
3433
|
bat_power = bat['power']
|
3402
3434
|
bms_temperature = bat['temperature']
|
3403
|
-
capacity =
|
3435
|
+
capacity = bat['capacity']
|
3404
3436
|
cell_volts = get_cell_volts()
|
3405
3437
|
if cell_volts is None:
|
3406
3438
|
output_close()
|
@@ -3455,6 +3487,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3455
3487
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3456
3488
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3457
3489
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3490
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3458
3491
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3459
3492
|
output(f"\nInfo by battery:")
|
3460
3493
|
for i in range(0, nbat):
|
foxesscloud/openapi.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 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
|
@@ -541,10 +541,28 @@ battery = None
|
|
541
541
|
battery_settings = None
|
542
542
|
battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy' ]
|
543
543
|
battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
544
|
-
|
544
|
+
|
545
|
+
# 1 = returns Residual Energy. 2 = resturns Residual Capacity
|
546
|
+
residual_handling = 1
|
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
|
+
}
|
545
563
|
|
546
564
|
def get_battery(v = None, info=0):
|
547
|
-
global device_sn, battery, debug_setting, residual_handling
|
565
|
+
global device_sn, battery, debug_setting, residual_handling, battery_params
|
548
566
|
if get_device() is None:
|
549
567
|
return None
|
550
568
|
output(f"getting battery", 2)
|
@@ -558,10 +576,20 @@ def get_battery(v = None, info=0):
|
|
558
576
|
if residual_handling == 2:
|
559
577
|
capacity = battery.get('residual')
|
560
578
|
soc = battery.get('soc')
|
561
|
-
|
562
|
-
|
563
|
-
|
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)
|
564
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'])]
|
565
593
|
return battery
|
566
594
|
|
567
595
|
##################################################################################################
|
@@ -2406,7 +2434,7 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2406
2434
|
profile = []
|
2407
2435
|
h = base_hour - time_offset
|
2408
2436
|
while h < 0:
|
2409
|
-
profile.append(
|
2437
|
+
profile.append(None)
|
2410
2438
|
h += 1 / steps_per_hour
|
2411
2439
|
while h < 48:
|
2412
2440
|
day = today if h < 24 else tomorrow
|
@@ -2416,10 +2444,10 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2416
2444
|
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2417
2445
|
else:
|
2418
2446
|
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2419
|
-
profile.append(
|
2447
|
+
profile.append(value)
|
2420
2448
|
h += 1 / steps_per_hour
|
2421
2449
|
while len(profile) < run_time:
|
2422
|
-
profile.append(
|
2450
|
+
profile.append(None)
|
2423
2451
|
return profile[:run_time]
|
2424
2452
|
|
2425
2453
|
# build the timed work mode profile from the tariff strategy:
|
@@ -2455,23 +2483,23 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2455
2483
|
return work_mode_timed
|
2456
2484
|
|
2457
2485
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2486
|
+
# note: all power values are as measured at the inverter battery connection
|
2458
2487
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2459
2488
|
global charge_config, steps_per_hour
|
2460
|
-
bat_timed = []
|
2461
2489
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2462
2490
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2463
2491
|
charge_loss = charge_config['charge_loss']
|
2492
|
+
discharge_loss = charge_config['discharge_loss']
|
2464
2493
|
charge_limit = charge_config['charge_limit']
|
2465
2494
|
float_charge = charge_config['float_charge']
|
2466
2495
|
for i in range(0, len(work_mode_timed)):
|
2467
|
-
bat_timed.append(kwh_current)
|
2468
2496
|
w = work_mode_timed[i]
|
2469
2497
|
w['kwh'] = kwh_current
|
2470
2498
|
max_now = w['max_soc'] * capacity / 100
|
2471
2499
|
if kwh_current < max_now and w['charge'] > 0.0:
|
2472
2500
|
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2473
2501
|
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2474
|
-
kwh_current += (w['pv'] - w['discharge']
|
2502
|
+
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2475
2503
|
if kwh_current > capacity:
|
2476
2504
|
# battery is full
|
2477
2505
|
kwh_current = capacity
|
@@ -2492,16 +2520,16 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2492
2520
|
reserve_drain = reserve_now
|
2493
2521
|
if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
|
2494
2522
|
kwh_min = kwh_current
|
2495
|
-
return (
|
2523
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
|
2496
2524
|
|
2525
|
+
# use work_mode_timed to generate time periods for the inverter schedule
|
2497
2526
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
2498
2527
|
global steps_per_hour
|
2499
|
-
output(f"\nConfiguring schedule:",1)
|
2500
2528
|
strategy = []
|
2501
2529
|
start = base_hour
|
2502
|
-
|
2530
|
+
times = []
|
2503
2531
|
for t in range(0, min([24 * steps_per_hour, len(work_mode_timed)])):
|
2504
|
-
period =
|
2532
|
+
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2505
2533
|
next_period = work_mode_timed[t]
|
2506
2534
|
h = base_hour + t / steps_per_hour
|
2507
2535
|
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
|
@@ -2513,15 +2541,19 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2513
2541
|
s['max_soc'] = period.get('max_soc')
|
2514
2542
|
elif period['mode'] == 'SelfUse' and period['hold'] == 1:
|
2515
2543
|
s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
|
2516
|
-
|
2544
|
+
s['end'] = (start + 1 / steps_per_hour) % 24
|
2545
|
+
for p in times:
|
2517
2546
|
p['min_soc'] = s['min_soc']
|
2518
2547
|
if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
|
2519
2548
|
strategy.append(s)
|
2520
2549
|
start = h
|
2521
|
-
|
2522
|
-
|
2523
|
-
if len(strategy)
|
2550
|
+
times = []
|
2551
|
+
times.append(work_mode_timed[t])
|
2552
|
+
if len(strategy) == 0:
|
2553
|
+
return []
|
2554
|
+
if strategy[-1]['min_soc'] != min_soc:
|
2524
2555
|
strategy.append({'start': start %24, 'end': (start + 1 / steps_per_hour) % 24, 'mode': 'SelfUse', 'min_soc': min_soc})
|
2556
|
+
output(f"\nConfiguring schedule:",1)
|
2525
2557
|
periods = []
|
2526
2558
|
for s in strategy:
|
2527
2559
|
periods.append(set_period(segment = s, quiet=0))
|
@@ -2545,9 +2577,9 @@ charge_config = {
|
|
2545
2577
|
'charge_current': None, # max battery charge current setting in A
|
2546
2578
|
'discharge_current': None, # max battery discharge current setting in A
|
2547
2579
|
'export_limit': None, # maximum export power in kW
|
2548
|
-
'
|
2549
|
-
'pv_loss': 0.95, # loss converting PV power to battery charge power
|
2550
|
-
'
|
2580
|
+
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2581
|
+
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2582
|
+
'ac_dc_loss': 0.962, # loss converting AC grid power to DC battery charge power
|
2551
2583
|
'inverter_power': 101, # Inverter power consumption in W
|
2552
2584
|
'bms_power': 50, # BMS power consumption in W
|
2553
2585
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2568,11 +2600,8 @@ charge_config = {
|
|
2568
2600
|
'special_contingency': 33, # contingency for special days when consumption might be higher
|
2569
2601
|
'special_days': ['12-25', '12-26', '01-01'],
|
2570
2602
|
'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
|
2571
|
-
'derate_temp': 22, # battery temperature where cold derating starts to be applied
|
2572
|
-
'derate_step': 5, # scale for derating factors in C
|
2573
|
-
'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 22C, 17C, 12C, 7C
|
2574
2603
|
'data_wrap': 6, # data items to show per line
|
2575
|
-
'target_soc': None, #
|
2604
|
+
'target_soc': None, # the target SoC for charging (over-rides calculated value)
|
2576
2605
|
'shading': { # effect of shading on Solcast / forecast.solar
|
2577
2606
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2578
2607
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
@@ -2595,7 +2624,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2595
2624
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2596
2625
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2597
2626
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2598
|
-
global timed_strategy, steps_per_hour, base_time, storage
|
2627
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery
|
2599
2628
|
print(f"\n---------------- charge_needed ----------------")
|
2600
2629
|
# validate parameters
|
2601
2630
|
args = locals()
|
@@ -2695,8 +2724,22 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2695
2724
|
output(f"base_hour = {base_hour}, hour_adjustment = {hour_adjustment}, change_hour = {change_hour}, time_change = {time_change}")
|
2696
2725
|
output(f"time_to_start = {time_to_start}, run_time = {run_time}, charge_today = {charge_today}")
|
2697
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:
|
2698
2742
|
# get device and battery info from inverter
|
2699
|
-
if test_soc is None:
|
2700
2743
|
get_battery()
|
2701
2744
|
if battery is None or battery['status'] != 1:
|
2702
2745
|
output(f"\nBattery status is not available")
|
@@ -2707,27 +2750,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2707
2750
|
bat_current = battery['current']
|
2708
2751
|
temperature = battery['temperature']
|
2709
2752
|
residual = battery['residual']
|
2710
|
-
if charge_config.get('capacity') is not None
|
2711
|
-
|
2712
|
-
elif residual is not None and residual > 0.2 and current_soc is not None and current_soc > 1:
|
2713
|
-
capacity = residual * 100 / current_soc
|
2714
|
-
else:
|
2753
|
+
capacity = charge_config['capacity'] if charge_config.get('capacity') is not None else battery.get('capacity')
|
2754
|
+
if capacity is None:
|
2715
2755
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2716
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
|
2717
2760
|
device_power = device.get('power')
|
2718
2761
|
device_current = device.get('max_charge_current')
|
2719
2762
|
model = device.get('deviceType')
|
2720
|
-
else:
|
2721
|
-
current_soc = test_soc
|
2722
|
-
capacity = 14.54
|
2723
|
-
residual = test_soc * capacity / 100
|
2724
|
-
bat_volt = 315.4
|
2725
|
-
bat_power = 0.0
|
2726
|
-
temperature = 30
|
2727
|
-
bat_current = 0.0
|
2728
|
-
device_power = 6.0
|
2729
|
-
device_current = 25
|
2730
|
-
model = 'H1-6.0-E'
|
2731
2763
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2732
2764
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2733
2765
|
volt_curve = charge_config['volt_curve']
|
@@ -2746,61 +2778,51 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2746
2778
|
output(f" Current SoC: {current_soc}%")
|
2747
2779
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2748
2780
|
output(f" Temperature: {temperature:.1f}°C")
|
2781
|
+
output(f" Charge Rate: {bms_charge_current:.1f}A")
|
2749
2782
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2750
2783
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2751
|
-
# charge
|
2784
|
+
# charge current may be derated based on temperature
|
2752
2785
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2753
|
-
|
2754
|
-
|
2755
|
-
output(f"\nHigh battery temperature may affect the charge rate")
|
2756
|
-
elif round(temperature, 0) <= derate_temp:
|
2757
|
-
output(f"\nLow battery temperature may affect the charge rate")
|
2758
|
-
derating = charge_config['derating']
|
2759
|
-
derate_step = charge_config['derate_step']
|
2760
|
-
i = int((derate_temp - temperature) / (derate_step if derate_step is not None and derate_step > 0 else 1))
|
2761
|
-
if derating is not None and type(derating) is list and i < len(derating):
|
2762
|
-
derated_current = derating[i]
|
2763
|
-
if derated_current < charge_current:
|
2764
|
-
output(f" Charge current reduced from {charge_current:.0f}A to {derated_current:.0f}A" )
|
2765
|
-
charge_current = derated_current
|
2766
|
-
else:
|
2767
|
-
bat_hold = 2
|
2768
|
-
output(f" Full charge set")
|
2786
|
+
if charge_current > bms_charge_current:
|
2787
|
+
charge_current = bms_charge_current
|
2769
2788
|
# inverter losses
|
2770
2789
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
2771
2790
|
operating_loss = inverter_power / 1000
|
2772
2791
|
bms_power = charge_config['bms_power']
|
2773
2792
|
bms_loss = bms_power / 1000
|
2774
2793
|
# work out charge limit, power and losses. Max power going to the battery after ac conversion losses
|
2794
|
+
ac_dc_loss = charge_config['ac_dc_loss']
|
2775
2795
|
charge_limit = min([charge_current * (bat_ocv + charge_current * bat_resistance) / 1000, max([6, device_power])])
|
2776
2796
|
if charge_limit < 0.1:
|
2777
2797
|
output(f"** charge_current is too low ({charge_current:.1f}A)")
|
2778
|
-
charge_loss = 1.0 - charge_limit * 1000 * bat_resistance / bat_ocv ** 2
|
2779
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
|
2780
|
-
|
2781
|
-
charge_power = min([(device_power - operating_loss) * grid_loss, force_charge_power * grid_loss, charge_limit])
|
2799
|
+
charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
|
2782
2800
|
float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
|
2783
|
-
charge_config['
|
2784
|
-
charge_config['charge_limit'] = charge_limit
|
2785
|
-
charge_config['charge_power'] = charge_power
|
2786
|
-
charge_config['float_charge'] = float_charge
|
2801
|
+
pv_loss = charge_config['pv_loss']
|
2787
2802
|
# work out discharge limit = max power coming from the battery before ac conversion losses
|
2788
|
-
|
2789
|
-
discharge_limit = device_power /
|
2803
|
+
dc_ac_loss = charge_config['dc_ac_loss']
|
2804
|
+
discharge_limit = device_power / dc_ac_loss
|
2790
2805
|
discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
|
2791
2806
|
discharge_power = discharge_current * bat_ocv / 1000
|
2792
2807
|
discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
|
2793
2808
|
# charging happens if generation exceeds export limit in feedin work mode
|
2794
2809
|
export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
|
2795
|
-
export_limit = export_power /
|
2810
|
+
export_limit = export_power / dc_ac_loss
|
2796
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
|
2797
2819
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2798
2820
|
output(f"\nDevice Info:")
|
2799
2821
|
output(f" Model: {model}")
|
2800
2822
|
output(f" Rating: {device_power:.2f}kW")
|
2801
2823
|
output(f" Export: {export_power:.2f}kW")
|
2802
|
-
output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {
|
2803
|
-
output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {
|
2824
|
+
output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {ac_dc_loss * 100:.1f}% efficient")
|
2825
|
+
output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {dc_ac_loss * 100:.1f}% efficient")
|
2804
2826
|
output(f" Inverter: {inverter_power:.0f}W power consumption")
|
2805
2827
|
output(f" BMS: {bms_power:.0f}W power consumption")
|
2806
2828
|
if current_mode is not None:
|
@@ -2902,8 +2924,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2902
2924
|
output(f"\nSettings will not be updated when forecast is not available")
|
2903
2925
|
update_settings = 0
|
2904
2926
|
# produce time lines for charge, discharge and work mode
|
2905
|
-
charge_timed = [min([charge_limit, x *
|
2906
|
-
discharge_timed = [min([discharge_limit, x /
|
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]
|
2907
2929
|
work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2908
2930
|
for i in range(0, len(work_mode_timed)):
|
2909
2931
|
# get work mode
|
@@ -2914,9 +2936,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2914
2936
|
discharge_timed[i] = discharge_timed[i] * (1.0 - duration)
|
2915
2937
|
work_mode_timed[i]['charge'] = charge_power * duration
|
2916
2938
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
2917
|
-
fdpwr = work_mode_timed[i]['fdpwr'] /
|
2918
|
-
fdpwr = min([discharge_limit, export_limit + discharge_timed[i]
|
2919
|
-
discharge_timed[i] = fdpwr * duration
|
2939
|
+
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
2940
|
+
fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
|
2941
|
+
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
2920
2942
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
2921
2943
|
discharge_timed[i] = bms_loss
|
2922
2944
|
if timed_mode > 1:
|
@@ -2961,13 +2983,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2961
2983
|
end_timed = time_to_end
|
2962
2984
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
2963
2985
|
else:
|
2964
|
-
if test_charge is None:
|
2965
|
-
output(f"\nCharge needed: {kwh_needed:.2f}kWh:")
|
2966
|
-
charge_message = "with charge added"
|
2967
|
-
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2968
2986
|
# work out time to add kwh_needed to battery
|
2969
2987
|
charge_rate = charge_power * charge_loss
|
2970
2988
|
hours = kwh_needed / charge_rate
|
2989
|
+
if test_charge is None:
|
2990
|
+
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
2991
|
+
charge_message = "with charge added"
|
2992
|
+
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2971
2993
|
# check if charge time exceeded or charge needed exceeds capacity
|
2972
2994
|
hours_to_full = (capacity - start_residual) / charge_rate
|
2973
2995
|
if hours > charge_time:
|
@@ -3008,7 +3030,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3008
3030
|
if i >= start_timed and i < end_timed:
|
3009
3031
|
work_mode_timed[i]['mode'] = 'ForceCharge'
|
3010
3032
|
work_mode_timed[i]['charge'] = charge_power * t
|
3011
|
-
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else
|
3033
|
+
work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
|
3012
3034
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3013
3035
|
# rebuild the battery residual with any charge added and min_soc
|
3014
3036
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
@@ -3080,7 +3102,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3080
3102
|
data['capacity'] = capacity
|
3081
3103
|
data['config'] = charge_config
|
3082
3104
|
data['time'] = time_line
|
3083
|
-
data['bat'] = bat_timed
|
3084
3105
|
data['work_mode'] = work_mode_timed
|
3085
3106
|
data['generation'] = generation_timed
|
3086
3107
|
data['consumption'] = consumption_timed
|
@@ -3096,8 +3117,12 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3096
3117
|
|
3097
3118
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3098
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')
|
3099
3122
|
if save is None and charge_config.get('save') is not None:
|
3100
|
-
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
|
3101
3126
|
if save is None:
|
3102
3127
|
print(f"** charge_compare(): please provide a saved file to load")
|
3103
3128
|
return
|
@@ -3114,10 +3139,10 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3114
3139
|
steps_per_hour = data.get('steps')
|
3115
3140
|
capacity = data.get('capacity')
|
3116
3141
|
time_line = data.get('time')
|
3117
|
-
bat_timed = data.get('bat')
|
3118
3142
|
generation_timed = data.get('generation')
|
3119
3143
|
consumption_timed = data.get('consumption')
|
3120
3144
|
work_mode_timed = data.get('work_mode')
|
3145
|
+
bat_timed = data['bat'] if data.get('bat') is not None else [work_mode_timed[t]['kwh'] for t in range(0, len(work_mode_timed))]
|
3121
3146
|
run_time = len(time_line)
|
3122
3147
|
base_hour = int(time_hours(base_time[11:16]))
|
3123
3148
|
start_day = base_time[:10]
|
@@ -3159,7 +3184,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3159
3184
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3160
3185
|
h = base_hour
|
3161
3186
|
t = 0
|
3162
|
-
while t < len(time_line) and bat_timed[t] is not None:
|
3187
|
+
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3163
3188
|
col = h % data_wrap
|
3164
3189
|
s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
|
3165
3190
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
@@ -3240,14 +3265,14 @@ battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
|
3240
3265
|
|
3241
3266
|
# show information about the current state of the batteries
|
3242
3267
|
def battery_info(log=0, plot=1, count=None, info=1):
|
3243
|
-
global debug_setting, battery_info_app_key
|
3268
|
+
global debug_setting, battery_info_app_key, residual_handling
|
3244
3269
|
output_spool(battery_info_app_key)
|
3245
3270
|
bat = get_battery(info=info)
|
3246
3271
|
if bat is None:
|
3247
3272
|
output_close()
|
3248
3273
|
return None
|
3249
3274
|
nbat = None
|
3250
|
-
if bat.get('info') is not None:
|
3275
|
+
if info == 1 and bat.get('info') is not None:
|
3251
3276
|
for b in bat['info']:
|
3252
3277
|
output(f"\nSN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3253
3278
|
nbat = 0
|
@@ -3260,7 +3285,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3260
3285
|
bat_current = bat['current']
|
3261
3286
|
bat_power = bat['power']
|
3262
3287
|
bms_temperature = bat['temperature']
|
3263
|
-
capacity =
|
3288
|
+
capacity = bat['capacity']
|
3264
3289
|
cell_volts = get_cell_volts()
|
3265
3290
|
if cell_volts is None:
|
3266
3291
|
output_close()
|
@@ -3315,6 +3340,7 @@ def battery_info(log=0, plot=1, count=None, info=1):
|
|
3315
3340
|
output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
|
3316
3341
|
output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
|
3317
3342
|
output(f"BMS Temperature: {bms_temperature:.1f}°C")
|
3343
|
+
output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
|
3318
3344
|
output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
|
3319
3345
|
output(f"\nInfo by battery:")
|
3320
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
|
|
@@ -363,44 +368,41 @@ Given the data available, the modelling works as follows:
|
|
363
368
|
|
364
369
|
The following parameters and default values are used to configure charge_needed and may be updated if required using name=value:
|
365
370
|
```
|
366
|
-
contingency: [20,10,5,10]
|
367
|
-
capacity: None
|
368
|
-
charge_current: None
|
369
|
-
discharge_current: None
|
370
|
-
export_limit: None
|
371
|
-
|
372
|
-
pv_loss: 0.
|
373
|
-
|
374
|
-
inverter_power: None
|
375
|
-
bms_power: 50
|
376
|
-
force_charge_power: 5.00
|
377
|
-
allowed_drain: 4,
|
378
|
-
float_current: 4,
|
379
|
-
bat_resistance: 0.070
|
380
|
-
volt_curve: lifepo4_curve
|
381
|
-
nominal_soc: 55
|
382
|
-
generation_days: 3
|
383
|
-
consumption_days: 3
|
384
|
-
consumption_span: 'week'
|
385
|
-
use_today: 21.0
|
386
|
-
min_hours: 0.25
|
387
|
-
min_kwh: 0.5
|
388
|
-
solcast_adjust: 100
|
389
|
-
solar_adjust: 100
|
390
|
-
forecast_selection: 1
|
391
|
-
annual_consumption: None
|
392
|
-
timed_mode: 0
|
393
|
-
special_contingency: 30
|
371
|
+
contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
|
372
|
+
capacity: None # Battery capacity in kWh (over-rides generated value if set)
|
373
|
+
charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
|
374
|
+
discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
|
375
|
+
export_limit: None # maximum export power in kW. None uses the inverter power rating
|
376
|
+
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
377
|
+
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
378
|
+
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
379
|
+
inverter_power: None # inverter power consumption in W (dynamically set)
|
380
|
+
bms_power: 50 # BMS power consumption in W
|
381
|
+
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
382
|
+
allowed_drain: 4, # % tolerance below min_soc before float charge starts
|
383
|
+
float_current: 4, # BMS float charge current in A
|
384
|
+
bat_resistance: 0.070 # internal resistance of a battery in ohms
|
385
|
+
volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
|
386
|
+
nominal_soc: 55 # SoC for nominal open circuit voltage
|
387
|
+
generation_days: 3 # number of days to use for average generation (1-7)
|
388
|
+
consumption_days: 3 # number of days to use for average consumption (1-7)
|
389
|
+
consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
|
390
|
+
use_today: 21.0 # hour when today's generation and consumption data will be used
|
391
|
+
min_hours: 0.25 # minimum charge time to set (in decimal hours)
|
392
|
+
min_kwh: 0.5 # minimum charge to add in kwh
|
393
|
+
solcast_adjust: 100 # % adjustment to make to Solcast forecast
|
394
|
+
solar_adjust: 100 # % adjustment to make to Solar forecast
|
395
|
+
forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
|
396
|
+
annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
|
397
|
+
timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
|
398
|
+
special_contingency: 30 # contingency for special days when consumption might be higher
|
394
399
|
special_days: ['12-25', '12-26', '01-01']
|
395
|
-
full_charge: None
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
target_soc: None # target soc for charging
|
402
|
-
shading: {} # effect of shading on Solcast / Solar (see below)
|
403
|
-
save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
|
400
|
+
full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
|
401
|
+
force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
|
402
|
+
data_wrap: 6 # data items to show per line
|
403
|
+
target_soc: None # target soc for charging (over-rides calculated value)
|
404
|
+
shading: {} # effect of shading on Solcast / Solar (see below)
|
405
|
+
save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
|
404
406
|
```
|
405
407
|
|
406
408
|
These values are stored / available in f.charge_config.
|
@@ -784,6 +786,14 @@ This setting can be:
|
|
784
786
|
|
785
787
|
# Version Info
|
786
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
|
+
|
792
|
+
2.5.9<br>
|
793
|
+
Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
|
794
|
+
Update charge calibration for new BMS firmware.
|
795
|
+
Increase de-rating temperature from 21C to 28C for new BMS firmware.
|
796
|
+
|
787
797
|
2.5.8<br>
|
788
798
|
Fix incorrect charging setup when force_charge=1.
|
789
799
|
Rework charge_periods() to consolidate charge periods to reduce number of time segments when timed_mode=2.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=2Nsr3hmtq6jcFLslcOrkBEvFKgjPiZdpsQO0O85S928,213371
|
2
|
+
foxesscloud/openapi.py,sha256=sJopUqWbk8OrO9MXFAnEQvs14G6t4GWtX05lL-tXymI,206640
|
3
|
+
foxesscloud-2.6.0.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.6.0.dist-info/METADATA,sha256=GUbw1ZyKzfPaxtmBxGnuM__RKDZqAJpeejiGb4KZfSM,56993
|
5
|
+
foxesscloud-2.6.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.6.0.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.6.0.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=9WivKa8ysTZ52aTgPmTxJYDAnAz5TobibgvpJkrK_KM,211855
|
2
|
-
foxesscloud/openapi.py,sha256=SCr8VOz1aP0Nk5vlUi3-kJ-AMNpW0STlVWQRbbABjBE,205502
|
3
|
-
foxesscloud-2.5.8.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.5.8.dist-info/METADATA,sha256=Aon8oxksMZP4Z9NgWKOwsz7Dpo8raj8A2cIjakNL_xM,56304
|
5
|
-
foxesscloud-2.5.8.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.5.8.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.5.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|