foxesscloud 2.7.0__tar.gz → 2.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/PKG-INFO +23 -7
- foxesscloud-2.7.0/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.7.2/README.md +22 -20
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/pyproject.toml +1 -1
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/src/foxesscloud/foxesscloud.py +106 -92
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/src/foxesscloud/openapi.py +97 -86
- foxesscloud-2.7.0/README.md → foxesscloud-2.7.2/src/foxesscloud.egg-info/PKG-INFO +36 -6
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/LICENCE +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/setup.cfg +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.2}/src/foxesscloud.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.7.
|
3
|
+
Version: 2.7.2
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -158,12 +158,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
|
|
158
158
|
+ minSoc: min Soc setting e.g. 10 = 10%
|
159
159
|
|
160
160
|
set_charge() takes the charge times from the battery_settings and applies these to the inverter. The parameters are optional and will update battery_settings. You should specify all 3 parameter for a time period:
|
161
|
-
+ ch1: enable charge from grid for period 1 (True
|
162
|
-
+ st1: the start time for period 1
|
163
|
-
+ en1: the end time for period 1
|
164
|
-
+ ch2: enable charge from grid for period 2 (True
|
165
|
-
+ st2: the start time for period 2
|
166
|
-
+ en2: the end time for period 2
|
161
|
+
+ ch1: enable charge from grid for period 1 (default True)
|
162
|
+
+ st1: the start time for period 1 (default 0)
|
163
|
+
+ en1: the end time for period 1 (default 0)
|
164
|
+
+ ch2: enable charge from grid for period 2 (default True)
|
165
|
+
+ st2: the start time for period 2 (default 0)
|
166
|
+
+ en2: the end time for period 2 (default 0)
|
167
167
|
+ enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
|
168
168
|
|
169
169
|
set_period() returns a period structure that can be used to build a list for set_schedule()
|
@@ -807,6 +807,22 @@ This setting can be:
|
|
807
807
|
|
808
808
|
# Version Info
|
809
809
|
|
810
|
+
2.7.2<br>
|
811
|
+
Fix to get_battery() to return error and flag status=0 in f.battery when the cloud is not returning valid data.
|
812
|
+
Fix exception calculating SoH if ratedCapacity is returned as 0 when cloud is not returning valid data.
|
813
|
+
Update charge_rate in charge_needed() to use a blended charge rate based on battery warming up during charging.
|
814
|
+
Fix exception in set_charge() caused by incorrect default parameter values.
|
815
|
+
Update charge_needed() to only show forecast that is in use.
|
816
|
+
|
817
|
+
2.7.1<br>
|
818
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
819
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
820
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
821
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
822
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
823
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
824
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
825
|
+
|
810
826
|
2.7.0<br>
|
811
827
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
828
|
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
@@ -1,17 +1,3 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: foxesscloud
|
3
|
-
Version: 2.7.0
|
4
|
-
Summary: library for accessing Fox ESS cloud data using Open API
|
5
|
-
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
|
-
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
7
|
-
Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
10
|
-
Classifier: Operating System :: OS Independent
|
11
|
-
Requires-Python: >=3.7
|
12
|
-
Description-Content-Type: text/markdown
|
13
|
-
License-File: LICENCE
|
14
|
-
|
15
1
|
# FoxESS-Cloud
|
16
2
|
|
17
3
|
<a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
|
@@ -158,12 +144,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
|
|
158
144
|
+ minSoc: min Soc setting e.g. 10 = 10%
|
159
145
|
|
160
146
|
set_charge() takes the charge times from the battery_settings and applies these to the inverter. The parameters are optional and will update battery_settings. You should specify all 3 parameter for a time period:
|
161
|
-
+ ch1: enable charge from grid for period 1 (True
|
162
|
-
+ st1: the start time for period 1
|
163
|
-
+ en1: the end time for period 1
|
164
|
-
+ ch2: enable charge from grid for period 2 (True
|
165
|
-
+ st2: the start time for period 2
|
166
|
-
+ en2: the end time for period 2
|
147
|
+
+ ch1: enable charge from grid for period 1 (default True)
|
148
|
+
+ st1: the start time for period 1 (default 0)
|
149
|
+
+ en1: the end time for period 1 (default 0)
|
150
|
+
+ ch2: enable charge from grid for period 2 (default True)
|
151
|
+
+ st2: the start time for period 2 (default 0)
|
152
|
+
+ en2: the end time for period 2 (default 0)
|
167
153
|
+ enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
|
168
154
|
|
169
155
|
set_period() returns a period structure that can be used to build a list for set_schedule()
|
@@ -807,6 +793,22 @@ This setting can be:
|
|
807
793
|
|
808
794
|
# Version Info
|
809
795
|
|
796
|
+
2.7.2<br>
|
797
|
+
Fix to get_battery() to return error and flag status=0 in f.battery when the cloud is not returning valid data.
|
798
|
+
Fix exception calculating SoH if ratedCapacity is returned as 0 when cloud is not returning valid data.
|
799
|
+
Update charge_rate in charge_needed() to use a blended charge rate based on battery warming up during charging.
|
800
|
+
Fix exception in set_charge() caused by incorrect default parameter values.
|
801
|
+
Update charge_needed() to only show forecast that is in use.
|
802
|
+
|
803
|
+
2.7.1<br>
|
804
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
805
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
806
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
807
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
808
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
809
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
810
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
811
|
+
|
810
812
|
2.7.0<br>
|
811
813
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
814
|
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 23 November 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.8.
|
13
|
+
version = "1.8.3"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -600,7 +600,7 @@ battery_params = {
|
|
600
600
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
601
601
|
'step': 5,
|
602
602
|
'offset': 5,
|
603
|
-
'charge_loss': 1.
|
603
|
+
'charge_loss': 1.08,
|
604
604
|
'discharge_loss': 0.95},
|
605
605
|
# Mira BMS with firmware 1.014 or later
|
606
606
|
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
@@ -615,6 +615,7 @@ def get_battery(info=1, rated=None, count=None):
|
|
615
615
|
if get_device() is None:
|
616
616
|
return None
|
617
617
|
output(f"getting battery", 2)
|
618
|
+
battery = None
|
618
619
|
params = {'id': device_id}
|
619
620
|
response = signed_get(path="/c/v0/device/battery/info", params=params)
|
620
621
|
if response.status_code != 200:
|
@@ -625,11 +626,8 @@ def get_battery(info=1, rated=None, count=None):
|
|
625
626
|
errno = response.json().get('errno')
|
626
627
|
output(f"** get_battery(), no result data, {errno_message(errno)}")
|
627
628
|
return None
|
628
|
-
saved_info = battery['info'] if battery is not None and battery.get('info') is not None else None
|
629
629
|
battery = result
|
630
|
-
if
|
631
|
-
battery['info'] = saved_info
|
632
|
-
elif info == 1:
|
630
|
+
if info == 1:
|
633
631
|
response = signed_get(path="/generic/v0/device/battery/list", params=params)
|
634
632
|
if response.status_code != 200:
|
635
633
|
output(f"** get_battery().info got response code {response.status_code}: {response.reason}")
|
@@ -649,6 +647,9 @@ def get_battery(info=1, rated=None, count=None):
|
|
649
647
|
battery['residual_handling'] = residual_handling
|
650
648
|
battery['soh'] = None
|
651
649
|
battery['soh_supported'] = False
|
650
|
+
if battery.get('status') is None or battery['status'] != 1:
|
651
|
+
output(f"** get_battery(): battery status not available")
|
652
|
+
return None
|
652
653
|
if battery.get('residual') is not None:
|
653
654
|
battery['residual'] /= 1000
|
654
655
|
if battery['residual_handling'] == 2:
|
@@ -680,9 +681,9 @@ def get_battery(info=1, rated=None, count=None):
|
|
680
681
|
battery['charge_loss'] = params['charge_loss']
|
681
682
|
battery['discharge_loss'] = params['discharge_loss']
|
682
683
|
if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
|
683
|
-
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
|
684
|
+
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1) if battery['ratedCapacity'] > 0.0 else None
|
684
685
|
if battery.get('temperature') is not None:
|
685
|
-
battery['charge_rate'] =
|
686
|
+
battery['charge_rate'] = interpolate((battery['temperature'] - params['offset']) / params['step'], params['table'])
|
686
687
|
return battery
|
687
688
|
|
688
689
|
def get_batteries(info=1, rated=None, count=None):
|
@@ -690,6 +691,7 @@ def get_batteries(info=1, rated=None, count=None):
|
|
690
691
|
if get_device() is None:
|
691
692
|
return None
|
692
693
|
output(f"getting batteries", 2)
|
694
|
+
batteries = None
|
693
695
|
params = {'id': device_id}
|
694
696
|
response = signed_get(path="/generic/v0/device/battery/info", params=params)
|
695
697
|
if response.status_code != 200:
|
@@ -722,6 +724,9 @@ def get_batteries(info=1, rated=None, count=None):
|
|
722
724
|
while len(count) < len(batteries):
|
723
725
|
count.append(None)
|
724
726
|
for i,b in enumerate(batteries):
|
727
|
+
if b.get('status') is None or b['status'] != 1:
|
728
|
+
output(f"** get_batteries(): battery {i+1} status not available")
|
729
|
+
continue
|
725
730
|
b['residual_handling'] = residual_handling
|
726
731
|
if b.get('info') is not None:
|
727
732
|
if b['info'].get('slaveBatteries') is not None:
|
@@ -738,6 +743,8 @@ def get_batteries(info=1, rated=None, count=None):
|
|
738
743
|
b['soh'] = int(soh) if soh.isnumeric() and int(soh) > 10 else None
|
739
744
|
b['soh_supported'] = b['soh'] is not None
|
740
745
|
for i, b in enumerate(batteries):
|
746
|
+
if b.get('status') is None or b['status'] != 1:
|
747
|
+
continue
|
741
748
|
if i == 0:
|
742
749
|
residual_handling = b['residual_handling']
|
743
750
|
get_battery(info=0)
|
@@ -751,13 +758,13 @@ def get_batteries(info=1, rated=None, count=None):
|
|
751
758
|
residual = b['capacity'] * b['soc'] / 100
|
752
759
|
b['residual'] = round(residual, 3)
|
753
760
|
if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
|
754
|
-
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
|
761
|
+
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1) if b['ratedCapacity'] > 0.0 else None
|
755
762
|
b['charge_rate'] = None
|
756
763
|
params = battery_params[b['residual_handling']]
|
757
764
|
b['charge_loss'] = params['charge_loss']
|
758
765
|
b['discharge_loss'] = params['discharge_loss']
|
759
766
|
if b.get('temperature') is not None:
|
760
|
-
b['charge_rate'] =
|
767
|
+
b['charge_rate'] = interpolate((b['temperature'] - params['offset']) / params['step'], params['table'])
|
761
768
|
return batteries
|
762
769
|
|
763
770
|
##################################################################################################
|
@@ -801,7 +808,7 @@ def time_period(t):
|
|
801
808
|
result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
|
802
809
|
return result
|
803
810
|
|
804
|
-
def set_charge(ch1=
|
811
|
+
def set_charge(ch1=True, st1=0, en1=0, ch2=True, st2=0, en2=0, force=0, enable=1):
|
805
812
|
global device_sn, battery_settings, debug_setting, messages, schedule
|
806
813
|
if get_device() is None:
|
807
814
|
return None
|
@@ -1081,6 +1088,8 @@ def get_remote_settings(key):
|
|
1081
1088
|
|
1082
1089
|
def get_named_settings(name):
|
1083
1090
|
global named_settings
|
1091
|
+
if get_device() is None:
|
1092
|
+
return None
|
1084
1093
|
if type(name) is list:
|
1085
1094
|
result = []
|
1086
1095
|
for n in name:
|
@@ -2774,8 +2783,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2774
2783
|
strategy = get_strategy(timed_mode=timed_mode)
|
2775
2784
|
for i in range(0, run_time):
|
2776
2785
|
h = time_line[i]
|
2777
|
-
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2778
|
-
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2786
|
+
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2787
|
+
'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2779
2788
|
if strategy is not None:
|
2780
2789
|
period['mode'] = 'SelfUse'
|
2781
2790
|
for d in strategy:
|
@@ -2810,33 +2819,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2810
2819
|
for i in range(0, run_time):
|
2811
2820
|
w = work_mode_timed[i]
|
2812
2821
|
w['kwh'] = kwh_current
|
2822
|
+
kwh_next = kwh_current
|
2813
2823
|
max_now = w['max_soc'] * capacity / 100
|
2814
|
-
|
2815
|
-
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2816
|
-
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2817
|
-
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2818
|
-
if kwh_current > capacity:
|
2819
|
-
# battery is full
|
2820
|
-
kwh_current = capacity
|
2821
|
-
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2822
|
-
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2824
|
+
min_soc_now = w['min_soc']
|
2823
2825
|
reserve_now = capacity * min_soc_now / 100
|
2824
|
-
|
2826
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2827
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2828
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2829
|
+
# charge from grid or force charge
|
2830
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2831
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2832
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2833
|
+
# force discharge
|
2834
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2835
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2836
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2837
|
+
else:
|
2838
|
+
# normal discharge
|
2839
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2840
|
+
if kwh_next > capacity:
|
2841
|
+
# battery is full
|
2842
|
+
kwh_next = capacity
|
2843
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2825
2844
|
# battery is empty, check if charge is needed
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2845
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2846
|
+
kwh_next = reserve_now
|
2847
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2829
2848
|
if reserve_drain <= reserve_limit:
|
2849
|
+
# float charge
|
2830
2850
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2851
|
+
kwh_next = reserve_drain
|
2831
2852
|
else:
|
2832
2853
|
# BMS power drain
|
2854
|
+
kwh_next = reserve_drain
|
2833
2855
|
reserve_drain -= bms_loss / steps_per_hour
|
2834
2856
|
else:
|
2835
2857
|
# reset drain level
|
2836
2858
|
reserve_drain = reserve_now
|
2837
|
-
if kwh_min is not None and
|
2838
|
-
kwh_min =
|
2839
|
-
|
2859
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2860
|
+
kwh_min = kwh_next
|
2861
|
+
kwh_current = kwh_next
|
2862
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2840
2863
|
|
2841
2864
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2842
2865
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -3044,7 +3067,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3044
3067
|
output(f"full_charge = {full_charge}")
|
3045
3068
|
if test_soc is not None:
|
3046
3069
|
current_soc = test_soc
|
3047
|
-
capacity = 14.
|
3070
|
+
capacity = 14.43
|
3048
3071
|
residual = test_soc * capacity / 100
|
3049
3072
|
bat_volt = 317.4
|
3050
3073
|
bat_power = 0.0
|
@@ -3060,7 +3083,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3060
3083
|
# get device and battery info from inverter
|
3061
3084
|
get_battery()
|
3062
3085
|
if battery is None or battery['status'] != 1:
|
3063
|
-
output(f"\nBattery status is not available")
|
3064
3086
|
return None
|
3065
3087
|
current_soc = battery['soc']
|
3066
3088
|
bat_volt = battery['volt']
|
@@ -3176,47 +3198,19 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3176
3198
|
consumption_timed = timed_list([consumption * x / daily_sum for x in consumption_by_hour], base_hour, run_time)
|
3177
3199
|
# get Solcast data and produce time line
|
3178
3200
|
solcast_value = None
|
3179
|
-
solcast_profile = None
|
3180
3201
|
if forecast is None and solcast_api_key is not None and solcast_api_key != 'my.solcast_api_key' and (system_time.hour in forecast_times or run_after == 0):
|
3181
3202
|
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'), d=base_time)
|
3182
3203
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
3183
3204
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
3184
3205
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
3185
|
-
solcast_from = time_hours(fsolcast.daily[today]['from']) if fsolcast.daily[today].get('from') is not None else 0
|
3186
|
-
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
|
3187
3206
|
# get forecast.solar data and produce time line
|
3188
3207
|
solar_value = None
|
3189
|
-
solar_profile = None
|
3190
3208
|
if forecast is None and solar_arrays is not None and (system_time.hour in forecast_times or run_after == 0):
|
3191
3209
|
fsolar = Solar(quiet=True, shading=charge_config.get('shading'), d=base_time)
|
3192
3210
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
3193
3211
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
3194
3212
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
3195
|
-
|
3196
|
-
if solcast_value is None and solar_value is None and debug_setting > 1:
|
3197
|
-
output(f"\nNo forecasts available at this time")
|
3198
|
-
# get generation data
|
3199
|
-
generation = None
|
3200
|
-
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
3201
|
-
gen_days = charge_config['generation_days']
|
3202
|
-
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
3203
|
-
pv_history = {}
|
3204
|
-
if history is not None and len(history) > 0:
|
3205
|
-
for day in history:
|
3206
|
-
date = day['date']
|
3207
|
-
if pv_history.get(date) is None:
|
3208
|
-
pv_history[date] = 0.0
|
3209
|
-
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
3210
|
-
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
3211
|
-
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
3212
|
-
output(f"\nGeneration (kWh):")
|
3213
|
-
s = ""
|
3214
|
-
for d in sorted(pv_history.keys())[-gen_days:]:
|
3215
|
-
s += f" {d} {pv_history[d]:4.1f},"
|
3216
|
-
output(' ' + s[:-1])
|
3217
|
-
generation = pv_sum / gen_days
|
3218
|
-
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
3219
|
-
# choose expected value and produce generation time line
|
3213
|
+
# choose expected value
|
3220
3214
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
3221
3215
|
sun_name = seasonal_sun[quarter]['name']
|
3222
3216
|
sun_profile = seasonal_sun[quarter]['sun']
|
@@ -3230,14 +3224,37 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3230
3224
|
elif solcast_value is not None:
|
3231
3225
|
expected = solcast_value
|
3232
3226
|
generation_timed = solcast_timed
|
3227
|
+
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
|
3233
3228
|
elif solar_value is not None:
|
3234
3229
|
expected = solar_value
|
3235
3230
|
generation_timed = solar_timed
|
3236
|
-
|
3237
|
-
output(f"\nNo generation data available")
|
3238
|
-
output_close()
|
3239
|
-
return None
|
3231
|
+
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
3240
3232
|
else:
|
3233
|
+
# no forecast, use generation history
|
3234
|
+
generation = None
|
3235
|
+
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
3236
|
+
gen_days = charge_config['generation_days']
|
3237
|
+
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
3238
|
+
pv_history = {}
|
3239
|
+
if history is not None and len(history) > 0:
|
3240
|
+
for day in history:
|
3241
|
+
date = day['date']
|
3242
|
+
if pv_history.get(date) is None:
|
3243
|
+
pv_history[date] = 0.0
|
3244
|
+
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
3245
|
+
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
3246
|
+
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
3247
|
+
output(f"\nGeneration (kWh):")
|
3248
|
+
s = ""
|
3249
|
+
for d in sorted(pv_history.keys())[-gen_days:]:
|
3250
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
3251
|
+
output(' ' + s[:-1])
|
3252
|
+
generation = pv_sum / gen_days
|
3253
|
+
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
3254
|
+
if generation is None or generation == 0.0:
|
3255
|
+
output(f"\nNo generation data available")
|
3256
|
+
output_close()
|
3257
|
+
return None
|
3241
3258
|
expected = generation
|
3242
3259
|
generation_timed = [expected * x / sun_sum for x in sun_timed]
|
3243
3260
|
if charge_config['forecast_selection'] == 1 and update_settings > 0:
|
@@ -3245,7 +3262,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3245
3262
|
update_settings = 0
|
3246
3263
|
# produce time lines for charge, discharge and work mode
|
3247
3264
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3248
|
-
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) +
|
3265
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
|
3249
3266
|
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3250
3267
|
for i in range(0, len(work_mode_timed)):
|
3251
3268
|
# get work mode
|
@@ -3257,19 +3274,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3257
3274
|
work_mode_timed[i]['charge'] = charge_power * duration
|
3258
3275
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
3259
3276
|
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
3260
|
-
|
3261
|
-
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
3277
|
+
work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
|
3262
3278
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
3263
|
-
discharge_timed[i] =
|
3264
|
-
|
3265
|
-
work_mode_timed[i]['hold'] = 1
|
3279
|
+
discharge_timed[i] = operating_loss
|
3280
|
+
work_mode_timed[i]['hold'] = 1
|
3266
3281
|
elif timed_mode > 0 and work_mode == 'Backup':
|
3267
|
-
discharge_timed[i] =
|
3282
|
+
discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
|
3268
3283
|
elif timed_mode > 0 and work_mode == 'Feedin':
|
3269
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3284
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3270
3285
|
0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
|
3271
3286
|
else: # work_mode == 'SelfUse'
|
3272
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3287
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3273
3288
|
0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
|
3274
3289
|
work_mode_timed[i]['pv'] = charge_timed[i]
|
3275
3290
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
@@ -3282,8 +3297,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3282
3297
|
kwh_contingency = consumption * contingency / 100
|
3283
3298
|
kwh_needed = reserve + kwh_contingency - kwh_min
|
3284
3299
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
3285
|
-
|
3286
|
-
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
3300
|
+
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends (without charging)
|
3287
3301
|
target_soc = charge_config.get('target_soc')
|
3288
3302
|
target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
3289
3303
|
if target_kwh > (end_residual + kwh_needed):
|
@@ -3302,7 +3316,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3302
3316
|
hours = 0.0
|
3303
3317
|
start_timed = time_to_end
|
3304
3318
|
end_timed = time_to_end
|
3305
|
-
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3306
3319
|
else:
|
3307
3320
|
# work out time to add kwh_needed to battery
|
3308
3321
|
charge_rate = charge_power * charge_loss
|
@@ -3313,30 +3326,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3313
3326
|
charge_message = "with charge added"
|
3314
3327
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3315
3328
|
# check if charge time exceeded or charge needed exceeds capacity
|
3316
|
-
hours_to_full = (capacity -
|
3317
|
-
if hours > charge_time:
|
3329
|
+
hours_to_full = (capacity - end_residual) / charge_rate
|
3330
|
+
if hours > charge_time or bat_hold == 2:
|
3318
3331
|
hours = charge_time
|
3319
3332
|
elif hours > hours_to_full:
|
3320
|
-
kwh_shortfall = kwh_needed - (capacity -
|
3333
|
+
kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
|
3321
3334
|
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
3322
3335
|
hours = required if required > hours and required < charge_time else charge_time
|
3323
|
-
# round charge time
|
3336
|
+
# round charge time
|
3324
3337
|
min_hours = charge_config['min_hours']
|
3325
3338
|
hours = int(hours / min_hours + 0.99) * min_hours
|
3326
|
-
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3327
|
-
kwh_added += discharge_rate * hours # discharge saved by charging
|
3328
|
-
kwh_spare = kwh_min - reserve + kwh_added
|
3329
3339
|
# rework charge and discharge
|
3330
3340
|
charge_period = get_best_charge_period(start_at, hours)
|
3331
|
-
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else
|
3341
|
+
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
|
3332
3342
|
price = charge_period.get('price') if charge_period is not None else None
|
3333
3343
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3334
3344
|
end_timed = start_timed + hours * steps_per_hour
|
3335
3345
|
start_residual = interpolate(start_timed, bat_timed)
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3346
|
+
start_soc = start_residual / capacity * 100
|
3347
|
+
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3348
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3349
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3350
|
+
output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3351
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
|
3352
|
+
+ (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
|
3340
3353
|
for i in range(int(time_to_start), int(time_to_end)):
|
3341
3354
|
j = i + 1
|
3342
3355
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3359,8 +3372,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3359
3372
|
# rebuild the battery residual with the charge added and min_soc
|
3360
3373
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3361
3374
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3375
|
+
end_soc = end_residual / capacity * 100
|
3362
3376
|
# show the results
|
3363
|
-
output(f" End SoC: {
|
3377
|
+
output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3364
3378
|
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3365
3379
|
if not charge_today:
|
3366
3380
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
@@ -4356,7 +4370,7 @@ class Solcast :
|
|
4356
4370
|
estimate_values = [self.estimate[r][hours_time(t)] for t in times]
|
4357
4371
|
plots[r] = estimate_values
|
4358
4372
|
total_forecast = 0.0
|
4359
|
-
if self.daily.get(day) is not None:
|
4373
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4360
4374
|
sun_times = get_suntimes(day)
|
4361
4375
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4362
4376
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
@@ -4679,7 +4693,7 @@ class Solar :
|
|
4679
4693
|
estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
|
4680
4694
|
plots[r] = estimate_values
|
4681
4695
|
total_forecast = 0.0
|
4682
|
-
if self.daily.get(day) is not None:
|
4696
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4683
4697
|
sun_times = get_suntimes(day)
|
4684
4698
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4685
4699
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 23 November 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.7.
|
13
|
+
version = "2.7.2"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -564,7 +564,7 @@ battery_params = {
|
|
564
564
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
565
565
|
'step': 5,
|
566
566
|
'offset': 5,
|
567
|
-
'charge_loss': 1.
|
567
|
+
'charge_loss': 1.08,
|
568
568
|
'discharge_loss': 0.95},
|
569
569
|
# Mira BMS with firmware 1.014 or later
|
570
570
|
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
@@ -582,13 +582,18 @@ def get_battery(info=0, v=None, rated=None, count=None):
|
|
582
582
|
if v is None:
|
583
583
|
v = battery_vars
|
584
584
|
result = get_real(v)
|
585
|
-
|
586
|
-
battery = {}
|
585
|
+
battery = {}
|
587
586
|
for i in range(0, len(battery_vars)):
|
588
587
|
battery[battery_data[i]] = result[i].get('value')
|
589
588
|
battery['residual_handling'] = residual_handling
|
590
589
|
battery['soh'] = None
|
591
590
|
battery['soh_supported'] = False
|
591
|
+
if battery.get('status') is None:
|
592
|
+
battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 0 else 1
|
593
|
+
if battery['status'] != 1:
|
594
|
+
output(f"** get_battery(): battery status not available")
|
595
|
+
return None
|
596
|
+
battery['status'] = 1
|
592
597
|
if battery['residual_handling'] == 2:
|
593
598
|
capacity = battery.get('residual')
|
594
599
|
soc = battery.get('soc')
|
@@ -621,9 +626,9 @@ def get_battery(info=0, v=None, rated=None, count=None):
|
|
621
626
|
battery['charge_loss'] = params['charge_loss']
|
622
627
|
battery['discharge_loss'] = params['discharge_loss']
|
623
628
|
if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
|
624
|
-
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
|
629
|
+
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1) if battery['ratedCapacity'] > 0.0 else None
|
625
630
|
if battery.get('temperature') is not None:
|
626
|
-
battery['charge_rate'] =
|
631
|
+
battery['charge_rate'] = interpolate((battery['temperature'] - params['offset']) / params['step'], params['table'])
|
627
632
|
return battery
|
628
633
|
|
629
634
|
def get_batteries(info=0, rated=None, count=None):
|
@@ -674,7 +679,7 @@ def time_period(t, n):
|
|
674
679
|
result += f" Charge from grid" if enable else f" Battery Hold"
|
675
680
|
return result
|
676
681
|
|
677
|
-
def set_charge(ch1=
|
682
|
+
def set_charge(ch1=True, st1=0, en1=0, ch2=True, st2=0, en2=0, force = 0, enable=1):
|
678
683
|
global device_sn, battery_settings, debug_setting, time_period_vars
|
679
684
|
if get_device() is None:
|
680
685
|
return None
|
@@ -2439,8 +2444,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2439
2444
|
strategy = get_strategy(timed_mode=timed_mode)
|
2440
2445
|
for i in range(0, run_time):
|
2441
2446
|
h = time_line[i]
|
2442
|
-
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2443
|
-
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2447
|
+
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2448
|
+
'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2444
2449
|
if strategy is not None:
|
2445
2450
|
period['mode'] = 'SelfUse'
|
2446
2451
|
for d in strategy:
|
@@ -2474,33 +2479,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2474
2479
|
for i in range(0, run_time):
|
2475
2480
|
w = work_mode_timed[i]
|
2476
2481
|
w['kwh'] = kwh_current
|
2482
|
+
kwh_next = kwh_current
|
2477
2483
|
max_now = w['max_soc'] * capacity / 100
|
2478
|
-
|
2479
|
-
kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2480
|
-
kwh_current = max_now if kwh_current > max_now else kwh_current
|
2481
|
-
kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2482
|
-
if kwh_current > capacity:
|
2483
|
-
# battery is full
|
2484
|
-
kwh_current = capacity
|
2485
|
-
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2486
|
-
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2484
|
+
min_soc_now = w['min_soc']
|
2487
2485
|
reserve_now = capacity * min_soc_now / 100
|
2488
|
-
|
2486
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2487
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2488
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2489
|
+
# charge from grid or force charge
|
2490
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2491
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2492
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2493
|
+
# force discharge
|
2494
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2495
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2496
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2497
|
+
else:
|
2498
|
+
# normal discharge
|
2499
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2500
|
+
if kwh_next > capacity:
|
2501
|
+
# battery is full
|
2502
|
+
kwh_next = capacity
|
2503
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2489
2504
|
# battery is empty, check if charge is needed
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2505
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2506
|
+
kwh_next = reserve_now
|
2507
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2493
2508
|
if reserve_drain <= reserve_limit:
|
2509
|
+
# float charge
|
2494
2510
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2511
|
+
kwh_next = reserve_drain
|
2495
2512
|
else:
|
2496
2513
|
# BMS power drain
|
2514
|
+
kwh_next = reserve_drain
|
2497
2515
|
reserve_drain -= bms_loss / steps_per_hour
|
2498
2516
|
else:
|
2499
2517
|
# reset drain level
|
2500
2518
|
reserve_drain = reserve_now
|
2501
|
-
if kwh_min is not None and
|
2502
|
-
kwh_min =
|
2503
|
-
|
2519
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2520
|
+
kwh_min = kwh_next
|
2521
|
+
kwh_current = kwh_next
|
2522
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2504
2523
|
|
2505
2524
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2506
2525
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -2708,7 +2727,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2708
2727
|
output(f"full_charge = {full_charge}")
|
2709
2728
|
if test_soc is not None:
|
2710
2729
|
current_soc = test_soc
|
2711
|
-
capacity = 14.
|
2730
|
+
capacity = 14.43
|
2712
2731
|
residual = test_soc * capacity / 100
|
2713
2732
|
bat_volt = 317.4
|
2714
2733
|
bat_power = 0.0
|
@@ -2724,7 +2743,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2724
2743
|
# get device and battery info from inverter
|
2725
2744
|
get_battery()
|
2726
2745
|
if battery is None or battery['status'] != 1:
|
2727
|
-
output(f"\nBattery status is not available")
|
2728
2746
|
return None
|
2729
2747
|
current_soc = battery['soc']
|
2730
2748
|
bat_volt = battery['volt']
|
@@ -2844,46 +2862,19 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2844
2862
|
consumption_timed = timed_list([consumption * x / daily_sum for x in consumption_by_hour], base_hour, run_time)
|
2845
2863
|
# get Solcast data and produce time line
|
2846
2864
|
solcast_value = None
|
2847
|
-
solcast_profile = None
|
2848
2865
|
if forecast is None and solcast_api_key is not None and solcast_api_key != 'my.solcast_api_key' and (system_time.hour in forecast_times or run_after == 0):
|
2849
2866
|
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'), d=base_time)
|
2850
2867
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2851
2868
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2852
2869
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2853
|
-
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
|
2854
2870
|
# get forecast.solar data and produce time line
|
2855
2871
|
solar_value = None
|
2856
|
-
solar_profile = None
|
2857
2872
|
if forecast is None and solar_arrays is not None and (system_time.hour in forecast_times or run_after == 0):
|
2858
2873
|
fsolar = Solar(quiet=True, shading=charge_config.get('shading'), d=base_time)
|
2859
2874
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2860
2875
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2861
2876
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2862
|
-
|
2863
|
-
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2864
|
-
output(f"\nNo forecasts available at this time")
|
2865
|
-
# get generation data
|
2866
|
-
generation = None
|
2867
|
-
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
2868
|
-
gen_days = charge_config['generation_days']
|
2869
|
-
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
2870
|
-
pv_history = {}
|
2871
|
-
if history is not None and len(history) > 0:
|
2872
|
-
for day in history:
|
2873
|
-
date = day['date']
|
2874
|
-
if pv_history.get(date) is None:
|
2875
|
-
pv_history[date] = 0.0
|
2876
|
-
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
2877
|
-
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
2878
|
-
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
2879
|
-
output(f"\nGeneration (kWh):")
|
2880
|
-
s = ""
|
2881
|
-
for d in sorted(pv_history.keys())[-gen_days:]:
|
2882
|
-
s += f" {d}: {pv_history[d]:4.1f},"
|
2883
|
-
output(' ' + s[:-1])
|
2884
|
-
generation = pv_sum / gen_days
|
2885
|
-
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2886
|
-
# choose expected value and produce generation time line
|
2877
|
+
# choose expected value
|
2887
2878
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
2888
2879
|
sun_name = seasonal_sun[quarter]['name']
|
2889
2880
|
sun_profile = seasonal_sun[quarter]['sun']
|
@@ -2897,14 +2888,37 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2897
2888
|
elif solcast_value is not None:
|
2898
2889
|
expected = solcast_value
|
2899
2890
|
generation_timed = solcast_timed
|
2891
|
+
output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
|
2900
2892
|
elif solar_value is not None:
|
2901
2893
|
expected = solar_value
|
2902
2894
|
generation_timed = solar_timed
|
2903
|
-
|
2904
|
-
output(f"\nNo generation data available")
|
2905
|
-
output_close()
|
2906
|
-
return None
|
2895
|
+
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
2907
2896
|
else:
|
2897
|
+
# no forecast, use generation data
|
2898
|
+
generation = None
|
2899
|
+
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
2900
|
+
gen_days = charge_config['generation_days']
|
2901
|
+
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
2902
|
+
pv_history = {}
|
2903
|
+
if history is not None and len(history) > 0:
|
2904
|
+
for day in history:
|
2905
|
+
date = day['date']
|
2906
|
+
if pv_history.get(date) is None:
|
2907
|
+
pv_history[date] = 0.0
|
2908
|
+
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
2909
|
+
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
2910
|
+
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
2911
|
+
output(f"\nGeneration (kWh):")
|
2912
|
+
s = ""
|
2913
|
+
for d in sorted(pv_history.keys())[-gen_days:]:
|
2914
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
2915
|
+
output(' ' + s[:-1])
|
2916
|
+
generation = pv_sum / gen_days
|
2917
|
+
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2918
|
+
if generation is None or generation == 0.0:
|
2919
|
+
output(f"\nNo generation data available")
|
2920
|
+
output_close()
|
2921
|
+
return None
|
2908
2922
|
expected = generation
|
2909
2923
|
generation_timed = [expected * x / sun_sum for x in sun_timed]
|
2910
2924
|
if charge_config['forecast_selection'] == 1 and update_settings > 0:
|
@@ -2912,7 +2926,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2912
2926
|
update_settings = 0
|
2913
2927
|
# produce time lines for charge, discharge and work mode
|
2914
2928
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2915
|
-
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) +
|
2929
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
|
2916
2930
|
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2917
2931
|
for i in range(0, len(work_mode_timed)):
|
2918
2932
|
# get work mode
|
@@ -2924,19 +2938,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2924
2938
|
work_mode_timed[i]['charge'] = charge_power * duration
|
2925
2939
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
2926
2940
|
fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
|
2927
|
-
|
2928
|
-
discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
|
2941
|
+
work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
|
2929
2942
|
elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
|
2930
|
-
discharge_timed[i] =
|
2931
|
-
|
2932
|
-
work_mode_timed[i]['hold'] = 1
|
2943
|
+
discharge_timed[i] = operating_loss
|
2944
|
+
work_mode_timed[i]['hold'] = 1
|
2933
2945
|
elif timed_mode > 0 and work_mode == 'Backup':
|
2934
|
-
discharge_timed[i] =
|
2946
|
+
discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
|
2935
2947
|
elif timed_mode > 0 and work_mode == 'Feedin':
|
2936
|
-
(discharge_timed[i], charge_timed[i]) = (
|
2948
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
2937
2949
|
0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
|
2938
2950
|
else: # work_mode == 'SelfUse'
|
2939
|
-
(discharge_timed[i], charge_timed[i]) = (
|
2951
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
2940
2952
|
0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
|
2941
2953
|
work_mode_timed[i]['pv'] = charge_timed[i]
|
2942
2954
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
@@ -2949,7 +2961,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2949
2961
|
kwh_contingency = consumption * contingency / 100
|
2950
2962
|
kwh_needed = reserve + kwh_contingency - kwh_min
|
2951
2963
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
2952
|
-
start_soc = int(start_residual / capacity * 100 + 0.5)
|
2953
2964
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
2954
2965
|
target_soc = charge_config.get('target_soc')
|
2955
2966
|
target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
@@ -2969,7 +2980,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2969
2980
|
hours = 0.0
|
2970
2981
|
start_timed = time_to_end
|
2971
2982
|
end_timed = time_to_end
|
2972
|
-
end_soc = int(end_residual / capacity * 100 + 0.5)
|
2973
2983
|
else:
|
2974
2984
|
# work out time to add kwh_needed to battery
|
2975
2985
|
charge_rate = charge_power * charge_loss
|
@@ -2980,30 +2990,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2980
2990
|
charge_message = "with charge added"
|
2981
2991
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2982
2992
|
# check if charge time exceeded or charge needed exceeds capacity
|
2983
|
-
hours_to_full = (capacity -
|
2984
|
-
if hours > charge_time:
|
2993
|
+
hours_to_full = (capacity - end_residual) / charge_rate
|
2994
|
+
if hours > charge_time or bat_hold == 2:
|
2985
2995
|
hours = charge_time
|
2986
2996
|
elif hours > hours_to_full:
|
2987
|
-
kwh_shortfall = kwh_needed - (capacity -
|
2997
|
+
kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
|
2988
2998
|
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
2989
2999
|
hours = required if required > hours and required < charge_time else charge_time
|
2990
|
-
# round charge time
|
3000
|
+
# round charge time
|
2991
3001
|
min_hours = charge_config['min_hours']
|
2992
3002
|
hours = int(hours / min_hours + 0.99) * min_hours
|
2993
|
-
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
2994
|
-
kwh_added += discharge_rate * hours # discharge saved during charging
|
2995
|
-
kwh_spare = kwh_min - reserve + kwh_added
|
2996
3003
|
# rework charge and discharge
|
2997
3004
|
charge_period = get_best_charge_period(start_at, hours)
|
2998
|
-
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else
|
3005
|
+
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
|
2999
3006
|
price = charge_period.get('price') if charge_period is not None else None
|
3000
3007
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3001
3008
|
end_timed = start_timed + hours * steps_per_hour
|
3002
3009
|
start_residual = interpolate(start_timed, bat_timed)
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3010
|
+
start_soc = start_residual / capacity * 100
|
3011
|
+
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3012
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3013
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3014
|
+
output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3015
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
|
3016
|
+
+ (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
|
3007
3017
|
for i in range(int(time_to_start), int(time_to_end)):
|
3008
3018
|
j = i + 1
|
3009
3019
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3026,8 +3036,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3026
3036
|
# rebuild the battery residual with any charge added and min_soc
|
3027
3037
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3028
3038
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3039
|
+
end_soc = end_residual / capacity * 100
|
3029
3040
|
# show the results
|
3030
|
-
output(f" End SoC: {
|
3041
|
+
output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3031
3042
|
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3032
3043
|
if not charge_today:
|
3033
3044
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
@@ -4020,7 +4031,7 @@ class Solcast :
|
|
4020
4031
|
estimate_values = [self.estimate[r][hours_time(t)] for t in times]
|
4021
4032
|
plots[r] = estimate_values
|
4022
4033
|
total_forecast = 0.0
|
4023
|
-
if self.daily.get(day) is not None:
|
4034
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4024
4035
|
sun_times = get_suntimes(day)
|
4025
4036
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4026
4037
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
@@ -4343,7 +4354,7 @@ class Solar :
|
|
4343
4354
|
estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
|
4344
4355
|
plots[r] = estimate_values
|
4345
4356
|
total_forecast = 0.0
|
4346
|
-
if self.daily.get(day) is not None:
|
4357
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4347
4358
|
sun_times = get_suntimes(day)
|
4348
4359
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4349
4360
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
@@ -1,3 +1,17 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: foxesscloud
|
3
|
+
Version: 2.7.2
|
4
|
+
Summary: library for accessing Fox ESS cloud data using Open API
|
5
|
+
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
|
+
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
7
|
+
Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENCE
|
14
|
+
|
1
15
|
# FoxESS-Cloud
|
2
16
|
|
3
17
|
<a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
|
@@ -144,12 +158,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
|
|
144
158
|
+ minSoc: min Soc setting e.g. 10 = 10%
|
145
159
|
|
146
160
|
set_charge() takes the charge times from the battery_settings and applies these to the inverter. The parameters are optional and will update battery_settings. You should specify all 3 parameter for a time period:
|
147
|
-
+ ch1: enable charge from grid for period 1 (True
|
148
|
-
+ st1: the start time for period 1
|
149
|
-
+ en1: the end time for period 1
|
150
|
-
+ ch2: enable charge from grid for period 2 (True
|
151
|
-
+ st2: the start time for period 2
|
152
|
-
+ en2: the end time for period 2
|
161
|
+
+ ch1: enable charge from grid for period 1 (default True)
|
162
|
+
+ st1: the start time for period 1 (default 0)
|
163
|
+
+ en1: the end time for period 1 (default 0)
|
164
|
+
+ ch2: enable charge from grid for period 2 (default True)
|
165
|
+
+ st2: the start time for period 2 (default 0)
|
166
|
+
+ en2: the end time for period 2 (default 0)
|
153
167
|
+ enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
|
154
168
|
|
155
169
|
set_period() returns a period structure that can be used to build a list for set_schedule()
|
@@ -793,6 +807,22 @@ This setting can be:
|
|
793
807
|
|
794
808
|
# Version Info
|
795
809
|
|
810
|
+
2.7.2<br>
|
811
|
+
Fix to get_battery() to return error and flag status=0 in f.battery when the cloud is not returning valid data.
|
812
|
+
Fix exception calculating SoH if ratedCapacity is returned as 0 when cloud is not returning valid data.
|
813
|
+
Update charge_rate in charge_needed() to use a blended charge rate based on battery warming up during charging.
|
814
|
+
Fix exception in set_charge() caused by incorrect default parameter values.
|
815
|
+
Update charge_needed() to only show forecast that is in use.
|
816
|
+
|
817
|
+
2.7.1<br>
|
818
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
819
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
820
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
821
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
822
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
823
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
824
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
825
|
+
|
796
826
|
2.7.0<br>
|
797
827
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
798
828
|
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|