foxesscloud 2.6.8__py3-none-any.whl → 2.6.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxesscloud/foxesscloud.py +149 -96
- foxesscloud/openapi.py +49 -32
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.6.9.dist-info}/METADATA +21 -6
- foxesscloud-2.6.9.dist-info/RECORD +7 -0
- foxesscloud-2.6.8.dist-info/RECORD +0 -7
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.6.9.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.6.9.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.6.9.dist-info}/top_level.txt +0 -0
foxesscloud/foxesscloud.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 05 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.
|
13
|
+
version = "1.8.0"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -275,7 +275,7 @@ def get_token():
|
|
275
275
|
info = None
|
276
276
|
|
277
277
|
def get_info():
|
278
|
-
global
|
278
|
+
global debug_setting, info, messages
|
279
279
|
if get_token() is None:
|
280
280
|
return None
|
281
281
|
output(f"getting access", 2)
|
@@ -307,7 +307,7 @@ def get_info():
|
|
307
307
|
status = None
|
308
308
|
|
309
309
|
def get_status(station=0):
|
310
|
-
global
|
310
|
+
global debug_setting, info, messages, status
|
311
311
|
if get_token() is None:
|
312
312
|
return None
|
313
313
|
output(f"getting status", 2)
|
@@ -335,7 +335,7 @@ site = None
|
|
335
335
|
station_id = None
|
336
336
|
|
337
337
|
def get_site(name=None):
|
338
|
-
global
|
338
|
+
global site_list, site, debug_setting, messages, station_id
|
339
339
|
if get_token() is None:
|
340
340
|
return None
|
341
341
|
if site is not None and name is None:
|
@@ -376,6 +376,7 @@ def get_site(name=None):
|
|
376
376
|
station_id = site['stationID']
|
377
377
|
return site
|
378
378
|
|
379
|
+
|
379
380
|
##################################################################################################
|
380
381
|
# get list of data loggers
|
381
382
|
##################################################################################################
|
@@ -384,7 +385,7 @@ logger_list = None
|
|
384
385
|
logger = None
|
385
386
|
|
386
387
|
def get_logger(sn=None):
|
387
|
-
global
|
388
|
+
global logger_list, logger, debug_setting, messages
|
388
389
|
if get_token() is None:
|
389
390
|
return None
|
390
391
|
if logger is not None and sn is None:
|
@@ -435,7 +436,7 @@ var_list = None
|
|
435
436
|
raw_vars = var_list
|
436
437
|
|
437
438
|
def get_device(sn=None):
|
438
|
-
global
|
439
|
+
global device_list, device, device_id, device_sn, firmware, battery, var_list, debug_setting, messages, flag, schedule, templates, remote_settings
|
439
440
|
if get_token() is None:
|
440
441
|
return None
|
441
442
|
if device is not None:
|
@@ -527,7 +528,7 @@ def get_device(sn=None):
|
|
527
528
|
##################################################################################################
|
528
529
|
|
529
530
|
def get_vars():
|
530
|
-
global
|
531
|
+
global device_id, debug_setting, messages
|
531
532
|
if get_device() is None:
|
532
533
|
return None
|
533
534
|
output(f"getting variables", 2)
|
@@ -555,7 +556,7 @@ def get_vars():
|
|
555
556
|
firmware = None
|
556
557
|
|
557
558
|
def get_firmware():
|
558
|
-
global
|
559
|
+
global device_id, firmware, debug_setting, messages
|
559
560
|
if get_device() is None:
|
560
561
|
return None
|
561
562
|
output(f"getting firmware", 2)
|
@@ -609,8 +610,8 @@ battery_params = {
|
|
609
610
|
'discharge_loss': 0.974},
|
610
611
|
}
|
611
612
|
|
612
|
-
def get_battery(info=1):
|
613
|
-
global
|
613
|
+
def get_battery(info=1, rated=None, count=None):
|
614
|
+
global device_id, battery, debug_setting, messages, residual_handling, battery_params
|
614
615
|
if get_device() is None:
|
615
616
|
return None
|
616
617
|
output(f"getting battery", 2)
|
@@ -655,24 +656,26 @@ def get_battery(info=1):
|
|
655
656
|
soc = battery.get('soc')
|
656
657
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
657
658
|
if battery.get('count') is None:
|
658
|
-
battery['count'] = int(battery['volt'] / 49)
|
659
|
+
battery['count'] = int(battery['volt'] / 49) if count is None else count
|
659
660
|
if battery.get('ratedCapacity') is None:
|
660
|
-
battery['ratedCapacity'] = 2560 * battery['count']
|
661
|
+
battery['ratedCapacity'] = 2560 * battery['count'] if rated is None else rated
|
661
662
|
elif battery['residual_handling'] == 3:
|
662
663
|
if battery.get('count') is None:
|
663
|
-
battery['count'] = int(battery['volt'] / 49)
|
664
|
+
battery['count'] = int(battery['volt'] / 49) if count is None else count
|
664
665
|
capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
|
665
666
|
soc = battery.get('soc')
|
666
667
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
667
668
|
if battery.get('ratedCapacity') is None:
|
668
|
-
battery['ratedCapacity'] = 2450 * battery['count']
|
669
|
+
battery['ratedCapacity'] = 2450 * battery['count'] if rated is None else rated
|
669
670
|
else:
|
670
671
|
residual = battery.get('residual')
|
671
672
|
soc = battery.get('soc')
|
672
|
-
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
673
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
674
|
+
if battery.get('ratedCapacity') is None and rated is not None:
|
675
|
+
battery['ratedCapacity'] = rated
|
673
676
|
battery['capacity'] = round(capacity, 3)
|
674
677
|
battery['residual'] = round(residual, 3)
|
675
|
-
battery['charge_rate'] =
|
678
|
+
battery['charge_rate'] = None
|
676
679
|
params = battery_params[battery['residual_handling']]
|
677
680
|
battery['charge_loss'] = params['charge_loss']
|
678
681
|
battery['discharge_loss'] = params['discharge_loss']
|
@@ -682,8 +685,8 @@ def get_battery(info=1):
|
|
682
685
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
683
686
|
return battery
|
684
687
|
|
685
|
-
def get_batteries(info=1):
|
686
|
-
global
|
688
|
+
def get_batteries(info=1, rated=None, count=None):
|
689
|
+
global device_id, battery, debug_setting, messages, batteries, battery_params, residual_handling
|
687
690
|
if get_device() is None:
|
688
691
|
return None
|
689
692
|
output(f"getting batteries", 2)
|
@@ -710,7 +713,15 @@ def get_batteries(info=1):
|
|
710
713
|
else:
|
711
714
|
for i in range(0, len(batteries)):
|
712
715
|
batteries[i]['info'] = result['batteries'][i]
|
713
|
-
|
716
|
+
if type(rated) is not list:
|
717
|
+
rated = [rated]
|
718
|
+
while len(rated) < len(batteries):
|
719
|
+
rated.append(None)
|
720
|
+
if type(count) is not list:
|
721
|
+
count = [count]
|
722
|
+
while len(count) < len(batteries):
|
723
|
+
count.append(None)
|
724
|
+
for i,b in enumerate(batteries):
|
714
725
|
b['residual_handling'] = residual_handling
|
715
726
|
if b.get('info') is not None:
|
716
727
|
if b['info'].get('slaveBatteries') is not None:
|
@@ -718,9 +729,11 @@ def get_batteries(info=1):
|
|
718
729
|
if b['info']['masterSN'][:7] == '60BBHV2' and b['info']['masterVersion'] >= '1.014':
|
719
730
|
b['residual_handling'] = 2
|
720
731
|
elif b['info']['masterSN'][:7] == '60MBB01' and b['info']['masterVersion'] >= '1.014':
|
721
|
-
residual_handling = 3
|
732
|
+
b['residual_handling'] = 3
|
733
|
+
if b.get('count') is None:
|
734
|
+
b['count'] = count[i]
|
722
735
|
rated_capacity = b.get('ratedCapacity')
|
723
|
-
b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else
|
736
|
+
b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else rated[i]
|
724
737
|
soh = b.get('soh')
|
725
738
|
b['soh'] = int(soh) if soh.isnumeric() and int(soh) > 10 else None
|
726
739
|
b['soh_supported'] = b['soh'] is not None
|
@@ -739,7 +752,7 @@ def get_batteries(info=1):
|
|
739
752
|
b['residual'] = round(residual, 3)
|
740
753
|
if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
|
741
754
|
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
|
742
|
-
b['charge_rate'] =
|
755
|
+
b['charge_rate'] = None
|
743
756
|
params = battery_params[b['residual_handling']]
|
744
757
|
b['charge_loss'] = params['charge_loss']
|
745
758
|
b['discharge_loss'] = params['discharge_loss']
|
@@ -752,7 +765,7 @@ def get_batteries(info=1):
|
|
752
765
|
##################################################################################################
|
753
766
|
|
754
767
|
def get_charge():
|
755
|
-
global
|
768
|
+
global device_sn, battery_settings, debug_setting, messages
|
756
769
|
if get_device() is None:
|
757
770
|
return None
|
758
771
|
if battery_settings is None:
|
@@ -789,7 +802,7 @@ def time_period(t):
|
|
789
802
|
return result
|
790
803
|
|
791
804
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force=0, enable=1):
|
792
|
-
global
|
805
|
+
global device_sn, battery_settings, debug_setting, messages, schedule
|
793
806
|
if get_device() is None:
|
794
807
|
return None
|
795
808
|
if battery_settings is None:
|
@@ -863,7 +876,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
863
876
|
##################################################################################################
|
864
877
|
|
865
878
|
def get_min():
|
866
|
-
global
|
879
|
+
global device_sn, battery_settings, debug_setting, messages
|
867
880
|
if get_device() is None:
|
868
881
|
return None
|
869
882
|
if battery_settings is None:
|
@@ -888,7 +901,7 @@ def get_min():
|
|
888
901
|
##################################################################################################
|
889
902
|
|
890
903
|
def set_min(minGridSoc = None, minSoc = None, force = 0):
|
891
|
-
global
|
904
|
+
global device_sn, battery_settings, debug_setting, messages
|
892
905
|
if get_device() is None:
|
893
906
|
return None
|
894
907
|
if get_schedule().get('enable'):
|
@@ -944,6 +957,13 @@ merge_settings = { # keys to add
|
|
944
957
|
# 'k106__': 'operation_mode__work_mode',
|
945
958
|
},
|
946
959
|
'values': ['SelfUse', 'Feedin', 'Backup']},
|
960
|
+
'ExportLimit': {'keys': {
|
961
|
+
'h115__': 'basic2__05',
|
962
|
+
'h116__': 'basic2__05',
|
963
|
+
'h117__': 'basic2__05',
|
964
|
+
# 'k106__': 'basic2__05',
|
965
|
+
},
|
966
|
+
'valueType': 'int'},
|
947
967
|
'BatteryVolt': {'keys': {
|
948
968
|
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
949
969
|
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
@@ -1003,7 +1023,7 @@ def get_ui():
|
|
1003
1023
|
block = p['block'] and len(p['properties']) > 1
|
1004
1024
|
for e in p['properties']:
|
1005
1025
|
valueType = e['elemType']['valueType']
|
1006
|
-
item = {'name': e['key'].replace(protocol,'')} if block else {'
|
1026
|
+
item = {'name': e['key'].replace(protocol,'')} if block else {'keys': e['key']} #, 'group': p['name']}
|
1007
1027
|
if e['elemType'].get('uiItems') is not None:
|
1008
1028
|
item['values'] = e['elemType']['uiItems']
|
1009
1029
|
elif e.get('range') is not None:
|
@@ -1018,7 +1038,7 @@ def get_ui():
|
|
1018
1038
|
else:
|
1019
1039
|
named_settings[e['name']] = item
|
1020
1040
|
if block:
|
1021
|
-
named_settings[p['name']] = {'
|
1041
|
+
named_settings[p['name']] = {'keys': p['key'], 'type': 'block', 'items': items}
|
1022
1042
|
for name in merge_settings.keys():
|
1023
1043
|
if named_settings.get(name) is None and merge_settings[name]['keys'].get(protocol) is not None:
|
1024
1044
|
named_settings[name] = {'keys': merge_settings[name]['keys'][protocol]}
|
@@ -1028,7 +1048,7 @@ def get_ui():
|
|
1028
1048
|
return remote_settings
|
1029
1049
|
|
1030
1050
|
def get_remote_settings(key):
|
1031
|
-
global
|
1051
|
+
global device_id, debug_setting, messages
|
1032
1052
|
if get_device() is None:
|
1033
1053
|
return None
|
1034
1054
|
output(f"getting remote settings", 2)
|
@@ -1090,10 +1110,66 @@ def get_named_settings(name):
|
|
1090
1110
|
return values
|
1091
1111
|
return result
|
1092
1112
|
|
1113
|
+
def set_named_settings(name, value, force=0):
|
1114
|
+
global named_settings
|
1115
|
+
if get_device() is None:
|
1116
|
+
return None
|
1117
|
+
if force == 1 and get_schedule().get('enable'):
|
1118
|
+
set_schedule(enable=0)
|
1119
|
+
if type(name) is list:
|
1120
|
+
result = []
|
1121
|
+
for (n, v) in name:
|
1122
|
+
result.append(set_named_settings(name=n, value=v))
|
1123
|
+
return result
|
1124
|
+
if named_settings is None or named_settings.get(name) is None:
|
1125
|
+
output(f"** set_named_settings(): {name} was not recognised")
|
1126
|
+
return None
|
1127
|
+
keys = named_settings[name].get('keys')
|
1128
|
+
if keys is None:
|
1129
|
+
output(f"** set_named_settings(): no keys for name: {name}")
|
1130
|
+
return None
|
1131
|
+
item_type = named_settings[name].get('type')
|
1132
|
+
if item_type is None:
|
1133
|
+
values = {keys: str(value)}
|
1134
|
+
elif item_type == 'block':
|
1135
|
+
items = named_setting[name]['items']
|
1136
|
+
n = len(items)
|
1137
|
+
if type(value) is not list or n != len(value):
|
1138
|
+
output(f"** set_named_settings(): {name} requires list of {n} values")
|
1139
|
+
return None
|
1140
|
+
values = {}
|
1141
|
+
for i in range(0, n):
|
1142
|
+
values[items[i]['name']] = str(value[i])
|
1143
|
+
elif item_type == 'list':
|
1144
|
+
if type(value) is not list:
|
1145
|
+
output(f"** set_named_settings(): {name} requires a list of values")
|
1146
|
+
return None
|
1147
|
+
values = {keys: value}
|
1148
|
+
else:
|
1149
|
+
values = {keys: str(value)}
|
1150
|
+
output(f"\nSetting {name} to {value}", 1)
|
1151
|
+
values['raw'] = ''
|
1152
|
+
data = {'id': device_id, 'key': keys, 'values': values}
|
1153
|
+
setting_delay()
|
1154
|
+
response = signed_post(path="/c/v0/device/setting/set", data=data)
|
1155
|
+
if response.status_code != 200:
|
1156
|
+
output(f"** set_named_settings() got response code {response.status_code}: {response.reason}")
|
1157
|
+
return None
|
1158
|
+
errno = response.json().get('errno')
|
1159
|
+
if errno != 0:
|
1160
|
+
if errno == 44096:
|
1161
|
+
output(f"** cannot update {name} when schedule is active")
|
1162
|
+
else:
|
1163
|
+
output(f"** set_named_settings(): ({name}, {value}) {errno_message(errno)}")
|
1164
|
+
return 0
|
1165
|
+
return 1
|
1166
|
+
|
1093
1167
|
##################################################################################################
|
1094
1168
|
# wrappers for named settings
|
1095
1169
|
##################################################################################################
|
1096
1170
|
|
1171
|
+
work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
|
1172
|
+
settable_modes = work_modes[:3]
|
1097
1173
|
work_mode = None
|
1098
1174
|
|
1099
1175
|
def get_work_mode():
|
@@ -1103,6 +1179,18 @@ def get_work_mode():
|
|
1103
1179
|
work_mode = get_named_settings('WorkMode')
|
1104
1180
|
return work_mode
|
1105
1181
|
|
1182
|
+
def set_work_mode(mode, force=0):
|
1183
|
+
global settable_modes, work_mode, debug_setting
|
1184
|
+
if get_device() is None:
|
1185
|
+
return None
|
1186
|
+
if mode not in settable_modes:
|
1187
|
+
output(f"** work mode: must be one of {settable_modes}")
|
1188
|
+
return None
|
1189
|
+
result = set_named_settings(name='WorkMode', value=mode, force=force)
|
1190
|
+
if result is not None and result == 1:
|
1191
|
+
work_mode = mode
|
1192
|
+
return result
|
1193
|
+
|
1106
1194
|
def get_cell_volts():
|
1107
1195
|
values = get_named_settings('BatteryVolt')
|
1108
1196
|
if values is None:
|
@@ -1130,46 +1218,6 @@ def get_cell_temps(nbat=8):
|
|
1130
1218
|
break
|
1131
1219
|
return bat_temps
|
1132
1220
|
|
1133
|
-
|
1134
|
-
##################################################################################################
|
1135
|
-
# set work mode
|
1136
|
-
##################################################################################################
|
1137
|
-
|
1138
|
-
work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
|
1139
|
-
settable_modes = work_modes[:3]
|
1140
|
-
|
1141
|
-
def set_work_mode(mode, force = 0):
|
1142
|
-
global token, device_id, work_modes, work_mode, debug_setting, messages, schedule
|
1143
|
-
if get_device() is None:
|
1144
|
-
return None
|
1145
|
-
if mode not in settable_modes:
|
1146
|
-
output(f"** work mode: must be one of {settable_modes}")
|
1147
|
-
return None
|
1148
|
-
if get_flag() is None:
|
1149
|
-
return None
|
1150
|
-
if schedule.get('enable') == True:
|
1151
|
-
if force == 0:
|
1152
|
-
output(f"** set_work_mode(): cannot set work mode when a schedule is enabled")
|
1153
|
-
return None
|
1154
|
-
set_schedule(enable=0)
|
1155
|
-
output(f"\nSetting work mode: {mode}", 1)
|
1156
|
-
data = {'id': device_id, 'key': 'operation_mode__work_mode', 'values': {'operation_mode__work_mode': mode}, 'raw': ''}
|
1157
|
-
setting_delay()
|
1158
|
-
response = signed_post(path="/c/v0/device/setting/set", data=data)
|
1159
|
-
if response.status_code != 200:
|
1160
|
-
output(f"** set_work_mode() got response code {response.status_code}: {response.reason}")
|
1161
|
-
return None
|
1162
|
-
errno = response.json().get('errno')
|
1163
|
-
if errno != 0:
|
1164
|
-
if errno == 44096:
|
1165
|
-
output(f"** cannot update settings when schedule is active")
|
1166
|
-
else:
|
1167
|
-
output(f"** set_work_mode(), {errno_message(errno)}")
|
1168
|
-
return None
|
1169
|
-
work_mode = mode
|
1170
|
-
return work_mode
|
1171
|
-
|
1172
|
-
|
1173
1221
|
##################################################################################################
|
1174
1222
|
# get schedule
|
1175
1223
|
##################################################################################################
|
@@ -1179,7 +1227,7 @@ templates = None
|
|
1179
1227
|
|
1180
1228
|
# get the current enable flag
|
1181
1229
|
def get_flag():
|
1182
|
-
global
|
1230
|
+
global device_id, device_sn, schedule, debug_setting, messages
|
1183
1231
|
if get_device() is None:
|
1184
1232
|
return None
|
1185
1233
|
output(f"getting flag", 2)
|
@@ -1216,7 +1264,7 @@ def get_flag():
|
|
1216
1264
|
|
1217
1265
|
# get the current schedule
|
1218
1266
|
def get_schedule():
|
1219
|
-
global
|
1267
|
+
global device_id, schedule, debug_setting, messages
|
1220
1268
|
if get_flag() is None:
|
1221
1269
|
return None
|
1222
1270
|
if schedule.get('support') == False:
|
@@ -1258,7 +1306,7 @@ def build_strategy_from_schedule():
|
|
1258
1306
|
|
1259
1307
|
# get the details for a specific template
|
1260
1308
|
def get_template_detail(template):
|
1261
|
-
global
|
1309
|
+
global device_id, schedule, debug_setting, messages, templates
|
1262
1310
|
if get_flag() is None:
|
1263
1311
|
return None
|
1264
1312
|
if schedule.get('support') == False:
|
@@ -1280,7 +1328,7 @@ def get_template_detail(template):
|
|
1280
1328
|
|
1281
1329
|
# get the preset templates that contains periods
|
1282
1330
|
def get_templates(template_type=[1,2]):
|
1283
|
-
global
|
1331
|
+
global device_id, flag, schedule, debug_setting, messages, templates
|
1284
1332
|
if get_flag() is None:
|
1285
1333
|
return None
|
1286
1334
|
if schedule.get('support') == False:
|
@@ -1393,7 +1441,7 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1393
1441
|
|
1394
1442
|
# set a schedule from a period or list of periods
|
1395
1443
|
def set_schedule(periods=None, template=None, enable=True):
|
1396
|
-
global
|
1444
|
+
global device_sn, debug_setting, messages, schedule, templates
|
1397
1445
|
if get_flag() is None:
|
1398
1446
|
return None
|
1399
1447
|
if schedule.get('support') == False:
|
@@ -1482,7 +1530,7 @@ sample_time = 5.0 # 5 minutes default
|
|
1482
1530
|
sample_rounding = 2 # round to 30 seconds
|
1483
1531
|
|
1484
1532
|
def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
|
1485
|
-
global
|
1533
|
+
global device_id, debug_setting, var_list, invert_ct2, tariff, max_power_kw, messages, sample_rounding, sample_time, storage
|
1486
1534
|
if station == 0 and get_device() is None:
|
1487
1535
|
return None
|
1488
1536
|
elif station == 1 and get_site() is None:
|
@@ -1754,7 +1802,7 @@ fix_value_threshold = 200000000.0
|
|
1754
1802
|
fix_value_mask = 0x0000FFFF
|
1755
1803
|
|
1756
1804
|
def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
|
1757
|
-
global
|
1805
|
+
global device_id, station_id, var_list, debug_setting, report_vars, messages, station_id
|
1758
1806
|
if station == 0 and get_device() is None:
|
1759
1807
|
return None
|
1760
1808
|
elif station == 1 and get_site() is None:
|
@@ -1990,7 +2038,7 @@ def plot_report(result, plot=1, station=0):
|
|
1990
2038
|
##################################################################################################
|
1991
2039
|
|
1992
2040
|
def get_earnings():
|
1993
|
-
global
|
2041
|
+
global device_id, station_id, var_list, debug_setting, messages
|
1994
2042
|
if get_device() is None:
|
1995
2043
|
return None
|
1996
2044
|
id_name = 'deviceID'
|
@@ -2259,7 +2307,7 @@ octopus_cosy = {
|
|
2259
2307
|
# time periods for Octopus Go
|
2260
2308
|
octopus_go = {
|
2261
2309
|
'name': 'Octopus Go',
|
2262
|
-
'off_peak1': {'start': 0.5, 'end':
|
2310
|
+
'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
|
2263
2311
|
'forecast_times': [21, 22]
|
2264
2312
|
}
|
2265
2313
|
|
@@ -2928,7 +2976,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2928
2976
|
if len(times) == 0:
|
2929
2977
|
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
|
2930
2978
|
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2931
|
-
|
2979
|
+
time_to_run = None
|
2932
2980
|
for t in times:
|
2933
2981
|
if hour_in(hour_now, t) and update_settings > 0:
|
2934
2982
|
update_settings = 0
|
@@ -2940,7 +2988,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2940
2988
|
t['time_to_start'] = time_to_start
|
2941
2989
|
t['time_to_end'] = time_to_end
|
2942
2990
|
t['charge_time'] = charge_time
|
2943
|
-
|
2991
|
+
if time_to_run is None:
|
2992
|
+
time_to_run = time_to_start
|
2944
2993
|
# get next charge slot
|
2945
2994
|
times = sorted(times, key=lambda t: t['time_to_start'])
|
2946
2995
|
charge_key = times[0]['key']
|
@@ -2952,7 +3001,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2952
3001
|
# work out time window and times with clock changes
|
2953
3002
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2954
3003
|
forecast_day = today if charge_today else tomorrow
|
2955
|
-
run_to =
|
3004
|
+
run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
|
2956
3005
|
run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
|
2957
3006
|
time_line = [round_time(base_hour + x / steps_per_hour - (hour_adjustment if x >= time_change else 0)) for x in range(0, run_time)]
|
2958
3007
|
bat_hold = times[0]['hold']
|
@@ -3007,12 +3056,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3007
3056
|
model = device.get('deviceType')
|
3008
3057
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
3009
3058
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
3059
|
+
reserve = capacity * min_soc / 100
|
3060
|
+
# charge current may be derated based on temperature
|
3061
|
+
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
3062
|
+
if bms_charge_current is not None and charge_current > bms_charge_current:
|
3063
|
+
charge_current = bms_charge_current
|
3010
3064
|
volt_curve = charge_config['volt_curve']
|
3011
3065
|
nominal_soc = charge_config['nominal_soc']
|
3012
3066
|
volt_nominal = interpolate(nominal_soc / 10, volt_curve)
|
3013
3067
|
bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
|
3014
3068
|
bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
|
3015
|
-
reserve = capacity * min_soc / 100
|
3016
3069
|
output(f"\nBattery Info:")
|
3017
3070
|
output(f" Capacity: {capacity:.2f}kWh")
|
3018
3071
|
output(f" Residual: {residual:.2f}kWh")
|
@@ -3022,15 +3075,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3022
3075
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
3023
3076
|
output(f" Current SoC: {current_soc}%")
|
3024
3077
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
3025
|
-
output(f" Max Charge: {
|
3078
|
+
output(f" Max Charge: {charge_current:.1f}A")
|
3026
3079
|
output(f" Temperature: {temperature:.1f}°C")
|
3027
3080
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
3028
3081
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
3029
3082
|
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
|
3030
|
-
# charge current may be derated based on temperature
|
3031
|
-
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
3032
|
-
if charge_current > bms_charge_current:
|
3033
|
-
charge_current = bms_charge_current
|
3034
3083
|
# inverter losses
|
3035
3084
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
3036
3085
|
operating_loss = inverter_power / 1000
|
@@ -3225,6 +3274,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3225
3274
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3226
3275
|
charge_message = "no charge needed"
|
3227
3276
|
kwh_needed = 0.0
|
3277
|
+
kwh_spare = kwh_min - reserve
|
3228
3278
|
hours = 0.0
|
3229
3279
|
start_timed = time_to_end
|
3230
3280
|
end_timed = time_to_end
|
@@ -3232,6 +3282,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3232
3282
|
else:
|
3233
3283
|
# work out time to add kwh_needed to battery
|
3234
3284
|
charge_rate = charge_power * charge_loss
|
3285
|
+
discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
|
3235
3286
|
hours = kwh_needed / charge_rate
|
3236
3287
|
if test_charge is None:
|
3237
3288
|
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
@@ -3242,13 +3293,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3242
3293
|
if hours > charge_time:
|
3243
3294
|
hours = charge_time
|
3244
3295
|
elif hours > hours_to_full:
|
3245
|
-
kwh_shortfall = (
|
3246
|
-
required = hours_to_full +
|
3296
|
+
kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
|
3297
|
+
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
3247
3298
|
hours = required if required > hours and required < charge_time else charge_time
|
3248
3299
|
# round charge time and work out what will actually be added
|
3249
3300
|
min_hours = charge_config['min_hours']
|
3250
3301
|
hours = int(hours / min_hours + 0.99) * min_hours
|
3251
3302
|
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3303
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3304
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3252
3305
|
# rework charge and discharge
|
3253
3306
|
charge_period = get_best_charge_period(start_at, hours)
|
3254
3307
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
@@ -3284,7 +3337,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3284
3337
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3285
3338
|
# show the results
|
3286
3339
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3287
|
-
output(f" Contingency: {
|
3340
|
+
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3288
3341
|
if not charge_today:
|
3289
3342
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3290
3343
|
# setup charging
|
@@ -3516,19 +3569,19 @@ def bat_count(cell_count):
|
|
3516
3569
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3517
3570
|
|
3518
3571
|
# show information about the current state of the batteries
|
3519
|
-
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3572
|
+
def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
|
3520
3573
|
global debug_setting, battery_info_app_key
|
3521
3574
|
if bat is None:
|
3522
|
-
bats = get_batteries(info=info)
|
3575
|
+
bats = get_batteries(info=info, rated=rated, count=count)
|
3523
3576
|
if bats is None:
|
3524
3577
|
return None
|
3525
3578
|
for i in range(0, len(bats)):
|
3526
3579
|
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3527
|
-
battery_info(log=log, plot=plot,
|
3580
|
+
battery_info(log=log, plot=plot, info=info, bat=bats[i])
|
3528
3581
|
return None
|
3529
3582
|
output_spool(battery_info_app_key)
|
3530
3583
|
nbat = None
|
3531
|
-
if
|
3584
|
+
if bat.get('info') is not None:
|
3532
3585
|
b = bat['info']
|
3533
3586
|
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3534
3587
|
nbat = 0
|
@@ -3551,7 +3604,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3551
3604
|
return None
|
3552
3605
|
nv = len(cell_volts)
|
3553
3606
|
if nbat is None:
|
3554
|
-
nbat = bat_count(nv) if count is None else count
|
3607
|
+
nbat = bat_count(nv) if bat.get('count') is None else bat['count']
|
3555
3608
|
if nbat is None:
|
3556
3609
|
output(f"** battery_info(): unable to match cells_per_battery for {nv}")
|
3557
3610
|
output_close()
|
foxesscloud/openapi.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 05 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.6.
|
13
|
+
version = "2.6.9"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -574,7 +574,7 @@ battery_params = {
|
|
574
574
|
'discharge_loss': 0.974},
|
575
575
|
}
|
576
576
|
|
577
|
-
def get_battery(info=0, v=None):
|
577
|
+
def get_battery(info=0, v=None, rated=None, count=None):
|
578
578
|
global device_sn, battery, debug_setting, residual_handling, battery_params
|
579
579
|
if get_device() is None:
|
580
580
|
return None
|
@@ -594,25 +594,29 @@ def get_battery(info=0, v=None):
|
|
594
594
|
soc = battery.get('soc')
|
595
595
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
596
596
|
if battery.get('count') is None:
|
597
|
-
battery['count'] = int(battery['volt'] / 49)
|
597
|
+
battery['count'] = int(battery['volt'] / 49) if count is None else count
|
598
598
|
if battery.get('ratedCapacity') is None:
|
599
|
-
battery['ratedCapacity'] = 2560 * battery['count']
|
599
|
+
battery['ratedCapacity'] = 2560 * battery['count'] if rated is None else rated
|
600
600
|
elif battery['residual_handling'] == 3:
|
601
601
|
if battery.get('count') is None:
|
602
|
-
battery['count'] = int(battery['volt'] / 49)
|
602
|
+
battery['count'] = int(battery['volt'] / 49) if count is None else count
|
603
603
|
capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
|
604
604
|
soc = battery.get('soc')
|
605
605
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
606
606
|
if battery.get('ratedCapacity') is None:
|
607
|
-
battery['ratedCapacity'] = 2450 * battery['count']
|
607
|
+
battery['ratedCapacity'] = 2450 * battery['count'] if rated is None else rated
|
608
608
|
else:
|
609
609
|
residual = battery.get('residual')
|
610
610
|
soc = battery.get('soc')
|
611
611
|
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
612
|
+
if battery.get('count') is None or battery['count'] < 1:
|
613
|
+
battery['count'] = count
|
614
|
+
if battery.get('ratedCapacity') is None or battery['ratedCapacity'] < 100:
|
615
|
+
battery['ratedCapacity'] = rated
|
612
616
|
battery['capacity'] = round(capacity, 3)
|
613
617
|
battery['residual'] = round(residual, 3)
|
614
618
|
battery['status'] = 1
|
615
|
-
battery['charge_rate'] =
|
619
|
+
battery['charge_rate'] = None
|
616
620
|
params = battery_params[battery['residual_handling']]
|
617
621
|
battery['charge_loss'] = params['charge_loss']
|
618
622
|
battery['discharge_loss'] = params['discharge_loss']
|
@@ -622,9 +626,13 @@ def get_battery(info=0, v=None):
|
|
622
626
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
623
627
|
return battery
|
624
628
|
|
625
|
-
def get_batteries(info=0):
|
629
|
+
def get_batteries(info=0, rated=None, count=None):
|
626
630
|
global battery, batteries
|
627
|
-
|
631
|
+
if type(rated) is not list:
|
632
|
+
rated = [rated]
|
633
|
+
if type(count) is not list:
|
634
|
+
count = [count]
|
635
|
+
get_battery(info=info, rated=rated[0], count=count[0])
|
628
636
|
if battery is None:
|
629
637
|
return None
|
630
638
|
batteries = [battery]
|
@@ -860,17 +868,17 @@ def get_remote_settings(name):
|
|
860
868
|
def get_named_settings(name):
|
861
869
|
return get_remote_settings(name)
|
862
870
|
|
863
|
-
def
|
871
|
+
def set_named_settings(name, value, force=0):
|
864
872
|
global device_sn, debug_setting
|
865
873
|
if get_device() is None:
|
866
874
|
return None
|
875
|
+
if force == 1 and get_schedule().get('enable'):
|
876
|
+
set_schedule(enable=0)
|
867
877
|
if type(name) is list:
|
868
|
-
|
878
|
+
result = []
|
869
879
|
for (n, v) in name:
|
870
|
-
result
|
871
|
-
|
872
|
-
count += 1
|
873
|
-
return count
|
880
|
+
result.append(set_named_settings(name=n, value=v))
|
881
|
+
return result
|
874
882
|
output(f"\nSetting {name} to {value}", 1)
|
875
883
|
body = {'sn': device_sn, 'key': name, 'value': f"{value}"}
|
876
884
|
setting_delay()
|
@@ -881,10 +889,10 @@ def set_named_setting(name, value):
|
|
881
889
|
errno = response.json().get('errno')
|
882
890
|
if errno != 0:
|
883
891
|
if errno == 44096:
|
884
|
-
output(f"** cannot update
|
892
|
+
output(f"** cannot update {name} when schedule is active")
|
885
893
|
else:
|
886
894
|
output(f"** set_named_settings(): ({name}, {value}) {errno_message(response)}")
|
887
|
-
return
|
895
|
+
return 0
|
888
896
|
return 1
|
889
897
|
|
890
898
|
##################################################################################################
|
@@ -1964,7 +1972,7 @@ octopus_cosy = {
|
|
1964
1972
|
# time periods for Octopus Go
|
1965
1973
|
octopus_go = {
|
1966
1974
|
'name': 'Octopus Go',
|
1967
|
-
'off_peak1': {'start': 0.5, 'end':
|
1975
|
+
'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
|
1968
1976
|
'forecast_times': [21, 22]
|
1969
1977
|
}
|
1970
1978
|
|
@@ -2632,7 +2640,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2632
2640
|
if len(times) == 0:
|
2633
2641
|
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
|
2634
2642
|
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2635
|
-
|
2643
|
+
time_to_run = None
|
2636
2644
|
for t in times:
|
2637
2645
|
if hour_in(hour_now, t) and update_settings > 0:
|
2638
2646
|
update_settings = 0
|
@@ -2644,7 +2652,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2644
2652
|
t['time_to_start'] = time_to_start
|
2645
2653
|
t['time_to_end'] = time_to_end
|
2646
2654
|
t['charge_time'] = charge_time
|
2647
|
-
|
2655
|
+
if time_to_run is None:
|
2656
|
+
time_to_run = time_to_start
|
2648
2657
|
# get next charge slot
|
2649
2658
|
times = sorted(times, key=lambda t: t['time_to_start'])
|
2650
2659
|
charge_key = times[0]['key']
|
@@ -2656,7 +2665,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2656
2665
|
# work out time window and times with clock changes
|
2657
2666
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2658
2667
|
forecast_day = today if charge_today else tomorrow
|
2659
|
-
run_to =
|
2668
|
+
run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
|
2660
2669
|
run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
|
2661
2670
|
time_line = [round_time(base_hour + x / steps_per_hour - (hour_adjustment if x >= time_change else 0)) for x in range(0, run_time)]
|
2662
2671
|
bat_hold = times[0]['hold']
|
@@ -2711,12 +2720,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2711
2720
|
model = device.get('deviceType')
|
2712
2721
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2713
2722
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2723
|
+
reserve = capacity * min_soc / 100
|
2724
|
+
# charge current may be derated based on temperature
|
2725
|
+
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2726
|
+
if bms_charge_current is not None and charge_current > bms_charge_current:
|
2727
|
+
charge_current = bms_charge_current
|
2714
2728
|
volt_curve = charge_config['volt_curve']
|
2715
2729
|
nominal_soc = charge_config['nominal_soc']
|
2716
2730
|
volt_nominal = interpolate(nominal_soc / 10, volt_curve)
|
2717
2731
|
bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
|
2718
2732
|
bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
|
2719
|
-
reserve = capacity * min_soc / 100
|
2720
2733
|
output(f"\nBattery Info:")
|
2721
2734
|
output(f" Capacity: {capacity:.2f}kWh")
|
2722
2735
|
output(f" Residual: {residual:.2f}kWh")
|
@@ -2727,7 +2740,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2727
2740
|
output(f" Current SoC: {current_soc}%")
|
2728
2741
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2729
2742
|
output(f" Temperature: {temperature:.1f}°C")
|
2730
|
-
output(f" Max Charge: {
|
2743
|
+
output(f" Max Charge: {charge_current:.1f}A")
|
2731
2744
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2732
2745
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
2733
2746
|
# charge current may be derated based on temperature
|
@@ -2927,6 +2940,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2927
2940
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2928
2941
|
charge_message = "no charge needed"
|
2929
2942
|
kwh_needed = 0.0
|
2943
|
+
kwh_spare = kwh_min - reserve
|
2930
2944
|
hours = 0.0
|
2931
2945
|
start_timed = time_to_end
|
2932
2946
|
end_timed = time_to_end
|
@@ -2934,6 +2948,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2934
2948
|
else:
|
2935
2949
|
# work out time to add kwh_needed to battery
|
2936
2950
|
charge_rate = charge_power * charge_loss
|
2951
|
+
discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
|
2937
2952
|
hours = kwh_needed / charge_rate
|
2938
2953
|
if test_charge is None:
|
2939
2954
|
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
@@ -2944,13 +2959,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2944
2959
|
if hours > charge_time:
|
2945
2960
|
hours = charge_time
|
2946
2961
|
elif hours > hours_to_full:
|
2947
|
-
kwh_shortfall = (
|
2948
|
-
required = hours_to_full +
|
2962
|
+
kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
|
2963
|
+
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
2949
2964
|
hours = required if required > hours and required < charge_time else charge_time
|
2950
2965
|
# round charge time and work out what will actually be added
|
2951
2966
|
min_hours = charge_config['min_hours']
|
2952
2967
|
hours = int(hours / min_hours + 0.99) * min_hours
|
2953
2968
|
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
2969
|
+
kwh_added += discharge_rate * hours # discharge saved during charging
|
2970
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
2954
2971
|
# rework charge and discharge
|
2955
2972
|
charge_period = get_best_charge_period(start_at, hours)
|
2956
2973
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
@@ -2986,7 +3003,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2986
3003
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
2987
3004
|
# show the results
|
2988
3005
|
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
2989
|
-
output(f" Contingency: {
|
3006
|
+
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
2990
3007
|
if not charge_today:
|
2991
3008
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
2992
3009
|
# setup charging
|
@@ -3215,19 +3232,19 @@ def bat_count(cell_count):
|
|
3215
3232
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3216
3233
|
|
3217
3234
|
# show information about the current state of the batteries
|
3218
|
-
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3235
|
+
def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
|
3219
3236
|
global debug_setting, battery_info_app_key
|
3220
3237
|
if bat is None:
|
3221
|
-
bats = get_batteries(info=info)
|
3238
|
+
bats = get_batteries(info=info, rated=rated, count=count)
|
3222
3239
|
if bats is None:
|
3223
3240
|
return None
|
3224
3241
|
for i in range(0, len(bats)):
|
3225
3242
|
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3226
|
-
battery_info(log=log, plot=plot,
|
3243
|
+
battery_info(log=log, plot=plot, info=info, bat=bats[i])
|
3227
3244
|
return None
|
3228
3245
|
output_spool(battery_info_app_key)
|
3229
3246
|
nbat = None
|
3230
|
-
if
|
3247
|
+
if bat.get('info') is not None:
|
3231
3248
|
b = bat['info']
|
3232
3249
|
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3233
3250
|
nbat = 0
|
@@ -3250,7 +3267,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3250
3267
|
return None
|
3251
3268
|
nv = len(cell_volts)
|
3252
3269
|
if nbat is None:
|
3253
|
-
nbat = bat_count(nv) if count is None else count
|
3270
|
+
nbat = bat_count(nv) if bat.get('count') is None else bat['count']
|
3254
3271
|
if nbat is None:
|
3255
3272
|
output(f"** battery_info(): unable to match cells_per_battery for {nv}")
|
3256
3273
|
output_close()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.9
|
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
|
@@ -104,8 +104,8 @@ Once an inverter is selected, you can make other calls to get information:
|
|
104
104
|
|
105
105
|
```
|
106
106
|
f.get_generation()
|
107
|
-
f.get_battery()
|
108
|
-
f.get_batteries()
|
107
|
+
f.get_battery(info, rated, count)
|
108
|
+
f.get_batteries(info, rated, count)
|
109
109
|
f.get_settings()
|
110
110
|
f.get_charge()
|
111
111
|
f.get_min()
|
@@ -119,7 +119,12 @@ Each of these calls will return a dictionary or list containing the relevant inf
|
|
119
119
|
get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
|
120
120
|
|
121
121
|
get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery / f.batteries.
|
122
|
-
get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery.
|
122
|
+
get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Parameters:
|
123
|
+
+ 'info': get battery serial number info, if available. Default 0 (not available via Open API)
|
124
|
+
+ 'rated': optional rated capacity for the battery in Wh to work out SoH. If not provided, it will try to work this out.
|
125
|
+
+ 'count': optional battery count. If not provided, it will try to work this out.
|
126
|
+
|
127
|
+
Additional battery attributes provided include:
|
123
128
|
+ 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
|
124
129
|
+ 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
|
125
130
|
+ 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
|
@@ -145,7 +150,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
|
|
145
150
|
f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
|
146
151
|
f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
|
147
152
|
f.set_schedule(periods, enable)
|
148
|
-
f.set_named_settings(name, value)
|
153
|
+
f.set_named_settings(name, value, force)
|
149
154
|
```
|
150
155
|
|
151
156
|
set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
|
@@ -188,7 +193,9 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
|
|
188
193
|
|
189
194
|
set_named_settings() sets the 'name' setting to 'value'.
|
190
195
|
+ 'name' may also be a list of (name, value) pairs.
|
191
|
-
+
|
196
|
+
+ 'force': setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
|
197
|
+
+ A return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
|
198
|
+
+ the only 'name' currently supported is 'ExportLimit'
|
192
199
|
|
193
200
|
|
194
201
|
## Real Time Data
|
@@ -797,6 +804,14 @@ This setting can be:
|
|
797
804
|
|
798
805
|
# Version Info
|
799
806
|
|
807
|
+
2.6.9<br>
|
808
|
+
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
809
|
+
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
810
|
+
Updates to get_battery() / get_batteries() to add optional rated and count parameters.
|
811
|
+
Updates to charge_needed() to end prediction at start of next charge period.
|
812
|
+
Correct charge time for Octopus Go tariff.
|
813
|
+
Update charge_needed() to show contingency achieved rather than requested.
|
814
|
+
|
800
815
|
2.6.8<br>
|
801
816
|
Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
|
802
817
|
Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=2rBhstr0ZH9PxvCipZhxf79JN7xQ_CqQc_xRyy-0Yd4,221818
|
2
|
+
foxesscloud/openapi.py,sha256=NncnwOSohoTTVWLELZWfkXnD5Peav6gl8aq2tRGsizk,205719
|
3
|
+
foxesscloud-2.6.9.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.6.9.dist-info/METADATA,sha256=NfNht53QGiO5BHMY-SMM6zo49qPS1HQHE4yKo3O_mLI,60502
|
5
|
+
foxesscloud-2.6.9.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.6.9.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.6.9.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=o4SAnr0y0dNtIXxY4BOxyoJE7g-tg3Tyt8EJupzGi6c,219881
|
2
|
-
foxesscloud/openapi.py,sha256=XFtZGaWr11_CV5kawpz9aTQxATMmEdTXzMVDGUiTc3Y,204609
|
3
|
-
foxesscloud-2.6.8.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.6.8.dist-info/METADATA,sha256=Inu6jh79s-Af8lcyBg06QWizy9rUY_5UWRNOBNkwO4A,59461
|
5
|
-
foxesscloud-2.6.8.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.6.8.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.6.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|