foxesscloud 2.6.4__tar.gz → 2.6.6__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.4 → foxesscloud-2.6.6}/PKG-INFO +26 -1
- foxesscloud-2.6.4/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.6.6/README.md +25 -14
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/pyproject.toml +1 -1
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/src/foxesscloud/foxesscloud.py +98 -59
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/src/foxesscloud/openapi.py +139 -200
- foxesscloud-2.6.4/README.md → foxesscloud-2.6.6/src/foxesscloud.egg-info/PKG-INFO +39 -0
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/LICENCE +0 -0
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/setup.cfg +0 -0
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.6.4 → foxesscloud-2.6.6}/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.6.
|
3
|
+
Version: 2.6.6
|
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
|
@@ -111,6 +111,7 @@ f.get_charge()
|
|
111
111
|
f.get_min()
|
112
112
|
f.get_flag()
|
113
113
|
f.get_schedule()
|
114
|
+
f.get_named_settings(name)
|
114
115
|
|
115
116
|
```
|
116
117
|
Each of these calls will return a dictionary or list containing the relevant information.
|
@@ -130,6 +131,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
|
|
130
131
|
|
131
132
|
get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
|
132
133
|
|
134
|
+
get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
|
135
|
+
+ f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
|
136
|
+
+ the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
|
137
|
+
|
133
138
|
|
134
139
|
## Inverter Settings
|
135
140
|
You can change inverter settings using:
|
@@ -140,6 +145,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
|
|
140
145
|
f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
|
141
146
|
f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
|
142
147
|
f.set_schedule(periods, enable)
|
148
|
+
f.set_named_settings(name, value)
|
143
149
|
```
|
144
150
|
|
145
151
|
set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
|
@@ -180,6 +186,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
|
|
180
186
|
+ periods: a time segment or list of time segments created using f.set_period().
|
181
187
|
+ enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
|
182
188
|
|
189
|
+
set_named_settings() sets the 'name' setting to 'value'.
|
190
|
+
+ 'name' may also be a list of (name, value) pairs.
|
191
|
+
+ the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
|
192
|
+
|
183
193
|
|
184
194
|
## Real Time Data
|
185
195
|
Real time data reports the latest values for inverter variables, collected every 5 minutes:
|
@@ -787,6 +797,21 @@ This setting can be:
|
|
787
797
|
|
788
798
|
# Version Info
|
789
799
|
|
800
|
+
2.6.6<br>
|
801
|
+
Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
|
802
|
+
Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
|
803
|
+
Allow unlimited periods in strategy, including overlap with charge periods but warn and limit if the periods sent to inverter would be more than 8.
|
804
|
+
Improve behaviour prediction for schedules when clocks change due to day light saving.
|
805
|
+
Improve schedule generation and prediction when Min Soc changes.
|
806
|
+
Cache Solcast RIDS to reduce API usage (run with reload=1 if arrays are edited and cached RIDs need to be updated).
|
807
|
+
Remove spurious error message when (failing) to get inverter work mode.
|
808
|
+
|
809
|
+
2.6.5<br>
|
810
|
+
Add get_named_settings() and set_named_settings().
|
811
|
+
Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
|
812
|
+
Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
|
813
|
+
Update calibration of 'charge_loss' and 'discharge_loss'.
|
814
|
+
|
790
815
|
2.6.4<br>
|
791
816
|
Increase default plungs_slots from 6 to 8.
|
792
817
|
Correct battery capacity in get_batteries().
|
@@ -1,17 +1,3 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: foxesscloud
|
3
|
-
Version: 2.6.4
|
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>
|
@@ -111,6 +97,7 @@ f.get_charge()
|
|
111
97
|
f.get_min()
|
112
98
|
f.get_flag()
|
113
99
|
f.get_schedule()
|
100
|
+
f.get_named_settings(name)
|
114
101
|
|
115
102
|
```
|
116
103
|
Each of these calls will return a dictionary or list containing the relevant information.
|
@@ -130,6 +117,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
|
|
130
117
|
|
131
118
|
get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
|
132
119
|
|
120
|
+
get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
|
121
|
+
+ f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
|
122
|
+
+ the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
|
123
|
+
|
133
124
|
|
134
125
|
## Inverter Settings
|
135
126
|
You can change inverter settings using:
|
@@ -140,6 +131,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
|
|
140
131
|
f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
|
141
132
|
f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
|
142
133
|
f.set_schedule(periods, enable)
|
134
|
+
f.set_named_settings(name, value)
|
143
135
|
```
|
144
136
|
|
145
137
|
set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
|
@@ -180,6 +172,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
|
|
180
172
|
+ periods: a time segment or list of time segments created using f.set_period().
|
181
173
|
+ enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
|
182
174
|
|
175
|
+
set_named_settings() sets the 'name' setting to 'value'.
|
176
|
+
+ 'name' may also be a list of (name, value) pairs.
|
177
|
+
+ the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
|
178
|
+
|
183
179
|
|
184
180
|
## Real Time Data
|
185
181
|
Real time data reports the latest values for inverter variables, collected every 5 minutes:
|
@@ -787,6 +783,21 @@ This setting can be:
|
|
787
783
|
|
788
784
|
# Version Info
|
789
785
|
|
786
|
+
2.6.6<br>
|
787
|
+
Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
|
788
|
+
Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
|
789
|
+
Allow unlimited periods in strategy, including overlap with charge periods but warn and limit if the periods sent to inverter would be more than 8.
|
790
|
+
Improve behaviour prediction for schedules when clocks change due to day light saving.
|
791
|
+
Improve schedule generation and prediction when Min Soc changes.
|
792
|
+
Cache Solcast RIDS to reduce API usage (run with reload=1 if arrays are edited and cached RIDs need to be updated).
|
793
|
+
Remove spurious error message when (failing) to get inverter work mode.
|
794
|
+
|
795
|
+
2.6.5<br>
|
796
|
+
Add get_named_settings() and set_named_settings().
|
797
|
+
Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
|
798
|
+
Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
|
799
|
+
Update calibration of 'charge_loss' and 'discharge_loss'.
|
800
|
+
|
790
801
|
2.6.4<br>
|
791
802
|
Increase default plungs_slots from 6 to 8.
|
792
803
|
Correct battery capacity in get_batteries().
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 01 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.7.
|
13
|
+
version = "1.7.8"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -583,7 +583,7 @@ battery = None
|
|
583
583
|
batteries = None
|
584
584
|
battery_settings = None
|
585
585
|
|
586
|
-
# 1 = Residual Energy, 2 = Residual Capacity
|
586
|
+
# 1 = Residual Energy, 2 = Residual Capacity (HV), 3 = Residual Capacity per battery (Mira)
|
587
587
|
residual_handling = 1
|
588
588
|
|
589
589
|
# charge rates based on residual_handling
|
@@ -593,13 +593,20 @@ battery_params = {
|
|
593
593
|
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
594
594
|
'step': 5,
|
595
595
|
'offset': 5,
|
596
|
-
'charge_loss': 0.
|
597
|
-
'discharge_loss': 0.
|
596
|
+
'charge_loss': 0.974,
|
597
|
+
'discharge_loss': 0.974},
|
598
|
+
# HV BMS v2 with firmware 1.014 or later
|
598
599
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
599
600
|
'step': 5,
|
600
601
|
'offset': 5,
|
601
602
|
'charge_loss': 1.08,
|
602
|
-
'discharge_loss': 0.
|
603
|
+
'discharge_loss': 0.95},
|
604
|
+
# Mira BMS with firmware 1.014 or later
|
605
|
+
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
606
|
+
'step': 5,
|
607
|
+
'offset': 5,
|
608
|
+
'charge_loss': 0.974,
|
609
|
+
'discharge_loss': 0.974},
|
603
610
|
}
|
604
611
|
|
605
612
|
def get_battery(info=1):
|
@@ -632,10 +639,13 @@ def get_battery(info=1):
|
|
632
639
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
633
640
|
else:
|
634
641
|
battery['info'] = result['batteries'][0]
|
635
|
-
if battery['info']
|
642
|
+
if battery['info'].get('slaveBatteries') is not None:
|
643
|
+
battery['count'] = len(battery['info']['slaveBatteries'])
|
644
|
+
if battery['info']['masterSN'][:7] == '60BBHV2' and battery['info']['masterVersion'] >= '1.014':
|
636
645
|
residual_handling = 2
|
646
|
+
elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
|
647
|
+
residual_handling = 3
|
637
648
|
battery['residual_handling'] = residual_handling
|
638
|
-
battery['rated_capacity'] = None
|
639
649
|
battery['soh'] = None
|
640
650
|
battery['soh_supported'] = False
|
641
651
|
if battery.get('residual') is not None:
|
@@ -644,6 +654,18 @@ def get_battery(info=1):
|
|
644
654
|
capacity = battery.get('residual')
|
645
655
|
soc = battery.get('soc')
|
646
656
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
657
|
+
if battery.get('count') is None:
|
658
|
+
battery['count'] = int(battery['volt'] / 49)
|
659
|
+
if battery.get('ratedCapacity') is None:
|
660
|
+
battery['ratedCapacity'] = 2560 * battery['count']
|
661
|
+
elif battery['residual_handling'] == 3:
|
662
|
+
if battery.get('count') is None:
|
663
|
+
battery['count'] = int(battery['volt'] / 49)
|
664
|
+
capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
|
665
|
+
soc = battery.get('soc')
|
666
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
667
|
+
if battery.get('ratedCapacity') is None:
|
668
|
+
battery['ratedCapacity'] = 2450 * battery['count']
|
647
669
|
else:
|
648
670
|
residual = battery.get('residual')
|
649
671
|
soc = battery.get('soc')
|
@@ -654,6 +676,8 @@ def get_battery(info=1):
|
|
654
676
|
params = battery_params[battery['residual_handling']]
|
655
677
|
battery['charge_loss'] = params['charge_loss']
|
656
678
|
battery['discharge_loss'] = params['discharge_loss']
|
679
|
+
if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
|
680
|
+
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
|
657
681
|
if battery.get('temperature') is not None:
|
658
682
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
659
683
|
return battery
|
@@ -688,8 +712,13 @@ def get_batteries(info=1):
|
|
688
712
|
batteries[i]['info'] = result['batteries'][i]
|
689
713
|
for b in batteries:
|
690
714
|
b['residual_handling'] = residual_handling
|
691
|
-
if b.get('info') is not None
|
692
|
-
b['
|
715
|
+
if b.get('info') is not None:
|
716
|
+
if b['info'].get('slaveBatteries') is not None:
|
717
|
+
b['count'] = len(b['info']['slaveBatteries'])
|
718
|
+
if b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
|
719
|
+
b['residual_handling'] = 2
|
720
|
+
elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
|
721
|
+
residual_handling = 3
|
693
722
|
rated_capacity = b.get('ratedCapacity')
|
694
723
|
b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else None
|
695
724
|
soh = b.get('soh')
|
@@ -708,7 +737,7 @@ def get_batteries(info=1):
|
|
708
737
|
soc = b.get('soc')
|
709
738
|
residual = b['capacity'] * b['soc'] / 100
|
710
739
|
b['residual'] = round(residual, 3)
|
711
|
-
if b.get('
|
740
|
+
if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
|
712
741
|
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
|
713
742
|
b['charge_rate'] = 50
|
714
743
|
params = battery_params[b['residual_handling']]
|
@@ -1022,7 +1051,7 @@ def get_remote_settings(key):
|
|
1022
1051
|
result = response.json().get('result')
|
1023
1052
|
if result is None:
|
1024
1053
|
errno = response.json().get('errno')
|
1025
|
-
output(f"** get_remote_settings(), no result data, {errno_message(
|
1054
|
+
output(f"** get_remote_settings(), no result data, {errno_message(response)}")
|
1026
1055
|
return None
|
1027
1056
|
values = result.get('values')
|
1028
1057
|
if values is None:
|
@@ -1323,10 +1352,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1323
1352
|
price = segment.get('price')
|
1324
1353
|
start = time_hours(start)
|
1325
1354
|
# adjust exclusive time to inclusive
|
1326
|
-
end =
|
1355
|
+
end = time_hours(end)
|
1327
1356
|
if start is None or end is None or start >= end:
|
1328
1357
|
output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
|
1329
1358
|
return None
|
1359
|
+
end = round_time(end - 1/60)
|
1330
1360
|
mode = 'SelfUse' if mode is None else mode
|
1331
1361
|
if mode not in work_modes:
|
1332
1362
|
output(f"** mode must be one of {work_modes}")
|
@@ -1399,7 +1429,7 @@ def set_schedule(periods=None, template=None, enable=True):
|
|
1399
1429
|
if len(periods) > 8:
|
1400
1430
|
output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
|
1401
1431
|
return None
|
1402
|
-
data = {'pollcy': periods, 'deviceSN': device_sn}
|
1432
|
+
data = {'pollcy': periods[-8:], 'deviceSN': device_sn}
|
1403
1433
|
schedule['pollcy'] = periods
|
1404
1434
|
schedule['template_id'] = None
|
1405
1435
|
elif template is not None:
|
@@ -1436,7 +1466,7 @@ def set_schedule(periods=None, template=None, enable=True):
|
|
1436
1466
|
# d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
|
1437
1467
|
# v = list of variables to get
|
1438
1468
|
# summary = 0: raw data, 1: add max, min, sum, 2: summarise and drop raw data, 3: calculate state
|
1439
|
-
# save = "xxxxx": save the raw results to
|
1469
|
+
# save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
|
1440
1470
|
# load = "<file>": load the raw results from <file>
|
1441
1471
|
# plot = 0: no plot, 1: plot variables separately, 2: combine variables
|
1442
1472
|
# station = 0: use device_id, 1: use station_id
|
@@ -1504,7 +1534,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1504
1534
|
result = json.load(file)
|
1505
1535
|
file.close()
|
1506
1536
|
if save is not None:
|
1507
|
-
file_name = save + "
|
1537
|
+
file_name = save + "_history_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
|
1508
1538
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1509
1539
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1510
1540
|
file.close()
|
@@ -1709,7 +1739,7 @@ get_history = get_raw
|
|
1709
1739
|
# d = day 'YYYY-MM-DD'
|
1710
1740
|
# v = list of report variables to get
|
1711
1741
|
# summary = 0, 1, 2: do a quick total energy report for a day
|
1712
|
-
# save = "xxxxx": save the report results to
|
1742
|
+
# save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
|
1713
1743
|
# load = "<file>": load the report results from <file>
|
1714
1744
|
# plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
|
1715
1745
|
# station = 0: use device_id, 1 = use station_id
|
@@ -1839,7 +1869,7 @@ def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=Non
|
|
1839
1869
|
result = json.load(file)
|
1840
1870
|
file.close()
|
1841
1871
|
elif save is not None:
|
1842
|
-
file_name = save + "
|
1872
|
+
file_name = save + "_report_" + report_type + "_" + d.replace('-','') + ".txt"
|
1843
1873
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1844
1874
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1845
1875
|
file.close()
|
@@ -2588,7 +2618,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2588
2618
|
elif type(strategy) is not list:
|
2589
2619
|
strategy = [strategy]
|
2590
2620
|
output(f"\nStrategy")
|
2591
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0
|
2621
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0) #, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2592
2622
|
output_close(plot=tariff_config['show_plot'])
|
2593
2623
|
if update == 1:
|
2594
2624
|
tariff = use
|
@@ -2665,15 +2695,15 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2665
2695
|
return profile[:run_time]
|
2666
2696
|
|
2667
2697
|
# build the timed work mode profile from the tariff strategy:
|
2668
|
-
def strategy_timed(timed_mode,
|
2698
|
+
def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, current_mode=None):
|
2669
2699
|
global tariff, steps_per_hour
|
2670
2700
|
work_mode_timed = []
|
2671
2701
|
min_soc_now = min_soc
|
2672
2702
|
max_soc_now = max_soc
|
2673
2703
|
current_mode = 'SelfUse' if current_mode is None else current_mode
|
2674
2704
|
strategy = get_strategy(timed_mode=timed_mode)
|
2675
|
-
h = base_hour
|
2676
2705
|
for i in range(0, run_time):
|
2706
|
+
h = time_line[i]
|
2677
2707
|
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0, 'charge': 0.0,
|
2678
2708
|
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2679
2709
|
if strategy is not None:
|
@@ -2692,8 +2722,8 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2692
2722
|
if d.get('fdpwr') is not None:
|
2693
2723
|
period['fdpwr'] = d['fdpwr']
|
2694
2724
|
period['duration'] = duration_in(h, d, steps_per_hour) * steps_per_hour
|
2725
|
+
break
|
2695
2726
|
work_mode_timed.append(period)
|
2696
|
-
h = round_time(h + 1 / steps_per_hour)
|
2697
2727
|
return work_mode_timed
|
2698
2728
|
|
2699
2729
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
@@ -2706,7 +2736,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2706
2736
|
discharge_loss = charge_config['discharge_loss']
|
2707
2737
|
charge_limit = charge_config['charge_limit']
|
2708
2738
|
float_charge = charge_config['float_charge']
|
2709
|
-
|
2739
|
+
run_time = len(work_mode_timed)
|
2740
|
+
for i in range(0, run_time):
|
2710
2741
|
w = work_mode_timed[i]
|
2711
2742
|
w['kwh'] = kwh_current
|
2712
2743
|
max_now = w['max_soc'] * capacity / 100
|
@@ -2717,6 +2748,7 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2717
2748
|
if kwh_current > capacity:
|
2718
2749
|
# battery is full
|
2719
2750
|
kwh_current = capacity
|
2751
|
+
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2720
2752
|
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2721
2753
|
reserve_now = capacity * min_soc_now / 100
|
2722
2754
|
if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
|
@@ -2746,7 +2778,7 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2746
2778
|
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2747
2779
|
next_period = work_mode_timed[t]
|
2748
2780
|
h = base_hour + t / steps_per_hour
|
2749
|
-
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
|
2781
|
+
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold'] or period['min_soc'] != next_period['min_soc']:
|
2750
2782
|
s = {'start': start % 24, 'end': h % 24, 'mode': period['mode'], 'min_soc': period['min_soc']}
|
2751
2783
|
if period['mode'] == 'ForceDischarge':
|
2752
2784
|
s['fdsoc'] = period.get('fdsoc')
|
@@ -2838,7 +2870,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2838
2870
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2839
2871
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2840
2872
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2841
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery,
|
2873
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
|
2842
2874
|
print(f"\n---------------- charge_needed ----------------")
|
2843
2875
|
# validate parameters
|
2844
2876
|
args = locals()
|
@@ -2945,8 +2977,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2945
2977
|
bat_power = 0.0
|
2946
2978
|
temperature = 30
|
2947
2979
|
bms_charge_current = 15
|
2948
|
-
charge_loss =
|
2949
|
-
discharge_loss =
|
2980
|
+
charge_loss = battery_params[2]['charge_loss']
|
2981
|
+
discharge_loss = battery_params[2]['discharge_loss']
|
2950
2982
|
bat_current = 0.0
|
2951
2983
|
device_power = 6.0
|
2952
2984
|
device_current = 35
|
@@ -2968,8 +3000,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2968
3000
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2969
3001
|
return None
|
2970
3002
|
bms_charge_current = battery.get('charge_rate')
|
2971
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else
|
2972
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else
|
3003
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3004
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2973
3005
|
device_power = device.get('power')
|
2974
3006
|
device_current = device.get('max_charge_current')
|
2975
3007
|
model = device.get('deviceType')
|
@@ -3141,7 +3173,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3141
3173
|
# produce time lines for charge, discharge and work mode
|
3142
3174
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3143
3175
|
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
3144
|
-
work_mode_timed = strategy_timed(timed_mode,
|
3176
|
+
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3145
3177
|
for i in range(0, len(work_mode_timed)):
|
3146
3178
|
# get work mode
|
3147
3179
|
work_mode = work_mode_timed[i]['mode']
|
@@ -3269,14 +3301,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3269
3301
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1, enable=update_settings)
|
3270
3302
|
if update_settings == 0:
|
3271
3303
|
output(f"\nNo changes made to charge settings")
|
3304
|
+
start_t = 0 # int(hour_now % 1 + 0.5) * steps_per_hour
|
3272
3305
|
if show_data > 0:
|
3273
3306
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3274
3307
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3275
3308
|
h = base_hour
|
3276
|
-
t =
|
3309
|
+
t = start_t
|
3277
3310
|
while t < len(time_line) and bat_timed[t] is not None:
|
3278
3311
|
col = h % data_wrap
|
3279
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3312
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3280
3313
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3281
3314
|
h += 1
|
3282
3315
|
t += steps_per_hour
|
@@ -3284,8 +3317,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3284
3317
|
if show_plot > 0:
|
3285
3318
|
print()
|
3286
3319
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3287
|
-
x_timed = [i for i in range(
|
3288
|
-
x_ticks = [i for i in range(
|
3320
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3321
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3289
3322
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3290
3323
|
if show_plot == 1:
|
3291
3324
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3395,14 +3428,15 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3395
3428
|
for v in plots.keys():
|
3396
3429
|
for i in range(0, run_time):
|
3397
3430
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3431
|
+
start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
|
3398
3432
|
if show_data > 0 and plots.get('SoC') is not None:
|
3399
3433
|
data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
|
3400
3434
|
s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
|
3401
3435
|
h = base_hour
|
3402
|
-
t =
|
3436
|
+
t = start_t
|
3403
3437
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3404
3438
|
col = h % data_wrap
|
3405
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3439
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3406
3440
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3407
3441
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3408
3442
|
h += 1
|
@@ -3411,8 +3445,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3411
3445
|
if show_plot > 0:
|
3412
3446
|
print()
|
3413
3447
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3414
|
-
x_timed = [i for i in range(
|
3415
|
-
x_ticks = [i for i in range(
|
3448
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3449
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3416
3450
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3417
3451
|
if show_plot == 1:
|
3418
3452
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3555,11 +3589,11 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3555
3589
|
s +=f",{v:.0f}"
|
3556
3590
|
return s
|
3557
3591
|
output(f"Current SoC: {current_soc}%")
|
3558
|
-
output(f"Capacity: {capacity:.2f}kWh" + (" (
|
3559
|
-
output(f"Residual: {residual:.2f}kWh" + (" (
|
3592
|
+
output(f"Capacity: {capacity:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [1,3] else ""))
|
3593
|
+
output(f"Residual: {residual:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [2,3] else ""))
|
3560
3594
|
if rated_capacity is not None and bat_soh is not None:
|
3561
3595
|
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3562
|
-
output(f"SoH: {bat_soh:.1f}%" + (" (
|
3596
|
+
output(f"SoH: {bat_soh:.1f}%" + (" (calculated)" if not bat['soh_supported'] else ""))
|
3563
3597
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
3564
3598
|
output(f"InvBatCurrent: {bat_current:.1f}A")
|
3565
3599
|
output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
|
@@ -3968,7 +4002,6 @@ class Solcast :
|
|
3968
4002
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3969
4003
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3970
4004
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3971
|
-
self.data = {}
|
3972
4005
|
now = convert_date(d)
|
3973
4006
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3974
4007
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
@@ -3976,6 +4009,8 @@ class Solcast :
|
|
3976
4009
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
3977
4010
|
self.yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3978
4011
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
4012
|
+
self.data = {}
|
4013
|
+
self.rids = []
|
3979
4014
|
if reload == 1 and os.path.exists(storage + self.save):
|
3980
4015
|
os.remove(storage + self.save)
|
3981
4016
|
if self.save is not None and os.path.exists(storage + self.save):
|
@@ -3984,33 +4019,37 @@ class Solcast :
|
|
3984
4019
|
file.close()
|
3985
4020
|
if len(self.data) == 0:
|
3986
4021
|
print(f"No data in {self.save}")
|
3987
|
-
|
3988
|
-
self.
|
3989
|
-
|
3990
|
-
|
3991
|
-
|
4022
|
+
else:
|
4023
|
+
self.rids = self.data['forecasts'].keys() if self.data.get('forecasts') is not None else []
|
4024
|
+
if reload == 2 and self.data.get('date') is not None and self.data['date'] != self.today:
|
4025
|
+
self.data = {}
|
4026
|
+
elif debug_setting > 0 and not quiet:
|
4027
|
+
print(f"Using data for {self.data['date']} from {self.save}")
|
4028
|
+
if len(self.data) == 0:
|
3992
4029
|
if solcast_api_key is None or solcast_api_key == 'my.solcast_api_key>':
|
3993
4030
|
print(f"\nSolcast: solcast_api_key not set, exiting")
|
3994
4031
|
return
|
3995
4032
|
self.credentials = HTTPBasicAuth(solcast_api_key, '')
|
3996
|
-
if
|
3997
|
-
|
3998
|
-
|
3999
|
-
|
4000
|
-
|
4001
|
-
if response.status_code
|
4002
|
-
|
4003
|
-
|
4004
|
-
|
4005
|
-
|
4006
|
-
|
4033
|
+
if len(self.rids) == 0:
|
4034
|
+
if debug_setting > 1 and not quiet:
|
4035
|
+
print(f"Getting rids from solcast.com")
|
4036
|
+
params = {'format' : 'json'}
|
4037
|
+
response = requests.get(solcast_url + 'rooftop_sites', auth = self.credentials, params = params)
|
4038
|
+
if response.status_code != 200:
|
4039
|
+
if response.status_code == 429:
|
4040
|
+
print(f"\nSolcast API call limit reached for today")
|
4041
|
+
else:
|
4042
|
+
print(f"Solcast: response code getting rooftop_sites was {response.status_code}: {response.reason}")
|
4043
|
+
return
|
4044
|
+
sites = response.json().get('sites')
|
4045
|
+
self.rids = [s['resource_id'] for s in sites]
|
4007
4046
|
if debug_setting > 0 and not quiet:
|
4008
4047
|
print(f"Getting forecast for {self.today} from solcast.com")
|
4009
4048
|
self.data['date'] = self.today
|
4010
4049
|
params = {'format' : 'json', 'hours' : 168, 'period' : 'PT30M'} # always get 168 x 30 min values
|
4011
4050
|
for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
|
4012
4051
|
self.data[t] = {}
|
4013
|
-
for rid in
|
4052
|
+
for rid in self.rids:
|
4014
4053
|
response = requests.get(solcast_url + 'rooftop_sites/' + rid + '/' + t, auth = self.credentials, params = params)
|
4015
4054
|
if response.status_code != 200 :
|
4016
4055
|
if response.status_code == 429:
|