foxesscloud 2.6.8__py3-none-any.whl → 2.7.0__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 +195 -134
- foxesscloud/openapi.py +95 -69
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.7.0.dist-info}/METADATA +30 -7
- foxesscloud-2.7.0.dist-info/RECORD +7 -0
- foxesscloud-2.6.8.dist-info/RECORD +0 -7
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.7.0.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.7.0.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.8.dist-info → foxesscloud-2.7.0.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: 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.
|
13
|
+
version = "1.8.1"
|
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)
|
@@ -599,7 +600,7 @@ battery_params = {
|
|
599
600
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
600
601
|
'step': 5,
|
601
602
|
'offset': 5,
|
602
|
-
'charge_loss': 1.
|
603
|
+
'charge_loss': 1.07,
|
603
604
|
'discharge_loss': 0.95},
|
604
605
|
# Mira BMS with firmware 1.014 or later
|
605
606
|
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
@@ -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:
|
@@ -785,11 +798,11 @@ def get_charge():
|
|
785
798
|
def time_period(t):
|
786
799
|
result = f"{t['startTime']['hour']:02d}:{t['startTime']['minute']:02d}-{t['endTime']['hour']:02d}:{t['endTime']['minute']:02d}"
|
787
800
|
if t['startTime']['hour'] != t['endTime']['hour'] or t['startTime']['minute'] != t['endTime']['minute']:
|
788
|
-
result += f" Charge from grid" if t['enableGrid'] else f"
|
801
|
+
result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
|
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:
|
@@ -1700,9 +1748,7 @@ def plot_raw(result, plot=1, station=0):
|
|
1700
1748
|
def report_value_profile(result):
|
1701
1749
|
if type(result) is not list or result[0]['type'] != 'day':
|
1702
1750
|
return (None, None)
|
1703
|
-
data = []
|
1704
|
-
for h in range(0,24):
|
1705
|
-
data.append((0.0, 0)) # value sum, count of values
|
1751
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1706
1752
|
totals = 0
|
1707
1753
|
n = 0
|
1708
1754
|
for day in result:
|
@@ -1732,6 +1778,30 @@ def report_value_profile(result):
|
|
1732
1778
|
# forwards compatibility
|
1733
1779
|
get_history = get_raw
|
1734
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
|
+
|
1735
1805
|
##################################################################################################
|
1736
1806
|
# get energy report data in kWh
|
1737
1807
|
##################################################################################################
|
@@ -1754,7 +1824,7 @@ fix_value_threshold = 200000000.0
|
|
1754
1824
|
fix_value_mask = 0x0000FFFF
|
1755
1825
|
|
1756
1826
|
def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
|
1757
|
-
global
|
1827
|
+
global device_id, station_id, var_list, debug_setting, report_vars, messages, station_id
|
1758
1828
|
if station == 0 and get_device() is None:
|
1759
1829
|
return None
|
1760
1830
|
elif station == 1 and get_site() is None:
|
@@ -1990,7 +2060,7 @@ def plot_report(result, plot=1, station=0):
|
|
1990
2060
|
##################################################################################################
|
1991
2061
|
|
1992
2062
|
def get_earnings():
|
1993
|
-
global
|
2063
|
+
global device_id, station_id, var_list, debug_setting, messages
|
1994
2064
|
if get_device() is None:
|
1995
2065
|
return None
|
1996
2066
|
id_name = 'deviceID'
|
@@ -2259,7 +2329,7 @@ octopus_cosy = {
|
|
2259
2329
|
# time periods for Octopus Go
|
2260
2330
|
octopus_go = {
|
2261
2331
|
'name': 'Octopus Go',
|
2262
|
-
'off_peak1': {'start': 0.5, 'end':
|
2332
|
+
'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
|
2263
2333
|
'forecast_times': [21, 22]
|
2264
2334
|
}
|
2265
2335
|
|
@@ -2732,8 +2802,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2732
2802
|
global charge_config, steps_per_hour
|
2733
2803
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2734
2804
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2735
|
-
charge_loss = charge_config['
|
2736
|
-
discharge_loss = charge_config['
|
2805
|
+
charge_loss = charge_config['_charge_loss']
|
2806
|
+
discharge_loss = charge_config['_discharge_loss']
|
2737
2807
|
charge_limit = charge_config['charge_limit']
|
2738
2808
|
float_charge = charge_config['float_charge']
|
2739
2809
|
run_time = len(work_mode_timed)
|
@@ -2826,6 +2896,8 @@ charge_config = {
|
|
2826
2896
|
'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
|
2827
2897
|
'pv_loss': 0.950, # loss converting PV power to DC battery charge power
|
2828
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
|
2829
2901
|
'inverter_power': 101, # Inverter power consumption in W
|
2830
2902
|
'bms_power': 50, # BMS power consumption in W
|
2831
2903
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2928,7 +3000,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2928
3000
|
if len(times) == 0:
|
2929
3001
|
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
|
2930
3002
|
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2931
|
-
|
3003
|
+
time_to_run = None
|
2932
3004
|
for t in times:
|
2933
3005
|
if hour_in(hour_now, t) and update_settings > 0:
|
2934
3006
|
update_settings = 0
|
@@ -2940,7 +3012,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2940
3012
|
t['time_to_start'] = time_to_start
|
2941
3013
|
t['time_to_end'] = time_to_end
|
2942
3014
|
t['charge_time'] = charge_time
|
2943
|
-
|
3015
|
+
if time_to_run is None:
|
3016
|
+
time_to_run = time_to_start
|
2944
3017
|
# get next charge slot
|
2945
3018
|
times = sorted(times, key=lambda t: t['time_to_start'])
|
2946
3019
|
charge_key = times[0]['key']
|
@@ -2952,7 +3025,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2952
3025
|
# work out time window and times with clock changes
|
2953
3026
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2954
3027
|
forecast_day = today if charge_today else tomorrow
|
2955
|
-
run_to =
|
3028
|
+
run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
|
2956
3029
|
run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
|
2957
3030
|
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
3031
|
bat_hold = times[0]['hold']
|
@@ -2977,8 +3050,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2977
3050
|
bat_power = 0.0
|
2978
3051
|
temperature = 30
|
2979
3052
|
bms_charge_current = 15
|
2980
|
-
charge_loss = battery_params[2]['charge_loss']
|
2981
|
-
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']
|
2982
3055
|
bat_current = 0.0
|
2983
3056
|
device_power = 6.0
|
2984
3057
|
device_current = 35
|
@@ -3000,19 +3073,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3000
3073
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
3001
3074
|
return None
|
3002
3075
|
bms_charge_current = battery.get('charge_rate')
|
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
|
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
|
3005
3078
|
device_power = device.get('power')
|
3006
3079
|
device_current = device.get('max_charge_current')
|
3007
3080
|
model = device.get('deviceType')
|
3008
3081
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
3009
3082
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
3083
|
+
reserve = capacity * min_soc / 100
|
3084
|
+
# charge current may be derated based on temperature
|
3085
|
+
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
3086
|
+
if bms_charge_current is not None and charge_current > bms_charge_current:
|
3087
|
+
charge_current = bms_charge_current
|
3010
3088
|
volt_curve = charge_config['volt_curve']
|
3011
3089
|
nominal_soc = charge_config['nominal_soc']
|
3012
3090
|
volt_nominal = interpolate(nominal_soc / 10, volt_curve)
|
3013
3091
|
bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
|
3014
3092
|
bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
|
3015
|
-
reserve = capacity * min_soc / 100
|
3016
3093
|
output(f"\nBattery Info:")
|
3017
3094
|
output(f" Capacity: {capacity:.2f}kWh")
|
3018
3095
|
output(f" Residual: {residual:.2f}kWh")
|
@@ -3022,15 +3099,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3022
3099
|
output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
|
3023
3100
|
output(f" Current SoC: {current_soc}%")
|
3024
3101
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
3025
|
-
output(f" Max Charge: {
|
3102
|
+
output(f" Max Charge: {charge_current:.1f}A")
|
3026
3103
|
output(f" Temperature: {temperature:.1f}°C")
|
3027
3104
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
3028
3105
|
output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
|
3029
|
-
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
|
3106
|
+
output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
|
3034
3107
|
# inverter losses
|
3035
3108
|
inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
|
3036
3109
|
operating_loss = inverter_power / 1000
|
@@ -3059,8 +3132,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3059
3132
|
charge_config['charge_limit'] = charge_limit
|
3060
3133
|
charge_config['charge_power'] = charge_power
|
3061
3134
|
charge_config['float_charge'] = float_charge
|
3062
|
-
charge_config['
|
3063
|
-
charge_config['
|
3135
|
+
charge_config['_charge_loss'] = charge_loss
|
3136
|
+
charge_config['_discharge_loss'] = discharge_loss
|
3064
3137
|
# display what we have
|
3065
3138
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
3066
3139
|
output(f"\nDevice Info:")
|
@@ -3225,6 +3298,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3225
3298
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3226
3299
|
charge_message = "no charge needed"
|
3227
3300
|
kwh_needed = 0.0
|
3301
|
+
kwh_spare = kwh_min - reserve
|
3228
3302
|
hours = 0.0
|
3229
3303
|
start_timed = time_to_end
|
3230
3304
|
end_timed = time_to_end
|
@@ -3232,6 +3306,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3232
3306
|
else:
|
3233
3307
|
# work out time to add kwh_needed to battery
|
3234
3308
|
charge_rate = charge_power * charge_loss
|
3309
|
+
discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
|
3235
3310
|
hours = kwh_needed / charge_rate
|
3236
3311
|
if test_charge is None:
|
3237
3312
|
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
@@ -3242,13 +3317,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3242
3317
|
if hours > charge_time:
|
3243
3318
|
hours = charge_time
|
3244
3319
|
elif hours > hours_to_full:
|
3245
|
-
kwh_shortfall = (
|
3246
|
-
required = hours_to_full +
|
3320
|
+
kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
|
3321
|
+
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
3247
3322
|
hours = required if required > hours and required < charge_time else charge_time
|
3248
3323
|
# round charge time and work out what will actually be added
|
3249
3324
|
min_hours = charge_config['min_hours']
|
3250
3325
|
hours = int(hours / min_hours + 0.99) * min_hours
|
3251
3326
|
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
3327
|
+
kwh_added += discharge_rate * hours # discharge saved by charging
|
3328
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
3252
3329
|
# rework charge and discharge
|
3253
3330
|
charge_period = get_best_charge_period(start_at, hours)
|
3254
3331
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
@@ -3284,7 +3361,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3284
3361
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3285
3362
|
# show the results
|
3286
3363
|
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: {
|
3364
|
+
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
3288
3365
|
if not charge_today:
|
3289
3366
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3290
3367
|
# setup charging
|
@@ -3516,19 +3593,19 @@ def bat_count(cell_count):
|
|
3516
3593
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3517
3594
|
|
3518
3595
|
# show information about the current state of the batteries
|
3519
|
-
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3596
|
+
def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
|
3520
3597
|
global debug_setting, battery_info_app_key
|
3521
3598
|
if bat is None:
|
3522
|
-
bats = get_batteries(info=info)
|
3599
|
+
bats = get_batteries(info=info, rated=rated, count=count)
|
3523
3600
|
if bats is None:
|
3524
3601
|
return None
|
3525
3602
|
for i in range(0, len(bats)):
|
3526
3603
|
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3527
|
-
battery_info(log=log, plot=plot,
|
3604
|
+
battery_info(log=log, plot=plot, info=info, bat=bats[i])
|
3528
3605
|
return None
|
3529
3606
|
output_spool(battery_info_app_key)
|
3530
3607
|
nbat = None
|
3531
|
-
if
|
3608
|
+
if bat.get('info') is not None:
|
3532
3609
|
b = bat['info']
|
3533
3610
|
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3534
3611
|
nbat = 0
|
@@ -3551,7 +3628,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3551
3628
|
return None
|
3552
3629
|
nv = len(cell_volts)
|
3553
3630
|
if nbat is None:
|
3554
|
-
nbat = bat_count(nv) if count is None else count
|
3631
|
+
nbat = bat_count(nv) if bat.get('count') is None else bat['count']
|
3555
3632
|
if nbat is None:
|
3556
3633
|
output(f"** battery_info(): unable to match cells_per_battery for {nv}")
|
3557
3634
|
output_close()
|
@@ -4251,17 +4328,9 @@ class Solcast :
|
|
4251
4328
|
total_actual = None
|
4252
4329
|
self.actual = get_history('day', d=day, v=v)
|
4253
4330
|
plots = {}
|
4331
|
+
times = [i/2 for i in range(0, 48)]
|
4254
4332
|
for v in self.actual:
|
4255
|
-
|
4256
|
-
actual_values = []
|
4257
|
-
average = 0.0
|
4258
|
-
for i in range(0, len(v.get('data'))):
|
4259
|
-
average += v['data'][i]['value'] / 6
|
4260
|
-
if i % 6 == 5:
|
4261
|
-
times.append(round_time((i - 5) / 12))
|
4262
|
-
actual_values.append(average)
|
4263
|
-
average = 0
|
4264
|
-
plots[v['variable']] = actual_values
|
4333
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4265
4334
|
if v['variable'] == 'pvPower':
|
4266
4335
|
total_actual = v.get('kwh')
|
4267
4336
|
if total_actual is None:
|
@@ -4293,10 +4362,10 @@ class Solcast :
|
|
4293
4362
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
4294
4363
|
total_forecast = sum(forecast_values) / 2
|
4295
4364
|
plots['forecast'] = forecast_values
|
4296
|
-
if total_actual is not None:
|
4297
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4298
4365
|
if total_forecast is not None:
|
4299
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")
|
4300
4369
|
print()
|
4301
4370
|
title = f"Forecast / Actual PV Power on {day}"
|
4302
4371
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4584,17 +4653,9 @@ class Solar :
|
|
4584
4653
|
total_actual = None
|
4585
4654
|
self.actual = get_history('day', d=day, v=v)
|
4586
4655
|
plots = {}
|
4656
|
+
times = [i/2 for i in range(0, 48)]
|
4587
4657
|
for v in self.actual:
|
4588
|
-
|
4589
|
-
actual_values = []
|
4590
|
-
average = 0.0
|
4591
|
-
for i in range(0, len(v.get('data'))):
|
4592
|
-
average += v['data'][i]['value'] / 6
|
4593
|
-
if i % 6 == 5:
|
4594
|
-
times.append(round_time((i - 5) / 12))
|
4595
|
-
actual_values.append(average)
|
4596
|
-
average = 0
|
4597
|
-
plots[v['variable']] = actual_values
|
4658
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4598
4659
|
if v['variable'] == 'pvPower':
|
4599
4660
|
total_actual = v.get('kwh')
|
4600
4661
|
if total_actual is None:
|
@@ -4624,10 +4685,10 @@ class Solar :
|
|
4624
4685
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4625
4686
|
total_forecast = sum(forecast_values) / 2
|
4626
4687
|
plots['forecast'] = forecast_values
|
4627
|
-
if total_actual is not None:
|
4628
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4629
4688
|
if total_forecast is not None:
|
4630
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")
|
4631
4692
|
print()
|
4632
4693
|
title = f"Forecast / Actual PV Power on {day}"
|
4633
4694
|
plt.figure(figsize=(figure_width, figure_width/3))
|
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: 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],
|
@@ -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]
|
@@ -663,7 +671,7 @@ def time_period(t, n):
|
|
663
671
|
(enable, start, end) = (t['enable1'], t['startTime1'], t['endTime1']) if n == 1 else (t['enable2'], t['startTime2'], t['endTime2'])
|
664
672
|
result = f"{start['hour']:02d}:{start['minute']:02d}-{end['hour']:02d}:{end['minute']:02d}"
|
665
673
|
if start['hour'] != end['hour'] or start['minute'] != end['minute']:
|
666
|
-
result += f" Charge from grid" if enable else f"
|
674
|
+
result += f" Charge from grid" if enable else f" Battery Hold"
|
667
675
|
return result
|
668
676
|
|
669
677
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
|
@@ -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
|
##################################################################################################
|
@@ -1440,9 +1448,7 @@ get_raw = get_history
|
|
1440
1448
|
def report_value_profile(result):
|
1441
1449
|
if type(result) is not list or result[0]['type'] != 'day':
|
1442
1450
|
return (None, None)
|
1443
|
-
data = []
|
1444
|
-
for h in range(0,24):
|
1445
|
-
data.append((0.0, 0)) # value sum, count of values
|
1451
|
+
data = [(0.0, 0) for h in range(0,24)]
|
1446
1452
|
totals = 0
|
1447
1453
|
n = 0
|
1448
1454
|
for day in result:
|
@@ -1469,6 +1475,30 @@ def report_value_profile(result):
|
|
1469
1475
|
result.append(by_hour[t] * daily_average / current_total)
|
1470
1476
|
return (daily_average, result)
|
1471
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
|
+
|
1472
1502
|
|
1473
1503
|
##################################################################################################
|
1474
1504
|
# get production report in kWh
|
@@ -1964,7 +1994,7 @@ octopus_cosy = {
|
|
1964
1994
|
# time periods for Octopus Go
|
1965
1995
|
octopus_go = {
|
1966
1996
|
'name': 'Octopus Go',
|
1967
|
-
'off_peak1': {'start': 0.5, 'end':
|
1997
|
+
'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
|
1968
1998
|
'forecast_times': [21, 22]
|
1969
1999
|
}
|
1970
2000
|
|
@@ -2436,8 +2466,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2436
2466
|
global charge_config, steps_per_hour
|
2437
2467
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
2438
2468
|
bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
|
2439
|
-
charge_loss = charge_config['
|
2440
|
-
discharge_loss = charge_config['
|
2469
|
+
charge_loss = charge_config['_charge_loss']
|
2470
|
+
discharge_loss = charge_config['_discharge_loss']
|
2441
2471
|
charge_limit = charge_config['charge_limit']
|
2442
2472
|
float_charge = charge_config['float_charge']
|
2443
2473
|
run_time = len(work_mode_timed)
|
@@ -2530,6 +2560,8 @@ charge_config = {
|
|
2530
2560
|
'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
|
2531
2561
|
'pv_loss': 0.95, # loss converting PV power to DC battery charge power
|
2532
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
|
2533
2565
|
'inverter_power': 101, # Inverter power consumption in W
|
2534
2566
|
'bms_power': 50, # BMS power consumption in W
|
2535
2567
|
'force_charge_power': 5.00, # charge power in kW when using force charge
|
@@ -2632,7 +2664,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2632
2664
|
if len(times) == 0:
|
2633
2665
|
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
|
2634
2666
|
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2635
|
-
|
2667
|
+
time_to_run = None
|
2636
2668
|
for t in times:
|
2637
2669
|
if hour_in(hour_now, t) and update_settings > 0:
|
2638
2670
|
update_settings = 0
|
@@ -2644,7 +2676,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2644
2676
|
t['time_to_start'] = time_to_start
|
2645
2677
|
t['time_to_end'] = time_to_end
|
2646
2678
|
t['charge_time'] = charge_time
|
2647
|
-
|
2679
|
+
if time_to_run is None:
|
2680
|
+
time_to_run = time_to_start
|
2648
2681
|
# get next charge slot
|
2649
2682
|
times = sorted(times, key=lambda t: t['time_to_start'])
|
2650
2683
|
charge_key = times[0]['key']
|
@@ -2656,7 +2689,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2656
2689
|
# work out time window and times with clock changes
|
2657
2690
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
2658
2691
|
forecast_day = today if charge_today else tomorrow
|
2659
|
-
run_to =
|
2692
|
+
run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
|
2660
2693
|
run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
|
2661
2694
|
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
2695
|
bat_hold = times[0]['hold']
|
@@ -2681,8 +2714,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2681
2714
|
bat_power = 0.0
|
2682
2715
|
temperature = 30
|
2683
2716
|
bms_charge_current = 15
|
2684
|
-
charge_loss = battery_params[2]['charge_loss']
|
2685
|
-
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']
|
2686
2719
|
bat_current = 0.0
|
2687
2720
|
device_power = 6.0
|
2688
2721
|
device_current = 35
|
@@ -2704,19 +2737,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2704
2737
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2705
2738
|
return None
|
2706
2739
|
bms_charge_current = battery.get('charge_rate')
|
2707
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2708
|
-
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
|
2709
2742
|
device_power = device.get('power')
|
2710
2743
|
device_current = device.get('max_charge_current')
|
2711
2744
|
model = device.get('deviceType')
|
2712
2745
|
min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
|
2713
2746
|
max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
|
2747
|
+
reserve = capacity * min_soc / 100
|
2748
|
+
# charge current may be derated based on temperature
|
2749
|
+
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2750
|
+
if bms_charge_current is not None and charge_current > bms_charge_current:
|
2751
|
+
charge_current = bms_charge_current
|
2714
2752
|
volt_curve = charge_config['volt_curve']
|
2715
2753
|
nominal_soc = charge_config['nominal_soc']
|
2716
2754
|
volt_nominal = interpolate(nominal_soc / 10, volt_curve)
|
2717
2755
|
bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
|
2718
2756
|
bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
|
2719
|
-
reserve = capacity * min_soc / 100
|
2720
2757
|
output(f"\nBattery Info:")
|
2721
2758
|
output(f" Capacity: {capacity:.2f}kWh")
|
2722
2759
|
output(f" Residual: {residual:.2f}kWh")
|
@@ -2727,9 +2764,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2727
2764
|
output(f" Current SoC: {current_soc}%")
|
2728
2765
|
output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
|
2729
2766
|
output(f" Temperature: {temperature:.1f}°C")
|
2730
|
-
output(f" Max Charge: {
|
2767
|
+
output(f" Max Charge: {charge_current:.1f}A")
|
2731
2768
|
output(f" Resistance: {bat_resistance:.2f} ohms")
|
2732
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)
|
2733
2771
|
# charge current may be derated based on temperature
|
2734
2772
|
charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
|
2735
2773
|
if charge_current > bms_charge_current:
|
@@ -2762,8 +2800,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2762
2800
|
charge_config['charge_limit'] = charge_limit
|
2763
2801
|
charge_config['charge_power'] = charge_power
|
2764
2802
|
charge_config['float_charge'] = float_charge
|
2765
|
-
charge_config['
|
2766
|
-
charge_config['
|
2803
|
+
charge_config['_charge_loss'] = charge_loss
|
2804
|
+
charge_config['_discharge_loss'] = discharge_loss
|
2767
2805
|
# display what we have
|
2768
2806
|
output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
|
2769
2807
|
output(f"\nDevice Info:")
|
@@ -2927,6 +2965,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2927
2965
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2928
2966
|
charge_message = "no charge needed"
|
2929
2967
|
kwh_needed = 0.0
|
2968
|
+
kwh_spare = kwh_min - reserve
|
2930
2969
|
hours = 0.0
|
2931
2970
|
start_timed = time_to_end
|
2932
2971
|
end_timed = time_to_end
|
@@ -2934,6 +2973,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2934
2973
|
else:
|
2935
2974
|
# work out time to add kwh_needed to battery
|
2936
2975
|
charge_rate = charge_power * charge_loss
|
2976
|
+
discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
|
2937
2977
|
hours = kwh_needed / charge_rate
|
2938
2978
|
if test_charge is None:
|
2939
2979
|
output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
|
@@ -2944,13 +2984,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2944
2984
|
if hours > charge_time:
|
2945
2985
|
hours = charge_time
|
2946
2986
|
elif hours > hours_to_full:
|
2947
|
-
kwh_shortfall = (
|
2948
|
-
required = hours_to_full +
|
2987
|
+
kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
|
2988
|
+
required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
|
2949
2989
|
hours = required if required > hours and required < charge_time else charge_time
|
2950
2990
|
# round charge time and work out what will actually be added
|
2951
2991
|
min_hours = charge_config['min_hours']
|
2952
2992
|
hours = int(hours / min_hours + 0.99) * min_hours
|
2953
2993
|
kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
|
2994
|
+
kwh_added += discharge_rate * hours # discharge saved during charging
|
2995
|
+
kwh_spare = kwh_min - reserve + kwh_added
|
2954
2996
|
# rework charge and discharge
|
2955
2997
|
charge_period = get_best_charge_period(start_at, hours)
|
2956
2998
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
@@ -2986,7 +3028,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2986
3028
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
2987
3029
|
# show the results
|
2988
3030
|
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: {
|
3031
|
+
output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
|
2990
3032
|
if not charge_today:
|
2991
3033
|
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
2992
3034
|
# setup charging
|
@@ -3215,19 +3257,19 @@ def bat_count(cell_count):
|
|
3215
3257
|
battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
|
3216
3258
|
|
3217
3259
|
# show information about the current state of the batteries
|
3218
|
-
def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
3260
|
+
def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
|
3219
3261
|
global debug_setting, battery_info_app_key
|
3220
3262
|
if bat is None:
|
3221
|
-
bats = get_batteries(info=info)
|
3263
|
+
bats = get_batteries(info=info, rated=rated, count=count)
|
3222
3264
|
if bats is None:
|
3223
3265
|
return None
|
3224
3266
|
for i in range(0, len(bats)):
|
3225
3267
|
output(f"\n----------------------- BMS {i+1} -----------------------")
|
3226
|
-
battery_info(log=log, plot=plot,
|
3268
|
+
battery_info(log=log, plot=plot, info=info, bat=bats[i])
|
3227
3269
|
return None
|
3228
3270
|
output_spool(battery_info_app_key)
|
3229
3271
|
nbat = None
|
3230
|
-
if
|
3272
|
+
if bat.get('info') is not None:
|
3231
3273
|
b = bat['info']
|
3232
3274
|
output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
|
3233
3275
|
nbat = 0
|
@@ -3250,7 +3292,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3250
3292
|
return None
|
3251
3293
|
nv = len(cell_volts)
|
3252
3294
|
if nbat is None:
|
3253
|
-
nbat = bat_count(nv) if count is None else count
|
3295
|
+
nbat = bat_count(nv) if bat.get('count') is None else bat['count']
|
3254
3296
|
if nbat is None:
|
3255
3297
|
output(f"** battery_info(): unable to match cells_per_battery for {nv}")
|
3256
3298
|
output_close()
|
@@ -3950,17 +3992,9 @@ class Solcast :
|
|
3950
3992
|
total_actual = None
|
3951
3993
|
self.actual = get_history('day', d=day, v=v)
|
3952
3994
|
plots = {}
|
3995
|
+
times = [i/2 for i in range(0, 48)]
|
3953
3996
|
for v in self.actual:
|
3954
|
-
|
3955
|
-
actual_values = []
|
3956
|
-
average = 0.0
|
3957
|
-
for i in range(0, len(v.get('data'))):
|
3958
|
-
average += v['data'][i]['value'] / 6
|
3959
|
-
if i % 6 == 5:
|
3960
|
-
times.append(round_time((i - 5) / 12))
|
3961
|
-
actual_values.append(average)
|
3962
|
-
average = 0
|
3963
|
-
plots[v['variable']] = actual_values
|
3997
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
3964
3998
|
if v['variable'] == 'pvPower':
|
3965
3999
|
total_actual = v.get('kwh')
|
3966
4000
|
if total_actual is None:
|
@@ -3992,10 +4026,10 @@ class Solcast :
|
|
3992
4026
|
forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
|
3993
4027
|
total_forecast = sum(forecast_values) / 2
|
3994
4028
|
plots['forecast'] = forecast_values
|
3995
|
-
if total_actual is not None:
|
3996
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
3997
4029
|
if total_forecast is not None:
|
3998
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")
|
3999
4033
|
print()
|
4000
4034
|
title = f"Forecast / Actual PV Power on {day}"
|
4001
4035
|
plt.figure(figsize=(figure_width, figure_width/3))
|
@@ -4283,17 +4317,9 @@ class Solar :
|
|
4283
4317
|
total_actual = None
|
4284
4318
|
self.actual = get_history('day', d=day, v=v)
|
4285
4319
|
plots = {}
|
4320
|
+
times = [i/2 for i in range(0, 48)]
|
4286
4321
|
for v in self.actual:
|
4287
|
-
|
4288
|
-
actual_values = []
|
4289
|
-
average = 0.0
|
4290
|
-
for i in range(0, len(v.get('data'))):
|
4291
|
-
average += v['data'][i]['value'] / 6
|
4292
|
-
if i % 6 == 5:
|
4293
|
-
times.append(round_time((i - 5) / 12))
|
4294
|
-
actual_values.append(average)
|
4295
|
-
average = 0
|
4296
|
-
plots[v['variable']] = actual_values
|
4322
|
+
plots[v['variable']] = rescale_history(v.get('data'), 2)
|
4297
4323
|
if v['variable'] == 'pvPower':
|
4298
4324
|
total_actual = v.get('kwh')
|
4299
4325
|
if total_actual is None:
|
@@ -4323,10 +4349,10 @@ class Solar :
|
|
4323
4349
|
forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
|
4324
4350
|
total_forecast = sum(forecast_values) / 2
|
4325
4351
|
plots['forecast'] = forecast_values
|
4326
|
-
if total_actual is not None:
|
4327
|
-
print(f" Total actual: {total_actual:.3f}kWh")
|
4328
4352
|
if total_forecast is not None:
|
4329
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")
|
4330
4356
|
print()
|
4331
4357
|
title = f"Forecast / Actual PV Power on {day}"
|
4332
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
|
@@ -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
|
@@ -334,7 +341,7 @@ The previous section provides functions that can be used to access and control y
|
|
334
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.
|
335
342
|
|
336
343
|
```
|
337
|
-
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)
|
338
345
|
```
|
339
346
|
|
340
347
|
All the parameters are optional:
|
@@ -345,6 +352,7 @@ All the parameters are optional:
|
|
345
352
|
+ update_settings: 0 no changes, 1 update charge settings. The default is 0
|
346
353
|
+ show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
|
347
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
|
348
356
|
|
349
357
|
### Modelling
|
350
358
|
|
@@ -387,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
|
|
387
395
|
dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
|
388
396
|
pv_loss: 0.950 # loss converting PV power to DC battery charge power
|
389
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
|
390
400
|
inverter_power: None # inverter power consumption in W (dynamically set)
|
391
401
|
bms_power: 50 # BMS power consumption in W
|
392
402
|
force_charge_power: 5.00 # power used when Force Charge is scheduled
|
@@ -797,6 +807,19 @@ This setting can be:
|
|
797
807
|
|
798
808
|
# Version Info
|
799
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
|
+
|
815
|
+
2.6.9<br>
|
816
|
+
Add get and set_named_settings() (for WorkMode and ExportLimit).
|
817
|
+
If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
|
818
|
+
Updates to get_battery() / get_batteries() to add optional rated and count parameters.
|
819
|
+
Updates to charge_needed() to end prediction at start of next charge period.
|
820
|
+
Correct charge time for Octopus Go tariff.
|
821
|
+
Update charge_needed() to show contingency achieved rather than requested.
|
822
|
+
|
800
823
|
2.6.8<br>
|
801
824
|
Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
|
802
825
|
Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=rr9famUpnu4L8sSyBgkcR1GfQyANvbfK05hw4p4d9LI,222420
|
2
|
+
foxesscloud/openapi.py,sha256=EvgmhA1zq70xx1YqpcifBa9AepRsqM7gt0x3M9M3Vrg,206406
|
3
|
+
foxesscloud-2.7.0.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.7.0.dist-info/METADATA,sha256=NqT_e_whf-RIU3rvVqAUzNN7N09WPctPJOp7xhyRCuE,61042
|
5
|
+
foxesscloud-2.7.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.7.0.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.7.0.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
|