foxesscloud 2.7.0__tar.gz → 2.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/PKG-INFO +17 -7
- foxesscloud-2.7.0/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.7.1/README.md +16 -20
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/pyproject.toml +1 -1
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/src/foxesscloud/foxesscloud.py +88 -75
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/src/foxesscloud/openapi.py +85 -74
- foxesscloud-2.7.0/README.md → foxesscloud-2.7.1/src/foxesscloud.egg-info/PKG-INFO +30 -6
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/LICENCE +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/setup.cfg +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.7.0 → foxesscloud-2.7.1}/src/foxesscloud.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.7.
|
3
|
+
Version: 2.7.1
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -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,16 @@ This setting can be:
|
|
807
807
|
|
808
808
|
# Version Info
|
809
809
|
|
810
|
+
2.7.1<br>
|
811
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
812
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
813
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
814
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
815
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
816
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
817
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
818
|
+
|
819
|
+
|
810
820
|
2.7.0<br>
|
811
821
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
822
|
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,16 @@ This setting can be:
|
|
807
793
|
|
808
794
|
# Version Info
|
809
795
|
|
796
|
+
2.7.1<br>
|
797
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
798
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
799
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
800
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
801
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
802
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
803
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
804
|
+
|
805
|
+
|
810
806
|
2.7.0<br>
|
811
807
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
808
|
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: 07 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.2"
|
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],
|
@@ -801,7 +801,7 @@ def time_period(t):
|
|
801
801
|
result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
|
802
802
|
return result
|
803
803
|
|
804
|
-
def set_charge(ch1=
|
804
|
+
def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force=0, enable=1):
|
805
805
|
global device_sn, battery_settings, debug_setting, messages, schedule
|
806
806
|
if get_device() is None:
|
807
807
|
return None
|
@@ -1081,6 +1081,8 @@ def get_remote_settings(key):
|
|
1081
1081
|
|
1082
1082
|
def get_named_settings(name):
|
1083
1083
|
global named_settings
|
1084
|
+
if get_device() is None:
|
1085
|
+
return None
|
1084
1086
|
if type(name) is list:
|
1085
1087
|
result = []
|
1086
1088
|
for n in name:
|
@@ -2774,8 +2776,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2774
2776
|
strategy = get_strategy(timed_mode=timed_mode)
|
2775
2777
|
for i in range(0, run_time):
|
2776
2778
|
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}
|
2779
|
+
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
|
2780
|
+
'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2779
2781
|
if strategy is not None:
|
2780
2782
|
period['mode'] = 'SelfUse'
|
2781
2783
|
for d in strategy:
|
@@ -2810,33 +2812,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2810
2812
|
for i in range(0, run_time):
|
2811
2813
|
w = work_mode_timed[i]
|
2812
2814
|
w['kwh'] = kwh_current
|
2815
|
+
kwh_next = kwh_current
|
2813
2816
|
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']
|
2817
|
+
min_soc_now = w['min_soc']
|
2823
2818
|
reserve_now = capacity * min_soc_now / 100
|
2824
|
-
|
2819
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2820
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2821
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2822
|
+
# charge from grid or force charge
|
2823
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2824
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2825
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2826
|
+
# force discharge
|
2827
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2828
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2829
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2830
|
+
else:
|
2831
|
+
# normal discharge
|
2832
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2833
|
+
if kwh_next > capacity:
|
2834
|
+
# battery is full
|
2835
|
+
kwh_next = capacity
|
2836
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2825
2837
|
# battery is empty, check if charge is needed
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2838
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2839
|
+
kwh_next = reserve_now
|
2840
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2829
2841
|
if reserve_drain <= reserve_limit:
|
2842
|
+
# float charge
|
2830
2843
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2844
|
+
kwh_next = reserve_drain
|
2831
2845
|
else:
|
2832
2846
|
# BMS power drain
|
2847
|
+
kwh_next = reserve_drain
|
2833
2848
|
reserve_drain -= bms_loss / steps_per_hour
|
2834
2849
|
else:
|
2835
2850
|
# reset drain level
|
2836
2851
|
reserve_drain = reserve_now
|
2837
|
-
if kwh_min is not None and
|
2838
|
-
kwh_min =
|
2839
|
-
|
2852
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2853
|
+
kwh_min = kwh_next
|
2854
|
+
kwh_current = kwh_next
|
2855
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2840
2856
|
|
2841
2857
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2842
2858
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -3044,7 +3060,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3044
3060
|
output(f"full_charge = {full_charge}")
|
3045
3061
|
if test_soc is not None:
|
3046
3062
|
current_soc = test_soc
|
3047
|
-
capacity = 14.
|
3063
|
+
capacity = 14.46
|
3048
3064
|
residual = test_soc * capacity / 100
|
3049
3065
|
bat_volt = 317.4
|
3050
3066
|
bat_power = 0.0
|
@@ -3195,27 +3211,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3195
3211
|
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
3196
3212
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
3197
3213
|
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
3214
|
# choose expected value and produce generation time line
|
3220
3215
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
3221
3216
|
sun_name = seasonal_sun[quarter]['name']
|
@@ -3233,11 +3228,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3233
3228
|
elif solar_value is not None:
|
3234
3229
|
expected = solar_value
|
3235
3230
|
generation_timed = solar_timed
|
3236
|
-
elif generation is None or generation == 0.0:
|
3237
|
-
output(f"\nNo generation data available")
|
3238
|
-
output_close()
|
3239
|
-
return None
|
3240
3231
|
else:
|
3232
|
+
# no forecast, use generation history
|
3233
|
+
generation = None
|
3234
|
+
last_date = today if hour_now >= charge_config['use_today'] else yesterday
|
3235
|
+
gen_days = charge_config['generation_days']
|
3236
|
+
history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
|
3237
|
+
pv_history = {}
|
3238
|
+
if history is not None and len(history) > 0:
|
3239
|
+
for day in history:
|
3240
|
+
date = day['date']
|
3241
|
+
if pv_history.get(date) is None:
|
3242
|
+
pv_history[date] = 0.0
|
3243
|
+
if day.get('kwh') is not None and day.get('kwh_neg') is not None:
|
3244
|
+
pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
|
3245
|
+
pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
|
3246
|
+
output(f"\nGeneration (kWh):")
|
3247
|
+
s = ""
|
3248
|
+
for d in sorted(pv_history.keys())[-gen_days:]:
|
3249
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
3250
|
+
output(' ' + s[:-1])
|
3251
|
+
generation = pv_sum / gen_days
|
3252
|
+
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
3253
|
+
if generation is None or generation == 0.0:
|
3254
|
+
output(f"\nNo generation data available")
|
3255
|
+
output_close()
|
3256
|
+
return None
|
3241
3257
|
expected = generation
|
3242
3258
|
generation_timed = [expected * x / sun_sum for x in sun_timed]
|
3243
3259
|
if charge_config['forecast_selection'] == 1 and update_settings > 0:
|
@@ -3245,7 +3261,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3245
3261
|
update_settings = 0
|
3246
3262
|
# produce time lines for charge, discharge and work mode
|
3247
3263
|
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]) +
|
3264
|
+
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
|
3249
3265
|
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3250
3266
|
for i in range(0, len(work_mode_timed)):
|
3251
3267
|
# get work mode
|
@@ -3257,19 +3273,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3257
3273
|
work_mode_timed[i]['charge'] = charge_power * duration
|
3258
3274
|
elif timed_mode > 0 and work_mode == 'ForceDischarge':
|
3259
3275
|
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
|
3276
|
+
work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
|
3262
3277
|
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
|
3278
|
+
discharge_timed[i] = operating_loss
|
3279
|
+
work_mode_timed[i]['hold'] = 1
|
3266
3280
|
elif timed_mode > 0 and work_mode == 'Backup':
|
3267
|
-
discharge_timed[i] =
|
3281
|
+
discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
|
3268
3282
|
elif timed_mode > 0 and work_mode == 'Feedin':
|
3269
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3283
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3270
3284
|
0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
|
3271
3285
|
else: # work_mode == 'SelfUse'
|
3272
|
-
(discharge_timed[i], charge_timed[i]) = (
|
3286
|
+
(discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
|
3273
3287
|
0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
|
3274
3288
|
work_mode_timed[i]['pv'] = charge_timed[i]
|
3275
3289
|
work_mode_timed[i]['discharge'] = discharge_timed[i]
|
@@ -3282,8 +3296,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3282
3296
|
kwh_contingency = consumption * contingency / 100
|
3283
3297
|
kwh_needed = reserve + kwh_contingency - kwh_min
|
3284
3298
|
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
|
3299
|
+
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends (without charging)
|
3287
3300
|
target_soc = charge_config.get('target_soc')
|
3288
3301
|
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
3302
|
if target_kwh > (end_residual + kwh_needed):
|
@@ -3302,7 +3315,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3302
3315
|
hours = 0.0
|
3303
3316
|
start_timed = time_to_end
|
3304
3317
|
end_timed = time_to_end
|
3305
|
-
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3306
3318
|
else:
|
3307
3319
|
# work out time to add kwh_needed to battery
|
3308
3320
|
charge_rate = charge_power * charge_loss
|
@@ -3313,30 +3325,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3313
3325
|
charge_message = "with charge added"
|
3314
3326
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3315
3327
|
# check if charge time exceeded or charge needed exceeds capacity
|
3316
|
-
hours_to_full = (capacity -
|
3317
|
-
if hours > charge_time:
|
3328
|
+
hours_to_full = (capacity - end_residual) / charge_rate
|
3329
|
+
if hours > charge_time or bat_hold == 2:
|
3318
3330
|
hours = charge_time
|
3319
3331
|
elif hours > hours_to_full:
|
3320
|
-
kwh_shortfall = kwh_needed - (capacity -
|
3332
|
+
kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
|
3321
3333
|
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
3322
3334
|
hours = required if required > hours and required < charge_time else charge_time
|
3323
|
-
# round charge time
|
3335
|
+
# round charge time
|
3324
3336
|
min_hours = charge_config['min_hours']
|
3325
3337
|
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
3338
|
# rework charge and discharge
|
3330
3339
|
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
|
3340
|
+
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
|
3332
3341
|
price = charge_period.get('price') if charge_period is not None else None
|
3333
3342
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3334
3343
|
end_timed = start_timed + hours * steps_per_hour
|
3335
3344
|
start_residual = interpolate(start_timed, bat_timed)
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3345
|
+
start_soc = start_residual / capacity * 100
|
3346
|
+
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3347
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3348
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3349
|
+
output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3350
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
|
3351
|
+
+ (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
|
3340
3352
|
for i in range(int(time_to_start), int(time_to_end)):
|
3341
3353
|
j = i + 1
|
3342
3354
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3359,8 +3371,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3359
3371
|
# rebuild the battery residual with the charge added and min_soc
|
3360
3372
|
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
|
3361
3373
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3374
|
+
end_soc = end_residual / capacity * 100
|
3362
3375
|
# show the results
|
3363
|
-
output(f" End SoC: {
|
3376
|
+
output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3364
3377
|
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3365
3378
|
if not charge_today:
|
3366
3379
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
@@ -4356,7 +4369,7 @@ class Solcast :
|
|
4356
4369
|
estimate_values = [self.estimate[r][hours_time(t)] for t in times]
|
4357
4370
|
plots[r] = estimate_values
|
4358
4371
|
total_forecast = 0.0
|
4359
|
-
if self.daily.get(day) is not None:
|
4372
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4360
4373
|
sun_times = get_suntimes(day)
|
4361
4374
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4362
4375
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
@@ -4679,7 +4692,7 @@ class Solar :
|
|
4679
4692
|
estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
|
4680
4693
|
plots[r] = estimate_values
|
4681
4694
|
total_forecast = 0.0
|
4682
|
-
if self.daily.get(day) is not None:
|
4695
|
+
if hasattr(self, 'daily') and self.daily.get(day) is not None:
|
4683
4696
|
sun_times = get_suntimes(day)
|
4684
4697
|
print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
|
4685
4698
|
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: 07 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.1"
|
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],
|
@@ -674,7 +674,7 @@ def time_period(t, n):
|
|
674
674
|
result += f" Charge from grid" if enable else f" Battery Hold"
|
675
675
|
return result
|
676
676
|
|
677
|
-
def set_charge(ch1=
|
677
|
+
def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force = 0, enable=1):
|
678
678
|
global device_sn, battery_settings, debug_setting, time_period_vars
|
679
679
|
if get_device() is None:
|
680
680
|
return None
|
@@ -2439,8 +2439,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
|
|
2439
2439
|
strategy = get_strategy(timed_mode=timed_mode)
|
2440
2440
|
for i in range(0, run_time):
|
2441
2441
|
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}
|
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, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
|
2444
2444
|
if strategy is not None:
|
2445
2445
|
period['mode'] = 'SelfUse'
|
2446
2446
|
for d in strategy:
|
@@ -2474,33 +2474,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2474
2474
|
for i in range(0, run_time):
|
2475
2475
|
w = work_mode_timed[i]
|
2476
2476
|
w['kwh'] = kwh_current
|
2477
|
+
kwh_next = kwh_current
|
2477
2478
|
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']
|
2479
|
+
min_soc_now = w['min_soc']
|
2487
2480
|
reserve_now = capacity * min_soc_now / 100
|
2488
|
-
|
2481
|
+
reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
|
2482
|
+
fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
|
2483
|
+
if kwh_next < max_now and w['charge'] > 0.0:
|
2484
|
+
# charge from grid or force charge
|
2485
|
+
kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
|
2486
|
+
kwh_next = max_now if kwh_next > max_now else kwh_next
|
2487
|
+
if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
|
2488
|
+
# force discharge
|
2489
|
+
kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
|
2490
|
+
if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
|
2491
|
+
kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
|
2492
|
+
else:
|
2493
|
+
# normal discharge
|
2494
|
+
kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
|
2495
|
+
if kwh_next > capacity:
|
2496
|
+
# battery is full
|
2497
|
+
kwh_next = capacity
|
2498
|
+
if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
|
2489
2499
|
# battery is empty, check if charge is needed
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2500
|
+
if kwh_current > reserve_now and kwh_next < reserve_now:
|
2501
|
+
kwh_next = reserve_now
|
2502
|
+
reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
|
2493
2503
|
if reserve_drain <= reserve_limit:
|
2504
|
+
# float charge
|
2494
2505
|
reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
|
2506
|
+
kwh_next = reserve_drain
|
2495
2507
|
else:
|
2496
2508
|
# BMS power drain
|
2509
|
+
kwh_next = reserve_drain
|
2497
2510
|
reserve_drain -= bms_loss / steps_per_hour
|
2498
2511
|
else:
|
2499
2512
|
# reset drain level
|
2500
2513
|
reserve_drain = reserve_now
|
2501
|
-
if kwh_min is not None and
|
2502
|
-
kwh_min =
|
2503
|
-
|
2514
|
+
if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
|
2515
|
+
kwh_min = kwh_next
|
2516
|
+
kwh_current = kwh_next
|
2517
|
+
return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
|
2504
2518
|
|
2505
2519
|
# use work_mode_timed to generate time periods for the inverter schedule
|
2506
2520
|
def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
@@ -2708,7 +2722,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2708
2722
|
output(f"full_charge = {full_charge}")
|
2709
2723
|
if test_soc is not None:
|
2710
2724
|
current_soc = test_soc
|
2711
|
-
capacity = 14.
|
2725
|
+
capacity = 14.46
|
2712
2726
|
residual = test_soc * capacity / 100
|
2713
2727
|
bat_volt = 317.4
|
2714
2728
|
bat_power = 0.0
|
@@ -2862,27 +2876,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2862
2876
|
output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
|
2863
2877
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2864
2878
|
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
2879
|
# choose expected value and produce generation time line
|
2887
2880
|
quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
|
2888
2881
|
sun_name = seasonal_sun[quarter]['name']
|
@@ -2900,11 +2893,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2900
2893
|
elif solar_value is not None:
|
2901
2894
|
expected = solar_value
|
2902
2895
|
generation_timed = solar_timed
|
2903
|
-
elif generation is None or generation == 0.0:
|
2904
|
-
output(f"\nNo generation data available")
|
2905
|
-
output_close()
|
2906
|
-
return None
|
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.1
|
4
|
+
Summary: library for accessing Fox ESS cloud data using Open API
|
5
|
+
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
|
+
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
7
|
+
Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.7
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENCE
|
14
|
+
|
1
15
|
# FoxESS-Cloud
|
2
16
|
|
3
17
|
<a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
|
@@ -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,16 @@ This setting can be:
|
|
793
807
|
|
794
808
|
# Version Info
|
795
809
|
|
810
|
+
2.7.1<br>
|
811
|
+
Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
|
812
|
+
Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
|
813
|
+
Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
|
814
|
+
Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
|
815
|
+
Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
|
816
|
+
Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
|
817
|
+
Correct exception in Solcast and Solar when a forecast is not available.
|
818
|
+
|
819
|
+
|
796
820
|
2.7.0<br>
|
797
821
|
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
798
822
|
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
|