foxesscloud 2.6.3__py3-none-any.whl → 2.6.5__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 +27 -30
- foxesscloud/openapi.py +67 -158
- {foxesscloud-2.6.3.dist-info → foxesscloud-2.6.5.dist-info}/METADATA +18 -2
- foxesscloud-2.6.5.dist-info/RECORD +7 -0
- foxesscloud-2.6.3.dist-info/RECORD +0 -7
- {foxesscloud-2.6.3.dist-info → foxesscloud-2.6.5.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.3.dist-info → foxesscloud-2.6.5.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.3.dist-info → foxesscloud-2.6.5.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: 43 October 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2023
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "1.7.
|
13
|
+
version = "1.7.7"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -593,13 +593,13 @@ battery_params = {
|
|
593
593
|
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
594
594
|
'step': 5,
|
595
595
|
'offset': 5,
|
596
|
-
'charge_loss': 0.
|
597
|
-
'discharge_loss': 0.
|
596
|
+
'charge_loss': 0.974,
|
597
|
+
'discharge_loss': 0.974},
|
598
598
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
599
599
|
'step': 5,
|
600
600
|
'offset': 5,
|
601
601
|
'charge_loss': 1.08,
|
602
|
-
'discharge_loss': 0.
|
602
|
+
'discharge_loss': 0.95},
|
603
603
|
}
|
604
604
|
|
605
605
|
def get_battery(info=1):
|
@@ -690,29 +690,26 @@ def get_batteries(info=1):
|
|
690
690
|
b['residual_handling'] = residual_handling
|
691
691
|
if b.get('info') is not None and b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
|
692
692
|
b['residual_handling'] = 2
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
else
|
697
|
-
|
698
|
-
b['soh'] = None
|
699
|
-
b['soh_supported'] = False
|
693
|
+
rated_capacity = b.get('ratedCapacity')
|
694
|
+
b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else None
|
695
|
+
soh = b.get('soh')
|
696
|
+
b['soh'] = int(soh) if soh.isnumeric() and int(soh) > 10 else None
|
697
|
+
b['soh_supported'] = b['soh'] is not None
|
700
698
|
for i, b in enumerate(batteries):
|
701
699
|
if i == 0:
|
702
700
|
residual_handling = b['residual_handling']
|
703
701
|
get_battery(info=0)
|
704
|
-
battery['ratedCapacity'] = b.get('ratedCapacity')
|
705
702
|
b['capacity'] = battery.get('capacity')
|
706
703
|
b['residual'] = battery.get('residual')
|
707
704
|
b['soc'] = battery.get('soc')
|
708
|
-
if b.get('capacity') is None and b.get('soh') is not None:
|
709
|
-
capacity = (b['ratedCapacity'] / 1000 * b['soh'] / 100)
|
705
|
+
if b.get('capacity') is None and b.get('ratedCapacity') is not None and b.get('soh') is not None:
|
706
|
+
b['capacity'] = round(b['ratedCapacity'] / 1000 * b['soh'] / 100, 3)
|
707
|
+
if b.get('residual') is None and b.get('capacity') is not None and b.get('soc') is not None:
|
710
708
|
soc = b.get('soc')
|
711
|
-
residual = capacity * soc / 100
|
712
|
-
b['capacity'] = round(capacity, 3)
|
709
|
+
residual = b['capacity'] * b['soc'] / 100
|
713
710
|
b['residual'] = round(residual, 3)
|
714
711
|
if b.get('capacity') is not None and b.get('ratedCapacity') is not None:
|
715
|
-
b['soh'] = round(b['capacity'] / b['ratedCapacity'] *
|
712
|
+
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
|
716
713
|
b['charge_rate'] = 50
|
717
714
|
params = battery_params[b['residual_handling']]
|
718
715
|
b['charge_loss'] = params['charge_loss']
|
@@ -1025,7 +1022,7 @@ def get_remote_settings(key):
|
|
1025
1022
|
result = response.json().get('result')
|
1026
1023
|
if result is None:
|
1027
1024
|
errno = response.json().get('errno')
|
1028
|
-
output(f"** get_remote_settings(), no result data, {errno_message(
|
1025
|
+
output(f"** get_remote_settings(), no result data, {errno_message(response)}")
|
1029
1026
|
return None
|
1030
1027
|
values = result.get('values')
|
1031
1028
|
if values is None:
|
@@ -1439,7 +1436,7 @@ def set_schedule(periods=None, template=None, enable=True):
|
|
1439
1436
|
# d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
|
1440
1437
|
# v = list of variables to get
|
1441
1438
|
# summary = 0: raw data, 1: add max, min, sum, 2: summarise and drop raw data, 3: calculate state
|
1442
|
-
# save = "xxxxx": save the raw results to
|
1439
|
+
# save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
|
1443
1440
|
# load = "<file>": load the raw results from <file>
|
1444
1441
|
# plot = 0: no plot, 1: plot variables separately, 2: combine variables
|
1445
1442
|
# station = 0: use device_id, 1: use station_id
|
@@ -1507,7 +1504,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1507
1504
|
result = json.load(file)
|
1508
1505
|
file.close()
|
1509
1506
|
if save is not None:
|
1510
|
-
file_name = save + "
|
1507
|
+
file_name = save + "_history_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
|
1511
1508
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1512
1509
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1513
1510
|
file.close()
|
@@ -1712,7 +1709,7 @@ get_history = get_raw
|
|
1712
1709
|
# d = day 'YYYY-MM-DD'
|
1713
1710
|
# v = list of report variables to get
|
1714
1711
|
# summary = 0, 1, 2: do a quick total energy report for a day
|
1715
|
-
# save = "xxxxx": save the report results to
|
1712
|
+
# save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
|
1716
1713
|
# load = "<file>": load the report results from <file>
|
1717
1714
|
# plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
|
1718
1715
|
# station = 0: use device_id, 1 = use station_id
|
@@ -1842,7 +1839,7 @@ def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=Non
|
|
1842
1839
|
result = json.load(file)
|
1843
1840
|
file.close()
|
1844
1841
|
elif save is not None:
|
1845
|
-
file_name = save + "
|
1842
|
+
file_name = save + "_report_" + report_type + "_" + d.replace('-','') + ".txt"
|
1846
1843
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1847
1844
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1848
1845
|
file.close()
|
@@ -2418,9 +2415,9 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2418
2415
|
plunge = []
|
2419
2416
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2420
2417
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2421
|
-
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else
|
2418
|
+
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 8
|
2422
2419
|
for i in range(0, len(prices)):
|
2423
|
-
# hour relative index into list of plunge prices, starting at 7am
|
2420
|
+
# hour relative index into list of plunge prices and slots, starting at 7am
|
2424
2421
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
2425
2422
|
if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
|
2426
2423
|
plunge.append(i)
|
@@ -2841,7 +2838,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2841
2838
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2842
2839
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2843
2840
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2844
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery,
|
2841
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
|
2845
2842
|
print(f"\n---------------- charge_needed ----------------")
|
2846
2843
|
# validate parameters
|
2847
2844
|
args = locals()
|
@@ -2948,8 +2945,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2948
2945
|
bat_power = 0.0
|
2949
2946
|
temperature = 30
|
2950
2947
|
bms_charge_current = 15
|
2951
|
-
charge_loss =
|
2952
|
-
discharge_loss =
|
2948
|
+
charge_loss = battery_params[2]['charge_loss']
|
2949
|
+
discharge_loss = battery_params[2]['discharge_loss']
|
2953
2950
|
bat_current = 0.0
|
2954
2951
|
device_power = 6.0
|
2955
2952
|
device_current = 35
|
@@ -2971,8 +2968,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2971
2968
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2972
2969
|
return None
|
2973
2970
|
bms_charge_current = battery.get('charge_rate')
|
2974
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else
|
2975
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else
|
2971
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2972
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2976
2973
|
device_power = device.get('power')
|
2977
2974
|
device_current = device.get('max_charge_current')
|
2978
2975
|
model = device.get('deviceType')
|
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: 14 October 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.5"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -281,7 +281,7 @@ var_table = None
|
|
281
281
|
var_list = None
|
282
282
|
|
283
283
|
def get_vars():
|
284
|
-
global var_table, var_list, debug_setting, messages, lang
|
284
|
+
global var_table, var_list, debug_setting, messages, lang
|
285
285
|
if api_key is None:
|
286
286
|
output(f"** please generate an API Key at foxesscloud.com and provide this (f.api_key='your API key')")
|
287
287
|
return None
|
@@ -558,13 +558,13 @@ battery_params = {
|
|
558
558
|
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
559
559
|
'step': 5,
|
560
560
|
'offset': 5,
|
561
|
-
'charge_loss': 0.
|
562
|
-
'discharge_loss': 0.
|
561
|
+
'charge_loss': 0.974,
|
562
|
+
'discharge_loss': 0.974},
|
563
563
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
564
564
|
'step': 5,
|
565
565
|
'offset': 5,
|
566
566
|
'charge_loss': 1.080,
|
567
|
-
'discharge_loss': 0.
|
567
|
+
'discharge_loss': 0.95},
|
568
568
|
}
|
569
569
|
|
570
570
|
def get_battery(info=0, v=None):
|
@@ -615,7 +615,7 @@ def get_batteries(info=0):
|
|
615
615
|
##################################################################################################
|
616
616
|
|
617
617
|
def get_charge():
|
618
|
-
global
|
618
|
+
global device_sn, battery_settings, debug_setting
|
619
619
|
if get_device() is None:
|
620
620
|
return None
|
621
621
|
if battery_settings is None:
|
@@ -647,7 +647,7 @@ def time_period(t, n):
|
|
647
647
|
return result
|
648
648
|
|
649
649
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
|
650
|
-
global
|
650
|
+
global device_sn, battery_settings, debug_setting, time_period_vars
|
651
651
|
if get_device() is None:
|
652
652
|
return None
|
653
653
|
if battery_settings is None:
|
@@ -723,7 +723,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
723
723
|
##################################################################################################
|
724
724
|
|
725
725
|
def get_min():
|
726
|
-
global
|
726
|
+
global device_sn, battery_settings, debug_setting
|
727
727
|
if get_device() is None:
|
728
728
|
return None
|
729
729
|
if battery_settings is None:
|
@@ -747,7 +747,7 @@ def get_min():
|
|
747
747
|
##################################################################################################
|
748
748
|
|
749
749
|
def set_min(minSocOnGrid = None, minSoc = None, force = 0):
|
750
|
-
global
|
750
|
+
global device_sn, schedule, battery_settings, debug_setting
|
751
751
|
if get_device() is None:
|
752
752
|
return None
|
753
753
|
if schedule['enable'] == True:
|
@@ -801,160 +801,71 @@ def get_settings():
|
|
801
801
|
# get remote settings
|
802
802
|
##################################################################################################
|
803
803
|
|
804
|
-
|
805
|
-
named_settings =
|
806
|
-
merge_settings = { # keys to add
|
807
|
-
'WorkMode': {'keys': {
|
808
|
-
'h115__': 'operation_mode__work_mode',
|
809
|
-
'h116__': 'operation_mode__work_mode',
|
810
|
-
'h117__': 'operation_mode__work_mode',
|
811
|
-
# 'k106__': 'operation_mode__work_mode',
|
812
|
-
},
|
813
|
-
'values': ['SelfUse', 'Feedin', 'Backup']},
|
814
|
-
'BatteryVolt': {'keys': {
|
815
|
-
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
816
|
-
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
817
|
-
'h117__': ['h117__15', 'h117__16', 'h117__17'],
|
818
|
-
# 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
|
819
|
-
},
|
820
|
-
'type': 'list',
|
821
|
-
'valueType': 'float',
|
822
|
-
'unit': 'V'},
|
823
|
-
'BatteryTemp': {'keys': {
|
824
|
-
'h115__': 'h115__17',
|
825
|
-
'h116__': 'h116__18',
|
826
|
-
'h117__': 'h117__18',
|
827
|
-
# 'k106__': 'k106__xx',
|
828
|
-
},
|
829
|
-
'type': 'list',
|
830
|
-
'valueType': 'int',
|
831
|
-
'unit': '℃'},
|
832
|
-
}
|
804
|
+
# store for named settings info
|
805
|
+
named_settings = {}
|
833
806
|
|
834
|
-
def
|
835
|
-
global
|
836
|
-
if get_device() is None:
|
837
|
-
return None
|
838
|
-
if remote_settings is None:
|
839
|
-
output(f"getting ui settings", 2)
|
840
|
-
params = {'id': device_id}
|
841
|
-
response = signed_get(path="/generic/v0/device/setting/ui", params=params)
|
842
|
-
if response.status_code != 200:
|
843
|
-
output(f"** get_ui() got response code {response.status_code}: {response.reason}")
|
844
|
-
return None
|
845
|
-
result = response.json().get('result')
|
846
|
-
if result is None:
|
847
|
-
errno = response.json().get('errno')
|
848
|
-
output(f"** get_ui(), no result data, {errno_message(errno)}")
|
849
|
-
return None
|
850
|
-
remote_settings = result
|
851
|
-
protocol = remote_settings['protocol'].lower().replace('xx','__')
|
852
|
-
named_settings = {'_protocol': protocol}
|
853
|
-
volt_n = 0
|
854
|
-
volt_keys = []
|
855
|
-
for p in remote_settings['parameters']:
|
856
|
-
if p['name'][:11] == 'BatteryVolt': # merge BatteryVolts
|
857
|
-
output(f" found {p['name']} with key {p['key']}", 2)
|
858
|
-
volt_n += 1
|
859
|
-
volt_keys.append(p['key'])
|
860
|
-
if volt_n == 3:
|
861
|
-
named_settings['BatteryVolt'] = {'keys': volt_keys, 'type': 'list', 'valueType': 'float', 'unit': p['properties'][0]['unit']}
|
862
|
-
elif volt_n > 3:
|
863
|
-
print(f"** get_ui(): more than 3 groups found for BatteryVolt, n={volt_n}")
|
864
|
-
elif p['name'][:11] == 'BatteryTemp':
|
865
|
-
output(f" found {p['name']} with key {p['key']}", 2)
|
866
|
-
named_settings['BatteryTemp'] = {'keys': p['key'], 'type': 'list', 'valueType': 'int', 'unit': p['properties'][0]['unit']}
|
867
|
-
else:
|
868
|
-
items = []
|
869
|
-
block = p['block'] and len(p['properties']) > 1
|
870
|
-
for e in p['properties']:
|
871
|
-
valueType = e['elemType']['valueType']
|
872
|
-
item = {'name': e['key'].replace(protocol,'')} if block else {'key': e['key']} #, 'group': p['name']}
|
873
|
-
if e['elemType'].get('uiItems') is not None:
|
874
|
-
item['values'] = e['elemType']['uiItems']
|
875
|
-
elif e.get('range') is not None:
|
876
|
-
item['range'] = e['range']
|
877
|
-
item['valueType'] = 'float' if type(e['range']['hi']) is float else 'int'
|
878
|
-
else:
|
879
|
-
item['type'] = valueType
|
880
|
-
if e.get('unit') is not None and len(e['unit']) > 0:
|
881
|
-
item['unit'] = e['unit']
|
882
|
-
if block:
|
883
|
-
items.append(item)
|
884
|
-
else:
|
885
|
-
named_settings[e['name']] = item
|
886
|
-
if block:
|
887
|
-
named_settings[p['name']] = {'key': p['key'], 'type': 'block', 'items': items}
|
888
|
-
for name in merge_settings.keys():
|
889
|
-
if named_settings.get(name) is None and merge_settings[name]['keys'].get(protocol) is not None:
|
890
|
-
named_settings[name] = {'keys': merge_settings[name]['keys'][protocol]}
|
891
|
-
for k in merge_settings[name].keys():
|
892
|
-
if k != 'keys':
|
893
|
-
named_settings[name][k] = merge_settings[name][k]
|
894
|
-
return remote_settings
|
895
|
-
|
896
|
-
def get_remote_settings(key):
|
897
|
-
global token, device_id, debug_setting, messages
|
807
|
+
def get_remote_settings(name):
|
808
|
+
global device_sn, debug_setting, messages, name_data
|
898
809
|
if get_device() is None:
|
899
810
|
return None
|
900
811
|
output(f"getting remote settings", 2)
|
901
|
-
if
|
812
|
+
if name is None:
|
902
813
|
return None
|
903
|
-
if type(
|
814
|
+
if type(name) is list:
|
904
815
|
values = {}
|
905
|
-
for
|
906
|
-
v = get_remote_settings(
|
816
|
+
for n in name:
|
817
|
+
v = get_remote_settings(n)
|
907
818
|
if v is None:
|
908
|
-
|
819
|
+
continue
|
909
820
|
for x in v.keys():
|
910
821
|
values[x] = v[x]
|
911
822
|
return values
|
912
|
-
|
913
|
-
response =
|
823
|
+
body = {'sn': device_sn, 'key': name}
|
824
|
+
response = signed_post(path="/op/v0/device/setting/get", body=body)
|
914
825
|
if response.status_code != 200:
|
915
826
|
output(f"** get_remote_settings() got response code {response.status_code}: {response.reason}")
|
916
827
|
return None
|
917
828
|
result = response.json().get('result')
|
918
829
|
if result is None:
|
919
830
|
errno = response.json().get('errno')
|
920
|
-
output(f"** get_remote_settings(), no result data, {errno_message(
|
831
|
+
output(f"** get_remote_settings(), no result data, {errno_message(response)}")
|
921
832
|
return None
|
922
|
-
|
923
|
-
|
924
|
-
|
833
|
+
named_settings[name] = result
|
834
|
+
value = result.get('value')
|
835
|
+
if value is None:
|
836
|
+
output(f"** get_remote_settings(), no value for {name}")
|
925
837
|
return None
|
926
|
-
return
|
838
|
+
return value
|
927
839
|
|
928
840
|
def get_named_settings(name):
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
return result
|
935
|
-
if named_settings is None or named_settings.get(name) is None:
|
936
|
-
output(f"** get_named_settings(): {name} was not recognised")
|
841
|
+
return get_remote_settings(name)
|
842
|
+
|
843
|
+
def set_named_setting(name, value):
|
844
|
+
global device_sn, debug_setting
|
845
|
+
if get_device() is None:
|
937
846
|
return None
|
938
|
-
|
939
|
-
|
940
|
-
|
847
|
+
if type(name) is list:
|
848
|
+
count = 0
|
849
|
+
for (n, v) in name:
|
850
|
+
result = set_named_settings(name=n, value=v)
|
851
|
+
if result is not None:
|
852
|
+
count += 1
|
853
|
+
return count
|
854
|
+
output(f"\nSetting {name} to {value}", 1)
|
855
|
+
body = {'sn': device_sn, 'key': name, 'value': f"{value}"}
|
856
|
+
setting_delay()
|
857
|
+
response = signed_post(path="/op/v0/device/setting/set", body=body)
|
858
|
+
if response.status_code != 200:
|
859
|
+
output(f"** set_named_settings(): ({name}, {value}) got response code {response.status_code}: {response.reason}")
|
941
860
|
return None
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
861
|
+
errno = response.json().get('errno')
|
862
|
+
if errno != 0:
|
863
|
+
if errno == 44096:
|
864
|
+
output(f"** cannot update settings when schedule is active")
|
865
|
+
else:
|
866
|
+
output(f"** set_named_settings(): ({name}, {value}) {errno_message(response)}")
|
946
867
|
return None
|
947
|
-
|
948
|
-
value_type = named_settings[name].get('valueType')
|
949
|
-
if result_type is None:
|
950
|
-
v = result.get([k for k in result.keys()][0])
|
951
|
-
return v if value_type is None else c_float(v) if value_type == 'float' else c_int(v)
|
952
|
-
if result_type == 'list':
|
953
|
-
values = []
|
954
|
-
for k in sorted(result.keys()):
|
955
|
-
values.append(result[k] if value_type is None else c_float(result[k]) if value_type == 'float' else c_int(result[k]))
|
956
|
-
return values
|
957
|
-
return result
|
868
|
+
return 1
|
958
869
|
|
959
870
|
##################################################################################################
|
960
871
|
# wrappers for named settings
|
@@ -964,8 +875,6 @@ work_mode = None
|
|
964
875
|
|
965
876
|
def get_work_mode():
|
966
877
|
global work_mode
|
967
|
-
# print(f"** get_work_mode(): not available via Open API")
|
968
|
-
return None
|
969
878
|
if get_device() is None:
|
970
879
|
return None
|
971
880
|
work_mode = get_named_settings('WorkMode')
|
@@ -983,6 +892,8 @@ temp_slots_per_battery = 8
|
|
983
892
|
|
984
893
|
def get_cell_temps(nbat=8):
|
985
894
|
global temp_slots_per_battery
|
895
|
+
print(f"** get_cell_temps(): not available via Open API")
|
896
|
+
return None
|
986
897
|
values = get_named_settings('BatteryTemp')
|
987
898
|
if values is None:
|
988
899
|
return None
|
@@ -1008,9 +919,7 @@ work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
|
|
1008
919
|
settable_modes = work_modes[:3]
|
1009
920
|
|
1010
921
|
def set_work_mode(mode, force = 0):
|
1011
|
-
global
|
1012
|
-
print(f"** set_work_mode(): not available via Open API")
|
1013
|
-
return None
|
922
|
+
global device_sn, work_modes, work_mode, debug_setting
|
1014
923
|
if get_device() is None:
|
1015
924
|
return None
|
1016
925
|
if mode not in settable_modes:
|
@@ -1022,7 +931,7 @@ def set_work_mode(mode, force = 0):
|
|
1022
931
|
return None
|
1023
932
|
set_schedule(enable=0)
|
1024
933
|
output(f"\nSetting work mode: {mode}", 1)
|
1025
|
-
body = {'sn': device_sn, 'key': '
|
934
|
+
body = {'sn': device_sn, 'key': 'WorkMode', 'value': mode}
|
1026
935
|
setting_delay()
|
1027
936
|
response = signed_post(path="/op/v0/device/setting/set", body=body)
|
1028
937
|
if response.status_code != 200:
|
@@ -1180,7 +1089,7 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1180
1089
|
|
1181
1090
|
# set a schedule from a period or list of time segment periods
|
1182
1091
|
def set_schedule(periods=None, enable=True):
|
1183
|
-
global
|
1092
|
+
global device_sn, debug_setting, schedule
|
1184
1093
|
if get_flag() is None:
|
1185
1094
|
return None
|
1186
1095
|
if schedule.get('support') == False:
|
@@ -1279,7 +1188,7 @@ def get_real(v = None):
|
|
1279
1188
|
# d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
|
1280
1189
|
# v = list of variables to get
|
1281
1190
|
# summary = 0: raw data, 1: add max, min, sum, 2: summarise and drop raw data, 3: calculate state
|
1282
|
-
# save = "xxxxx": save the raw results to
|
1191
|
+
# save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
|
1283
1192
|
# load = "<file>": load the raw results from <file>
|
1284
1193
|
# plot = 0: no plot, 1: plot variables separately, 2: combine variables
|
1285
1194
|
##################################################################################################
|
@@ -1294,7 +1203,7 @@ sample_time = 5.0 # 5 minutes default
|
|
1294
1203
|
sample_rounding = 2 # round to 30 seconds
|
1295
1204
|
|
1296
1205
|
def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1297
|
-
global
|
1206
|
+
global device_sn, debug_setting, var_list, invert_ct2, tariff, max_power_kw, sample_rounding, sample_time, residual_scale, storage
|
1298
1207
|
if get_device() is None:
|
1299
1208
|
return None
|
1300
1209
|
time_span = time_span.lower()
|
@@ -1546,7 +1455,7 @@ def report_value_profile(result):
|
|
1546
1455
|
# d = day 'YYYY-MM-DD'
|
1547
1456
|
# v = list of report variables to get
|
1548
1457
|
# summary = 0, 1, 2: do a quick total energy report for a day
|
1549
|
-
# save = "xxxxx": save the report results to
|
1458
|
+
# save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
|
1550
1459
|
# load = "<file>": load the report results from <file>
|
1551
1460
|
# plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
|
1552
1461
|
##################################################################################################
|
@@ -1560,7 +1469,7 @@ fix_value_threshold = 200000000.0
|
|
1560
1469
|
fix_value_mask = 0x0000FFFF
|
1561
1470
|
|
1562
1471
|
def get_report(dimension='day', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1563
|
-
global
|
1472
|
+
global device_sn, var_list, debug_setting, report_vars, storage
|
1564
1473
|
if get_device() is None:
|
1565
1474
|
return None
|
1566
1475
|
# process list of days
|
@@ -2219,7 +2128,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2219
2128
|
plunge = []
|
2220
2129
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2221
2130
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2222
|
-
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else
|
2131
|
+
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 8
|
2223
2132
|
for i in range(0, len(prices)):
|
2224
2133
|
# hour relative index into list of plunge prices, starting at 7am
|
2225
2134
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
@@ -2642,7 +2551,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2642
2551
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2643
2552
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2644
2553
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2645
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery,
|
2554
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
|
2646
2555
|
print(f"\n---------------- charge_needed ----------------")
|
2647
2556
|
# validate parameters
|
2648
2557
|
args = locals()
|
@@ -2749,8 +2658,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2749
2658
|
bat_power = 0.0
|
2750
2659
|
temperature = 30
|
2751
2660
|
bms_charge_current = 15
|
2752
|
-
charge_loss =
|
2753
|
-
discharge_loss =
|
2661
|
+
charge_loss = battery_params[2]['charge_loss']
|
2662
|
+
discharge_loss = battery_params[2]['discharge_loss']
|
2754
2663
|
bat_current = 0.0
|
2755
2664
|
device_power = 6.0
|
2756
2665
|
device_current = 35
|
@@ -2772,8 +2681,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2772
2681
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2773
2682
|
return None
|
2774
2683
|
bms_charge_current = battery.get('charge_rate')
|
2775
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else
|
2776
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else
|
2684
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
2685
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2777
2686
|
device_power = device.get('power')
|
2778
2687
|
device_current = device.get('max_charge_current')
|
2779
2688
|
model = device.get('deviceType')
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.5
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -111,6 +111,7 @@ f.get_charge()
|
|
111
111
|
f.get_min()
|
112
112
|
f.get_flag()
|
113
113
|
f.get_schedule()
|
114
|
+
f.get_named_settings(name)
|
114
115
|
|
115
116
|
```
|
116
117
|
Each of these calls will return a dictionary or list containing the relevant information.
|
@@ -130,6 +131,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
|
|
130
131
|
|
131
132
|
get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
|
132
133
|
|
134
|
+
get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
|
135
|
+
+ f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
|
136
|
+
+ the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
|
137
|
+
|
133
138
|
|
134
139
|
## Inverter Settings
|
135
140
|
You can change inverter settings using:
|
@@ -140,6 +145,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
|
|
140
145
|
f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
|
141
146
|
f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
|
142
147
|
f.set_schedule(periods, enable)
|
148
|
+
f.set_named_settings(name, value)
|
143
149
|
```
|
144
150
|
|
145
151
|
set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
|
@@ -180,6 +186,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
|
|
180
186
|
+ periods: a time segment or list of time segments created using f.set_period().
|
181
187
|
+ enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
|
182
188
|
|
189
|
+
set_named_settings() sets the 'name' setting to 'value'.
|
190
|
+
+ 'name' may also be a list of (name, value) pairs.
|
191
|
+
+ the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
|
192
|
+
|
183
193
|
|
184
194
|
## Real Time Data
|
185
195
|
Real time data reports the latest values for inverter variables, collected every 5 minutes:
|
@@ -787,7 +797,13 @@ This setting can be:
|
|
787
797
|
|
788
798
|
# Version Info
|
789
799
|
|
790
|
-
2.6.
|
800
|
+
2.6.5<br>
|
801
|
+
Add get_named_settings() and set_named_settings().
|
802
|
+
Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
|
803
|
+
Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
|
804
|
+
Update calibration of 'charge_loss' and 'discharge_loss'.
|
805
|
+
|
806
|
+
2.6.4<br>
|
791
807
|
Increase default plungs_slots from 6 to 8.
|
792
808
|
Correct battery capacity in get_batteries().
|
793
809
|
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=I9zb9WVDwV68G-UDrWipB9jYRLSj6dWst-grFdV0KiQ,217574
|
2
|
+
foxesscloud/openapi.py,sha256=ZK9GQmOSI44_Dm59-3A0-vtOzOLHmwqeBlzDT9RBl5A,202864
|
3
|
+
foxesscloud-2.6.5.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.6.5.dist-info/METADATA,sha256=S2q4LFhzJOBaFhNjqpEaJboKya9SIKvp67fyTvarGz8,58769
|
5
|
+
foxesscloud-2.6.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.6.5.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.6.5.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=qhra8fiKsM3xoQCbzQYsPoY0D4mNPbpOwXP_AgpXQ9E,217489
|
2
|
-
foxesscloud/openapi.py,sha256=YwFHpTH4CyTMyf9f5ZXD0-umFL-lWnq2aQHA0XV7YuM,207518
|
3
|
-
foxesscloud-2.6.3.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.6.3.dist-info/METADATA,sha256=j3UCMUoCfv10iOf5_8VcDPA8MbCO5CpHllwYi6jSRzw,57854
|
5
|
-
foxesscloud-2.6.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.6.3.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.6.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|