foxesscloud 2.6.9__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {foxesscloud-2.6.9/src/foxesscloud.egg-info → foxesscloud-2.7.0}/PKG-INFO +10 -2
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/README.md +9 -1
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/pyproject.toml +1 -1
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/src/foxesscloud/foxesscloud.py +48 -40
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/src/foxesscloud/openapi.py +48 -39
- {foxesscloud-2.6.9 → foxesscloud-2.7.0/src/foxesscloud.egg-info}/PKG-INFO +10 -2
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/LICENCE +0 -0
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/setup.cfg +0 -0
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.6.9 → foxesscloud-2.7.0}/src/foxesscloud.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.0
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -341,7 +341,7 @@ The previous section provides functions that can be used to access and control y
|
|
341
341
|
Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
|
342
342
|
|
343
343
|
```
|
344
|
-
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
|
344
|
+
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
|
345
345
|
```
|
346
346
|
|
347
347
|
All the parameters are optional:
|
@@ -352,6 +352,7 @@ All the parameters are optional:
|
|
352
352
|
+ update_settings: 0 no changes, 1 update charge settings. The default is 0
|
353
353
|
+ show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
|
354
354
|
+ show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
|
355
|
+
+ timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
|
355
356
|
|
356
357
|
### Modelling
|
357
358
|
|
@@ -394,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
394
395
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
395
396
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
396
397
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
398
|
+
charge_loss: None # loss converting charge energy to stored energy
|
399
|
+
discharge_loss: None # loss converting stored energy to discharge energy
|
397
400
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
398
401
|
bms_power: 50 # BMS power consumption in W
|
399
402
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -804,6 +807,11 @@ This setting can be:
|
|
804
807
|
|
805
808
|
# Version Info
|
806
809
|
|
810
|
+
2.7.0<br>
|
811
|
+
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
|
+
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
813
|
+
Correct problem with missing periods of actual data in forecast.compare()
|
814
|
+
|
807
815
|
2.6.9<br>
|
808
816
|
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
809
817
|
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
@@ -327,7 +327,7 @@ The previous section provides functions that can be used to access and control y
|
|
327
327
|
Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
|
328
328
|
|
329
329
|
```
|
330
|
-
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
|
330
|
+
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
|
331
331
|
```
|
332
332
|
|
333
333
|
All the parameters are optional:
|
@@ -338,6 +338,7 @@ All the parameters are optional:
|
|
338
338
|
+ update_settings: 0 no changes, 1 update charge settings. The default is 0
|
339
339
|
+ show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
|
340
340
|
+ show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
|
341
|
+
+ timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
|
341
342
|
|
342
343
|
### Modelling
|
343
344
|
|
@@ -380,6 +381,8 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
380
381
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
381
382
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
382
383
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
384
|
+
charge_loss: None # loss converting charge energy to stored energy
|
385
|
+
discharge_loss: None # loss converting stored energy to discharge energy
|
383
386
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
384
387
|
bms_power: 50 # BMS power consumption in W
|
385
388
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -790,6 +793,11 @@ This setting can be:
|
|
790
793
|
|
791
794
|
# Version Info
|
792
795
|
|
796
|
+
2.7.0<br>
|
797
|
+
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
798
|
+
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
799
|
+
Correct problem with missing periods of actual data in forecast.compare()
|
800
|
+
|
793
801
|
2.6.9<br>
|
794
802
|
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
795
803
|
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 06 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.1"
|
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.07,
|
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],
|
@@ -798,7 +798,7 @@ def get_charge():
|
|
798
798
|
def time_period(t):
|
799
799
|
result = f"{t['startTime']['hour']:02d}:{t['startTime']['minute']:02d}-{t['endTime']['hour']:02d}:{t['endTime']['minute']:02d}"
|
800
800
|
if t['startTime']['hour'] != t['endTime']['hour'] or t['startTime']['minute'] != t['endTime']['minute']:
|
801
|
-
result += f" Charge from grid" if t['enableGrid'] else f"
|
801
|
+
result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
|
802
802
|
return result
|
803
803
|
|
804
804
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force=0, enable=1):
|
@@ -1748,9 +1748,7 @@ def plot_raw(result, plot=1, station=0):
|
|
1748
1748
|
def report_value_profile(result):
|
1749
1749
|
if type(result) is not list or result[0]['type'] != 'day':
|
1750
1750
|
return (None, None)
|
1751
|
-
data = []
|
1752
|
-
for h in range(0,24):
|
1753
|
-
data.append((0.0, 0)) # value sum, count of values
|
1751
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1754
1752
|
totals = 0
|
1755
1753
|
n = 0
|
1756
1754
|
for day in result:
|
@@ -1780,6 +1778,30 @@ def report_value_profile(result):
|
|
1780
1778
|
# forwards compatibility
|
1781
1779
|
get_history = get_raw
|
1782
1780
|
|
1781
|
+
# rescale history data based on time and steps
|
1782
|
+
def rescale_history(data, steps):
|
1783
|
+
if data is None or len(data) < 1:
|
1784
|
+
return None
|
1785
|
+
result = [None for i in range(0, 24 * steps)]
|
1786
|
+
bst = 1 if 'BST' in data[0]['time'] else 0
|
1787
|
+
average = 0.0
|
1788
|
+
n = 0
|
1789
|
+
i = 0
|
1790
|
+
for d in data:
|
1791
|
+
h = round_time(time_hours(d['time'][11:]) + bst)
|
1792
|
+
new_i = int(h * steps)
|
1793
|
+
if new_i != i and i < len(result):
|
1794
|
+
result[i] = average / n if n > 0 else None
|
1795
|
+
average = 0.0
|
1796
|
+
n = 0
|
1797
|
+
i = new_i
|
1798
|
+
if d['value'] is not None:
|
1799
|
+
average += d['value']
|
1800
|
+
n += 1
|
1801
|
+
if n > 0 and i < len(result):
|
1802
|
+
result[i] = average / n
|
1803
|
+
return result
|
1804
|
+
|
1783
1805
|
##################################################################################################
|
1784
1806
|
# get energy report data in kWh
|
1785
1807
|
##################################################################################################
|
@@ -2780,8 +2802,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2780
2802
|
global charge_config, steps_per_hour
|
2781
2803
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2782
2804
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2783
|
-
charge_loss = charge_config['
|
2784
|
-
discharge_loss = charge_config['
|
2805
|
+
charge_loss = charge_config['_charge_loss']
|
2806
|
+
discharge_loss = charge_config['_discharge_loss']
|
2785
2807
|
charge_limit = charge_config['charge_limit']
|
2786
2808
|
float_charge = charge_config['float_charge']
|
2787
2809
|
run_time = len(work_mode_timed)
|
@@ -2874,6 +2896,8 @@ charge_config = {
|
|
2874
2896
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2875
2897
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2876
2898
|
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2899
|
+
'charge_loss': None, # loss converting charge energy to stored energy
|
2900
|
+
'discharge_loss': None, # loss converting stored energy to discharge energy
|
2877
2901
|
'inverter_power': 101, # Inverter power consumption in W
|
2878
2902
|
'bms_power': 50, # BMS power consumption in W
|
2879
2903
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -3026,8 +3050,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3026
3050
|
bat_power = 0.0
|
3027
3051
|
temperature = 30
|
3028
3052
|
bms_charge_current = 15
|
3029
|
-
charge_loss = battery_params[2]['charge_loss']
|
3030
|
-
discharge_loss = battery_params[2]['discharge_loss']
|
3053
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
|
3054
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
|
3031
3055
|
bat_current = 0.0
|
3032
3056
|
device_power = 6.0
|
3033
3057
|
device_current = 35
|
@@ -3049,8 +3073,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3049
3073
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
3050
3074
|
return None
|
3051
3075
|
bms_charge_current = battery.get('charge_rate')
|
3052
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3053
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
3076
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3077
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
3054
3078
|
device_power = device.get('power')
|
3055
3079
|
device_current = device.get('max_charge_current')
|
3056
3080
|
model = device.get('deviceType')
|
@@ -3079,7 +3103,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3079
3103
|
output(f" Temperature: {temperature:.1f}°C")
|
3080
3104
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
3081
3105
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
3082
|
-
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
|
3106
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
|
3083
3107
|
# inverter losses
|
3084
3108
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
3085
3109
|
operating_loss = inverter_power / 1000
|
@@ -3108,8 +3132,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3108
3132
|
charge_config['charge_limit'] = charge_limit
|
3109
3133
|
charge_config['charge_power'] = charge_power
|
3110
3134
|
charge_config['float_charge'] = float_charge
|
3111
|
-
charge_config['
|
3112
|
-
charge_config['
|
3135
|
+
charge_config['_charge_loss'] = charge_loss
|
3136
|
+
charge_config['_discharge_loss'] = discharge_loss
|
3113
3137
|
# display what we have
|
3114
3138
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
3115
3139
|
output(f"\nDevice Info:")
|
@@ -4304,17 +4328,9 @@ class Solcast :
|
|
4304
4328
|
total_actual = None
|
4305
4329
|
self.actual = get_history('day', d=day, v=v)
|
4306
4330
|
plots = {}
|
4331
|
+
times = [i/2 for i in range(0, 48)]
|
4307
4332
|
for v in self.actual:
|
4308
|
-
|
4309
|
-
actual_values = []
|
4310
|
-
average = 0.0
|
4311
|
-
for i in range(0, len(v.get('data'))):
|
4312
|
-
average += v['data'][i]['value'] / 6
|
4313
|
-
if i % 6 == 5:
|
4314
|
-
times.append(round_time((i - 5) / 12))
|
4315
|
-
actual_values.append(average)
|
4316
|
-
average = 0
|
4317
|
-
plots[v['variable']] = actual_values
|
4333
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4318
4334
|
if v['variable'] == 'pvPower':
|
4319
4335
|
total_actual = v.get('kwh')
|
4320
4336
|
if total_actual is None:
|
@@ -4346,10 +4362,10 @@ class Solcast :
|
|
4346
4362
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
4347
4363
|
total_forecast = sum(forecast_values) / 2
|
4348
4364
|
plots['forecast'] = forecast_values
|
4349
|
-
if total_actual is not None:
|
4350
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4351
4365
|
if total_forecast is not None:
|
4352
4366
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4367
|
+
if total_actual is not None:
|
4368
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4353
4369
|
print()
|
4354
4370
|
title = f"Forecast / Actual PV Power on {day}"
|
4355
4371
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4637,17 +4653,9 @@ class Solar :
|
|
4637
4653
|
total_actual = None
|
4638
4654
|
self.actual = get_history('day', d=day, v=v)
|
4639
4655
|
plots = {}
|
4656
|
+
times = [i/2 for i in range(0, 48)]
|
4640
4657
|
for v in self.actual:
|
4641
|
-
|
4642
|
-
actual_values = []
|
4643
|
-
average = 0.0
|
4644
|
-
for i in range(0, len(v.get('data'))):
|
4645
|
-
average += v['data'][i]['value'] / 6
|
4646
|
-
if i % 6 == 5:
|
4647
|
-
times.append(round_time((i - 5) / 12))
|
4648
|
-
actual_values.append(average)
|
4649
|
-
average = 0
|
4650
|
-
plots[v['variable']] = actual_values
|
4658
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4651
4659
|
if v['variable'] == 'pvPower':
|
4652
4660
|
total_actual = v.get('kwh')
|
4653
4661
|
if total_actual is None:
|
@@ -4677,10 +4685,10 @@ class Solar :
|
|
4677
4685
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4678
4686
|
total_forecast = sum(forecast_values) / 2
|
4679
4687
|
plots['forecast'] = forecast_values
|
4680
|
-
if total_actual is not None:
|
4681
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4682
4688
|
if total_forecast is not None:
|
4683
4689
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4690
|
+
if total_actual is not None:
|
4691
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4684
4692
|
print()
|
4685
4693
|
title = f"Forecast / Actual PV Power on {day}"
|
4686
4694
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 06 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.
|
13
|
+
version = "2.7.0"
|
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.07,
|
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],
|
@@ -671,7 +671,7 @@ def time_period(t, n):
|
|
671
671
|
(enable, start, end) = (t['enable1'], t['startTime1'], t['endTime1']) if n == 1 else (t['enable2'], t['startTime2'], t['endTime2'])
|
672
672
|
result = f"{start['hour']:02d}:{start['minute']:02d}-{end['hour']:02d}:{end['minute']:02d}"
|
673
673
|
if start['hour'] != end['hour'] or start['minute'] != end['minute']:
|
674
|
-
result += f" Charge from grid" if enable else f"
|
674
|
+
result += f" Charge from grid" if enable else f" Battery Hold"
|
675
675
|
return result
|
676
676
|
|
677
677
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
|
@@ -1448,9 +1448,7 @@ get_raw = get_history
|
|
1448
1448
|
def report_value_profile(result):
|
1449
1449
|
if type(result) is not list or result[0]['type'] != 'day':
|
1450
1450
|
return (None, None)
|
1451
|
-
data = []
|
1452
|
-
for h in range(0,24):
|
1453
|
-
data.append((0.0, 0)) # value sum, count of values
|
1451
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1454
1452
|
totals = 0
|
1455
1453
|
n = 0
|
1456
1454
|
for day in result:
|
@@ -1477,6 +1475,30 @@ def report_value_profile(result):
|
|
1477
1475
|
result.append(by_hour[t] * daily_average / current_total)
|
1478
1476
|
return (daily_average, result)
|
1479
1477
|
|
1478
|
+
# rescale history data based on time and steps
|
1479
|
+
def rescale_history(data, steps):
|
1480
|
+
if data is None:
|
1481
|
+
return None
|
1482
|
+
result = [None for i in range(0, 24 * steps)]
|
1483
|
+
bst = 1 if 'BST' in data[0]['time'] else 0
|
1484
|
+
average = 0.0
|
1485
|
+
n = 0
|
1486
|
+
i = 0
|
1487
|
+
for d in data:
|
1488
|
+
h = round_time(time_hours(d['time'][11:]) + bst)
|
1489
|
+
new_i = int(h * steps)
|
1490
|
+
if new_i != i and i < len(result):
|
1491
|
+
result[i] = average / n if n > 0 else None
|
1492
|
+
average = 0.0
|
1493
|
+
n = 0
|
1494
|
+
i = new_i
|
1495
|
+
if d['value'] is not None:
|
1496
|
+
average += d['value']
|
1497
|
+
n += 1
|
1498
|
+
if n > 0 and i < len(result):
|
1499
|
+
result[i] = average / n
|
1500
|
+
return result
|
1501
|
+
|
1480
1502
|
|
1481
1503
|
##################################################################################################
|
1482
1504
|
# get production report in kWh
|
@@ -2444,8 +2466,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2444
2466
|
global charge_config, steps_per_hour
|
2445
2467
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2446
2468
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2447
|
-
charge_loss = charge_config['
|
2448
|
-
discharge_loss = charge_config['
|
2469
|
+
charge_loss = charge_config['_charge_loss']
|
2470
|
+
discharge_loss = charge_config['_discharge_loss']
|
2449
2471
|
charge_limit = charge_config['charge_limit']
|
2450
2472
|
float_charge = charge_config['float_charge']
|
2451
2473
|
run_time = len(work_mode_timed)
|
@@ -2538,6 +2560,8 @@ charge_config = {
|
|
2538
2560
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2539
2561
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2540
2562
|
'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
|
2563
|
+
'charge_loss': None, # loss converting charge energy to stored energy
|
2564
|
+
'discharge_loss': None, # loss converting stored energy to discharge energy
|
2541
2565
|
'inverter_power': 101, # Inverter power consumption in W
|
2542
2566
|
'bms_power': 50, # BMS power consumption in W
|
2543
2567
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2690,8 +2714,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2690
2714
|
bat_power = 0.0
|
2691
2715
|
temperature = 30
|
2692
2716
|
bms_charge_current = 15
|
2693
|
-
charge_loss = battery_params[2]['charge_loss']
|
2694
|
-
discharge_loss = battery_params[2]['discharge_loss']
|
2717
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
|
2718
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
|
2695
2719
|
bat_current = 0.0
|
2696
2720
|
device_power = 6.0
|
2697
2721
|
device_current = 35
|
@@ -2713,8 +2737,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2713
2737
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2714
2738
|
return None
|
2715
2739
|
bms_charge_current = battery.get('charge_rate')
|
2716
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2717
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2740
|
+
charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2741
|
+
discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2718
2742
|
device_power = device.get('power')
|
2719
2743
|
device_current = device.get('max_charge_current')
|
2720
2744
|
model = device.get('deviceType')
|
@@ -2743,6 +2767,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2743
2767
|
output(f" Max Charge: {charge_current:.1f}A")
|
2744
2768
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2745
2769
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2770
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
|
2746
2771
|
# charge current may be derated based on temperature
|
2747
2772
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2748
2773
|
if charge_current > bms_charge_current:
|
@@ -2775,8 +2800,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2775
2800
|
charge_config['charge_limit'] = charge_limit
|
2776
2801
|
charge_config['charge_power'] = charge_power
|
2777
2802
|
charge_config['float_charge'] = float_charge
|
2778
|
-
charge_config['
|
2779
|
-
charge_config['
|
2803
|
+
charge_config['_charge_loss'] = charge_loss
|
2804
|
+
charge_config['_discharge_loss'] = discharge_loss
|
2780
2805
|
# display what we have
|
2781
2806
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2782
2807
|
output(f"\nDevice Info:")
|
@@ -3967,17 +3992,9 @@ class Solcast :
|
|
3967
3992
|
total_actual = None
|
3968
3993
|
self.actual = get_history('day', d=day, v=v)
|
3969
3994
|
plots = {}
|
3995
|
+
times = [i/2 for i in range(0, 48)]
|
3970
3996
|
for v in self.actual:
|
3971
|
-
|
3972
|
-
actual_values = []
|
3973
|
-
average = 0.0
|
3974
|
-
for i in range(0, len(v.get('data'))):
|
3975
|
-
average += v['data'][i]['value'] / 6
|
3976
|
-
if i % 6 == 5:
|
3977
|
-
times.append(round_time((i - 5) / 12))
|
3978
|
-
actual_values.append(average)
|
3979
|
-
average = 0
|
3980
|
-
plots[v['variable']] = actual_values
|
3997
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
3981
3998
|
if v['variable'] == 'pvPower':
|
3982
3999
|
total_actual = v.get('kwh')
|
3983
4000
|
if total_actual is None:
|
@@ -4009,10 +4026,10 @@ class Solcast :
|
|
4009
4026
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
4010
4027
|
total_forecast = sum(forecast_values) / 2
|
4011
4028
|
plots['forecast'] = forecast_values
|
4012
|
-
if total_actual is not None:
|
4013
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4014
4029
|
if total_forecast is not None:
|
4015
4030
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4031
|
+
if total_actual is not None:
|
4032
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4016
4033
|
print()
|
4017
4034
|
title = f"Forecast / Actual PV Power on {day}"
|
4018
4035
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4300,17 +4317,9 @@ class Solar :
|
|
4300
4317
|
total_actual = None
|
4301
4318
|
self.actual = get_history('day', d=day, v=v)
|
4302
4319
|
plots = {}
|
4320
|
+
times = [i/2 for i in range(0, 48)]
|
4303
4321
|
for v in self.actual:
|
4304
|
-
|
4305
|
-
actual_values = []
|
4306
|
-
average = 0.0
|
4307
|
-
for i in range(0, len(v.get('data'))):
|
4308
|
-
average += v['data'][i]['value'] / 6
|
4309
|
-
if i % 6 == 5:
|
4310
|
-
times.append(round_time((i - 5) / 12))
|
4311
|
-
actual_values.append(average)
|
4312
|
-
average = 0
|
4313
|
-
plots[v['variable']] = actual_values
|
4322
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4314
4323
|
if v['variable'] == 'pvPower':
|
4315
4324
|
total_actual = v.get('kwh')
|
4316
4325
|
if total_actual is None:
|
@@ -4340,10 +4349,10 @@ class Solar :
|
|
4340
4349
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4341
4350
|
total_forecast = sum(forecast_values) / 2
|
4342
4351
|
plots['forecast'] = forecast_values
|
4343
|
-
if total_actual is not None:
|
4344
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4345
4352
|
if total_forecast is not None:
|
4346
4353
|
print(f" Total forecast: {total_forecast:.3f}kWh")
|
4354
|
+
if total_actual is not None:
|
4355
|
+
print(f" Total actual: {total_actual:.3f}kWh")
|
4347
4356
|
print()
|
4348
4357
|
title = f"Forecast / Actual PV Power on {day}"
|
4349
4358
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.0
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -341,7 +341,7 @@ The previous section provides functions that can be used to access and control y
|
|
341
341
|
Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
|
342
342
|
|
343
343
|
```
|
344
|
-
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
|
344
|
+
f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
|
345
345
|
```
|
346
346
|
|
347
347
|
All the parameters are optional:
|
@@ -352,6 +352,7 @@ All the parameters are optional:
|
|
352
352
|
+ update_settings: 0 no changes, 1 update charge settings. The default is 0
|
353
353
|
+ show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
|
354
354
|
+ show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
|
355
|
+
+ timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
|
355
356
|
|
356
357
|
### Modelling
|
357
358
|
|
@@ -394,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
394
395
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
395
396
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
396
397
|
ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
|
398
|
+
charge_loss: None # loss converting charge energy to stored energy
|
399
|
+
discharge_loss: None # loss converting stored energy to discharge energy
|
397
400
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
398
401
|
bms_power: 50 # BMS power consumption in W
|
399
402
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -804,6 +807,11 @@ This setting can be:
|
|
804
807
|
|
805
808
|
# Version Info
|
806
809
|
|
810
|
+
2.7.0<br>
|
811
|
+
Allow charge_loss / discharge_loss to be configured for charge_needed().
|
812
|
+
Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
|
813
|
+
Correct problem with missing periods of actual data in forecast.compare()
|
814
|
+
|
807
815
|
2.6.9<br>
|
808
816
|
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
809
817
|
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|