foxesscloud 2.6.4__py3-none-any.whl → 2.6.6__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 +98 -59
- foxesscloud/openapi.py +139 -200
- {foxesscloud-2.6.4.dist-info → foxesscloud-2.6.6.dist-info}/METADATA +26 -1
- foxesscloud-2.6.6.dist-info/RECORD +7 -0
- foxesscloud-2.6.4.dist-info/RECORD +0 -7
- {foxesscloud-2.6.4.dist-info → foxesscloud-2.6.6.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.4.dist-info → foxesscloud-2.6.6.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.4.dist-info → foxesscloud-2.6.6.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: 01 November 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2023
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "1.7.
|
13
|
+
version = "1.7.8"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -583,7 +583,7 @@ battery = None
|
|
583
583
|
batteries = None
|
584
584
|
battery_settings = None
|
585
585
|
|
586
|
-
# 1 = Residual Energy, 2 = Residual Capacity
|
586
|
+
# 1 = Residual Energy, 2 = Residual Capacity (HV), 3 = Residual Capacity per battery (Mira)
|
587
587
|
residual_handling = 1
|
588
588
|
|
589
589
|
# charge rates based on residual_handling
|
@@ -593,13 +593,20 @@ battery_params = {
|
|
593
593
|
1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
|
594
594
|
'step': 5,
|
595
595
|
'offset': 5,
|
596
|
-
'charge_loss': 0.
|
597
|
-
'discharge_loss': 0.
|
596
|
+
'charge_loss': 0.974,
|
597
|
+
'discharge_loss': 0.974},
|
598
|
+
# HV BMS v2 with firmware 1.014 or later
|
598
599
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
599
600
|
'step': 5,
|
600
601
|
'offset': 5,
|
601
602
|
'charge_loss': 1.08,
|
602
|
-
'discharge_loss': 0.
|
603
|
+
'discharge_loss': 0.95},
|
604
|
+
# Mira BMS with firmware 1.014 or later
|
605
|
+
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
606
|
+
'step': 5,
|
607
|
+
'offset': 5,
|
608
|
+
'charge_loss': 0.974,
|
609
|
+
'discharge_loss': 0.974},
|
603
610
|
}
|
604
611
|
|
605
612
|
def get_battery(info=1):
|
@@ -632,10 +639,13 @@ def get_battery(info=1):
|
|
632
639
|
output(f"** get_battery().info, no result data, {errno_message(errno)}")
|
633
640
|
else:
|
634
641
|
battery['info'] = result['batteries'][0]
|
635
|
-
if battery['info']
|
642
|
+
if battery['info'].get('slaveBatteries') is not None:
|
643
|
+
battery['count'] = len(battery['info']['slaveBatteries'])
|
644
|
+
if battery['info']['masterSN'][:7] == '60BBHV2' and battery['info']['masterVersion'] >= '1.014':
|
636
645
|
residual_handling = 2
|
646
|
+
elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
|
647
|
+
residual_handling = 3
|
637
648
|
battery['residual_handling'] = residual_handling
|
638
|
-
battery['rated_capacity'] = None
|
639
649
|
battery['soh'] = None
|
640
650
|
battery['soh_supported'] = False
|
641
651
|
if battery.get('residual') is not None:
|
@@ -644,6 +654,18 @@ def get_battery(info=1):
|
|
644
654
|
capacity = battery.get('residual')
|
645
655
|
soc = battery.get('soc')
|
646
656
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
657
|
+
if battery.get('count') is None:
|
658
|
+
battery['count'] = int(battery['volt'] / 49)
|
659
|
+
if battery.get('ratedCapacity') is None:
|
660
|
+
battery['ratedCapacity'] = 2560 * battery['count']
|
661
|
+
elif battery['residual_handling'] == 3:
|
662
|
+
if battery.get('count') is None:
|
663
|
+
battery['count'] = int(battery['volt'] / 49)
|
664
|
+
capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
|
665
|
+
soc = battery.get('soc')
|
666
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
667
|
+
if battery.get('ratedCapacity') is None:
|
668
|
+
battery['ratedCapacity'] = 2450 * battery['count']
|
647
669
|
else:
|
648
670
|
residual = battery.get('residual')
|
649
671
|
soc = battery.get('soc')
|
@@ -654,6 +676,8 @@ def get_battery(info=1):
|
|
654
676
|
params = battery_params[battery['residual_handling']]
|
655
677
|
battery['charge_loss'] = params['charge_loss']
|
656
678
|
battery['discharge_loss'] = params['discharge_loss']
|
679
|
+
if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
|
680
|
+
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
|
657
681
|
if battery.get('temperature') is not None:
|
658
682
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
659
683
|
return battery
|
@@ -688,8 +712,13 @@ def get_batteries(info=1):
|
|
688
712
|
batteries[i]['info'] = result['batteries'][i]
|
689
713
|
for b in batteries:
|
690
714
|
b['residual_handling'] = residual_handling
|
691
|
-
if b.get('info') is not None
|
692
|
-
b['
|
715
|
+
if b.get('info') is not None:
|
716
|
+
if b['info'].get('slaveBatteries') is not None:
|
717
|
+
b['count'] = len(b['info']['slaveBatteries'])
|
718
|
+
if b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
|
719
|
+
b['residual_handling'] = 2
|
720
|
+
elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
|
721
|
+
residual_handling = 3
|
693
722
|
rated_capacity = b.get('ratedCapacity')
|
694
723
|
b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else None
|
695
724
|
soh = b.get('soh')
|
@@ -708,7 +737,7 @@ def get_batteries(info=1):
|
|
708
737
|
soc = b.get('soc')
|
709
738
|
residual = b['capacity'] * b['soc'] / 100
|
710
739
|
b['residual'] = round(residual, 3)
|
711
|
-
if b.get('
|
740
|
+
if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
|
712
741
|
b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
|
713
742
|
b['charge_rate'] = 50
|
714
743
|
params = battery_params[b['residual_handling']]
|
@@ -1022,7 +1051,7 @@ def get_remote_settings(key):
|
|
1022
1051
|
result = response.json().get('result')
|
1023
1052
|
if result is None:
|
1024
1053
|
errno = response.json().get('errno')
|
1025
|
-
output(f"** get_remote_settings(), no result data, {errno_message(
|
1054
|
+
output(f"** get_remote_settings(), no result data, {errno_message(response)}")
|
1026
1055
|
return None
|
1027
1056
|
values = result.get('values')
|
1028
1057
|
if values is None:
|
@@ -1323,10 +1352,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1323
1352
|
price = segment.get('price')
|
1324
1353
|
start = time_hours(start)
|
1325
1354
|
# adjust exclusive time to inclusive
|
1326
|
-
end =
|
1355
|
+
end = time_hours(end)
|
1327
1356
|
if start is None or end is None or start >= end:
|
1328
1357
|
output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
|
1329
1358
|
return None
|
1359
|
+
end = round_time(end - 1/60)
|
1330
1360
|
mode = 'SelfUse' if mode is None else mode
|
1331
1361
|
if mode not in work_modes:
|
1332
1362
|
output(f"** mode must be one of {work_modes}")
|
@@ -1399,7 +1429,7 @@ def set_schedule(periods=None, template=None, enable=True):
|
|
1399
1429
|
if len(periods) > 8:
|
1400
1430
|
output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
|
1401
1431
|
return None
|
1402
|
-
data = {'pollcy': periods, 'deviceSN': device_sn}
|
1432
|
+
data = {'pollcy': periods[-8:], 'deviceSN': device_sn}
|
1403
1433
|
schedule['pollcy'] = periods
|
1404
1434
|
schedule['template_id'] = None
|
1405
1435
|
elif template is not None:
|
@@ -1436,7 +1466,7 @@ def set_schedule(periods=None, template=None, enable=True):
|
|
1436
1466
|
# d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
|
1437
1467
|
# v = list of variables to get
|
1438
1468
|
# summary = 0: raw data, 1: add max, min, sum, 2: summarise and drop raw data, 3: calculate state
|
1439
|
-
# save = "xxxxx": save the raw results to
|
1469
|
+
# save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
|
1440
1470
|
# load = "<file>": load the raw results from <file>
|
1441
1471
|
# plot = 0: no plot, 1: plot variables separately, 2: combine variables
|
1442
1472
|
# station = 0: use device_id, 1: use station_id
|
@@ -1504,7 +1534,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1504
1534
|
result = json.load(file)
|
1505
1535
|
file.close()
|
1506
1536
|
if save is not None:
|
1507
|
-
file_name = save + "
|
1537
|
+
file_name = save + "_history_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
|
1508
1538
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1509
1539
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1510
1540
|
file.close()
|
@@ -1709,7 +1739,7 @@ get_history = get_raw
|
|
1709
1739
|
# d = day 'YYYY-MM-DD'
|
1710
1740
|
# v = list of report variables to get
|
1711
1741
|
# summary = 0, 1, 2: do a quick total energy report for a day
|
1712
|
-
# save = "xxxxx": save the report results to
|
1742
|
+
# save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
|
1713
1743
|
# load = "<file>": load the report results from <file>
|
1714
1744
|
# plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
|
1715
1745
|
# station = 0: use device_id, 1 = use station_id
|
@@ -1839,7 +1869,7 @@ def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=Non
|
|
1839
1869
|
result = json.load(file)
|
1840
1870
|
file.close()
|
1841
1871
|
elif save is not None:
|
1842
|
-
file_name = save + "
|
1872
|
+
file_name = save + "_report_" + report_type + "_" + d.replace('-','') + ".txt"
|
1843
1873
|
file = open(storage + file_name, 'w', encoding='utf-8')
|
1844
1874
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1845
1875
|
file.close()
|
@@ -2588,7 +2618,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2588
2618
|
elif type(strategy) is not list:
|
2589
2619
|
strategy = [strategy]
|
2590
2620
|
output(f"\nStrategy")
|
2591
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0
|
2621
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0) #, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2592
2622
|
output_close(plot=tariff_config['show_plot'])
|
2593
2623
|
if update == 1:
|
2594
2624
|
tariff = use
|
@@ -2665,15 +2695,15 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2665
2695
|
return profile[:run_time]
|
2666
2696
|
|
2667
2697
|
# build the timed work mode profile from the tariff strategy:
|
2668
|
-
def strategy_timed(timed_mode,
|
2698
|
+
def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, current_mode=None):
|
2669
2699
|
global tariff, steps_per_hour
|
2670
2700
|
work_mode_timed = []
|
2671
2701
|
min_soc_now = min_soc
|
2672
2702
|
max_soc_now = max_soc
|
2673
2703
|
current_mode = 'SelfUse' if current_mode is None else current_mode
|
2674
2704
|
strategy = get_strategy(timed_mode=timed_mode)
|
2675
|
-
h = base_hour
|
2676
2705
|
for i in range(0, run_time):
|
2706
|
+
h = time_line[i]
|
2677
2707
|
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0, 'charge': 0.0,
|
2678
2708
|
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2679
2709
|
if strategy is not None:
|
@@ -2692,8 +2722,8 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2692
2722
|
if d.get('fdpwr') is not None:
|
2693
2723
|
period['fdpwr'] = d['fdpwr']
|
2694
2724
|
period['duration'] = duration_in(h, d, steps_per_hour) * steps_per_hour
|
2725
|
+
break
|
2695
2726
|
work_mode_timed.append(period)
|
2696
|
-
h = round_time(h + 1 / steps_per_hour)
|
2697
2727
|
return work_mode_timed
|
2698
2728
|
|
2699
2729
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
@@ -2706,7 +2736,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2706
2736
|
discharge_loss = charge_config['discharge_loss']
|
2707
2737
|
charge_limit = charge_config['charge_limit']
|
2708
2738
|
float_charge = charge_config['float_charge']
|
2709
|
-
|
2739
|
+
run_time = len(work_mode_timed)
|
2740
|
+
for i in range(0, run_time):
|
2710
2741
|
w = work_mode_timed[i]
|
2711
2742
|
w['kwh'] = kwh_current
|
2712
2743
|
max_now = w['max_soc'] * capacity / 100
|
@@ -2717,6 +2748,7 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2717
2748
|
if kwh_current > capacity:
|
2718
2749
|
# battery is full
|
2719
2750
|
kwh_current = capacity
|
2751
|
+
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2720
2752
|
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2721
2753
|
reserve_now = capacity * min_soc_now / 100
|
2722
2754
|
if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
|
@@ -2746,7 +2778,7 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2746
2778
|
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2747
2779
|
next_period = work_mode_timed[t]
|
2748
2780
|
h = base_hour + t / steps_per_hour
|
2749
|
-
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
|
2781
|
+
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold'] or period['min_soc'] != next_period['min_soc']:
|
2750
2782
|
s = {'start': start % 24, 'end': h % 24, 'mode': period['mode'], 'min_soc': period['min_soc']}
|
2751
2783
|
if period['mode'] == 'ForceDischarge':
|
2752
2784
|
s['fdsoc'] = period.get('fdsoc')
|
@@ -2838,7 +2870,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2838
2870
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2839
2871
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2840
2872
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2841
|
-
global timed_strategy, steps_per_hour, base_time, storage, battery,
|
2873
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
|
2842
2874
|
print(f"\n---------------- charge_needed ----------------")
|
2843
2875
|
# validate parameters
|
2844
2876
|
args = locals()
|
@@ -2945,8 +2977,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2945
2977
|
bat_power = 0.0
|
2946
2978
|
temperature = 30
|
2947
2979
|
bms_charge_current = 15
|
2948
|
-
charge_loss =
|
2949
|
-
discharge_loss =
|
2980
|
+
charge_loss = battery_params[2]['charge_loss']
|
2981
|
+
discharge_loss = battery_params[2]['discharge_loss']
|
2950
2982
|
bat_current = 0.0
|
2951
2983
|
device_power = 6.0
|
2952
2984
|
device_current = 35
|
@@ -2968,8 +3000,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2968
3000
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2969
3001
|
return None
|
2970
3002
|
bms_charge_current = battery.get('charge_rate')
|
2971
|
-
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else
|
2972
|
-
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else
|
3003
|
+
charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
|
3004
|
+
discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
|
2973
3005
|
device_power = device.get('power')
|
2974
3006
|
device_current = device.get('max_charge_current')
|
2975
3007
|
model = device.get('deviceType')
|
@@ -3141,7 +3173,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3141
3173
|
# produce time lines for charge, discharge and work mode
|
3142
3174
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
3143
3175
|
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
3144
|
-
work_mode_timed = strategy_timed(timed_mode,
|
3176
|
+
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
3145
3177
|
for i in range(0, len(work_mode_timed)):
|
3146
3178
|
# get work mode
|
3147
3179
|
work_mode = work_mode_timed[i]['mode']
|
@@ -3269,14 +3301,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3269
3301
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1, enable=update_settings)
|
3270
3302
|
if update_settings == 0:
|
3271
3303
|
output(f"\nNo changes made to charge settings")
|
3304
|
+
start_t = 0 # int(hour_now % 1 + 0.5) * steps_per_hour
|
3272
3305
|
if show_data > 0:
|
3273
3306
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3274
3307
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3275
3308
|
h = base_hour
|
3276
|
-
t =
|
3309
|
+
t = start_t
|
3277
3310
|
while t < len(time_line) and bat_timed[t] is not None:
|
3278
3311
|
col = h % data_wrap
|
3279
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3312
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3280
3313
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3281
3314
|
h += 1
|
3282
3315
|
t += steps_per_hour
|
@@ -3284,8 +3317,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3284
3317
|
if show_plot > 0:
|
3285
3318
|
print()
|
3286
3319
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3287
|
-
x_timed = [i for i in range(
|
3288
|
-
x_ticks = [i for i in range(
|
3320
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3321
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3289
3322
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3290
3323
|
if show_plot == 1:
|
3291
3324
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3395,14 +3428,15 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3395
3428
|
for v in plots.keys():
|
3396
3429
|
for i in range(0, run_time):
|
3397
3430
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3431
|
+
start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
|
3398
3432
|
if show_data > 0 and plots.get('SoC') is not None:
|
3399
3433
|
data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
|
3400
3434
|
s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
|
3401
3435
|
h = base_hour
|
3402
|
-
t =
|
3436
|
+
t = start_t
|
3403
3437
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3404
3438
|
col = h % data_wrap
|
3405
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3439
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3406
3440
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3407
3441
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3408
3442
|
h += 1
|
@@ -3411,8 +3445,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
|
|
3411
3445
|
if show_plot > 0:
|
3412
3446
|
print()
|
3413
3447
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3414
|
-
x_timed = [i for i in range(
|
3415
|
-
x_ticks = [i for i in range(
|
3448
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3449
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3416
3450
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3417
3451
|
if show_plot == 1:
|
3418
3452
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3555,11 +3589,11 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3555
3589
|
s +=f",{v:.0f}"
|
3556
3590
|
return s
|
3557
3591
|
output(f"Current SoC: {current_soc}%")
|
3558
|
-
output(f"Capacity: {capacity:.2f}kWh" + (" (
|
3559
|
-
output(f"Residual: {residual:.2f}kWh" + (" (
|
3592
|
+
output(f"Capacity: {capacity:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [1,3] else ""))
|
3593
|
+
output(f"Residual: {residual:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [2,3] else ""))
|
3560
3594
|
if rated_capacity is not None and bat_soh is not None:
|
3561
3595
|
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3562
|
-
output(f"SoH: {bat_soh:.1f}%" + (" (
|
3596
|
+
output(f"SoH: {bat_soh:.1f}%" + (" (calculated)" if not bat['soh_supported'] else ""))
|
3563
3597
|
output(f"InvBatVolt: {bat_volt:.1f}V")
|
3564
3598
|
output(f"InvBatCurrent: {bat_current:.1f}A")
|
3565
3599
|
output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
|
@@ -3968,7 +4002,6 @@ class Solcast :
|
|
3968
4002
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3969
4003
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3970
4004
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3971
|
-
self.data = {}
|
3972
4005
|
now = convert_date(d)
|
3973
4006
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3974
4007
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
@@ -3976,6 +4009,8 @@ class Solcast :
|
|
3976
4009
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
3977
4010
|
self.yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3978
4011
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
4012
|
+
self.data = {}
|
4013
|
+
self.rids = []
|
3979
4014
|
if reload == 1 and os.path.exists(storage + self.save):
|
3980
4015
|
os.remove(storage + self.save)
|
3981
4016
|
if self.save is not None and os.path.exists(storage + self.save):
|
@@ -3984,33 +4019,37 @@ class Solcast :
|
|
3984
4019
|
file.close()
|
3985
4020
|
if len(self.data) == 0:
|
3986
4021
|
print(f"No data in {self.save}")
|
3987
|
-
|
3988
|
-
self.
|
3989
|
-
|
3990
|
-
|
3991
|
-
|
4022
|
+
else:
|
4023
|
+
self.rids = self.data['forecasts'].keys() if self.data.get('forecasts') is not None else []
|
4024
|
+
if reload == 2 and self.data.get('date') is not None and self.data['date'] != self.today:
|
4025
|
+
self.data = {}
|
4026
|
+
elif debug_setting > 0 and not quiet:
|
4027
|
+
print(f"Using data for {self.data['date']} from {self.save}")
|
4028
|
+
if len(self.data) == 0:
|
3992
4029
|
if solcast_api_key is None or solcast_api_key == 'my.solcast_api_key>':
|
3993
4030
|
print(f"\nSolcast: solcast_api_key not set, exiting")
|
3994
4031
|
return
|
3995
4032
|
self.credentials = HTTPBasicAuth(solcast_api_key, '')
|
3996
|
-
if
|
3997
|
-
|
3998
|
-
|
3999
|
-
|
4000
|
-
|
4001
|
-
if response.status_code
|
4002
|
-
|
4003
|
-
|
4004
|
-
|
4005
|
-
|
4006
|
-
|
4033
|
+
if len(self.rids) == 0:
|
4034
|
+
if debug_setting > 1 and not quiet:
|
4035
|
+
print(f"Getting rids from solcast.com")
|
4036
|
+
params = {'format' : 'json'}
|
4037
|
+
response = requests.get(solcast_url + 'rooftop_sites', auth = self.credentials, params = params)
|
4038
|
+
if response.status_code != 200:
|
4039
|
+
if response.status_code == 429:
|
4040
|
+
print(f"\nSolcast API call limit reached for today")
|
4041
|
+
else:
|
4042
|
+
print(f"Solcast: response code getting rooftop_sites was {response.status_code}: {response.reason}")
|
4043
|
+
return
|
4044
|
+
sites = response.json().get('sites')
|
4045
|
+
self.rids = [s['resource_id'] for s in sites]
|
4007
4046
|
if debug_setting > 0 and not quiet:
|
4008
4047
|
print(f"Getting forecast for {self.today} from solcast.com")
|
4009
4048
|
self.data['date'] = self.today
|
4010
4049
|
params = {'format' : 'json', 'hours' : 168, 'period' : 'PT30M'} # always get 168 x 30 min values
|
4011
4050
|
for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
|
4012
4051
|
self.data[t] = {}
|
4013
|
-
for rid in
|
4052
|
+
for rid in self.rids:
|
4014
4053
|
response = requests.get(solcast_url + 'rooftop_sites/' + rid + '/' + t, auth = self.credentials, params = params)
|
4015
4054
|
if response.status_code != 200 :
|
4016
4055
|
if response.status_code == 429:
|
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: 01 November 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2024
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "2.6.
|
13
|
+
version = "2.6.6"
|
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
|
@@ -548,7 +548,7 @@ battery_settings = None
|
|
548
548
|
battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy' ]
|
549
549
|
battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
|
550
550
|
|
551
|
-
# 1 =
|
551
|
+
# 1 = Residual Energy, 2 = Residual Capacity (HV), 3 = Residual Capacity per battery (Mira)
|
552
552
|
residual_handling = 1
|
553
553
|
|
554
554
|
# charge rates based on residual_handling
|
@@ -558,13 +558,20 @@ 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
|
+
# HV BMS v2 with firmware 1.014 or later
|
563
564
|
2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
564
565
|
'step': 5,
|
565
566
|
'offset': 5,
|
566
|
-
'charge_loss': 1.
|
567
|
-
'discharge_loss': 0.
|
567
|
+
'charge_loss': 1.08,
|
568
|
+
'discharge_loss': 0.95},
|
569
|
+
# Mira BMS with firmware 1.014 or later
|
570
|
+
3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
|
571
|
+
'step': 5,
|
572
|
+
'offset': 5,
|
573
|
+
'charge_loss': 0.974,
|
574
|
+
'discharge_loss': 0.974},
|
568
575
|
}
|
569
576
|
|
570
577
|
def get_battery(info=0, v=None):
|
@@ -580,17 +587,28 @@ def get_battery(info=0, v=None):
|
|
580
587
|
for i in range(0, len(battery_vars)):
|
581
588
|
battery[battery_data[i]] = result[i].get('value')
|
582
589
|
battery['residual_handling'] = residual_handling
|
583
|
-
battery['rated_capacity'] = None
|
584
590
|
battery['soh'] = None
|
585
591
|
battery['soh_supported'] = False
|
586
592
|
if battery['residual_handling'] == 2:
|
587
593
|
capacity = battery.get('residual')
|
588
594
|
soc = battery.get('soc')
|
589
595
|
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
596
|
+
if battery.get('count') is None:
|
597
|
+
battery['count'] = int(battery['volt'] / 49)
|
598
|
+
if battery.get('ratedCapacity') is None:
|
599
|
+
battery['ratedCapacity'] = 2560 * battery['count']
|
600
|
+
elif battery['residual_handling'] == 3:
|
601
|
+
if battery.get('count') is None:
|
602
|
+
battery['count'] = int(battery['volt'] / 49)
|
603
|
+
capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
|
604
|
+
soc = battery.get('soc')
|
605
|
+
residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
|
606
|
+
if battery.get('ratedCapacity') is None:
|
607
|
+
battery['ratedCapacity'] = 2450 * battery['count']
|
590
608
|
else:
|
591
609
|
residual = battery.get('residual')
|
592
610
|
soc = battery.get('soc')
|
593
|
-
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
611
|
+
capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
|
594
612
|
battery['capacity'] = round(capacity, 3)
|
595
613
|
battery['residual'] = round(residual, 3)
|
596
614
|
battery['status'] = 1
|
@@ -598,6 +616,8 @@ def get_battery(info=0, v=None):
|
|
598
616
|
params = battery_params[battery['residual_handling']]
|
599
617
|
battery['charge_loss'] = params['charge_loss']
|
600
618
|
battery['discharge_loss'] = params['discharge_loss']
|
619
|
+
if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
|
620
|
+
battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
|
601
621
|
if battery.get('temperature') is not None:
|
602
622
|
battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
|
603
623
|
return battery
|
@@ -615,7 +635,7 @@ def get_batteries(info=0):
|
|
615
635
|
##################################################################################################
|
616
636
|
|
617
637
|
def get_charge():
|
618
|
-
global
|
638
|
+
global device_sn, battery_settings, debug_setting
|
619
639
|
if get_device() is None:
|
620
640
|
return None
|
621
641
|
if battery_settings is None:
|
@@ -647,7 +667,7 @@ def time_period(t, n):
|
|
647
667
|
return result
|
648
668
|
|
649
669
|
def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
|
650
|
-
global
|
670
|
+
global device_sn, battery_settings, debug_setting, time_period_vars
|
651
671
|
if get_device() is None:
|
652
672
|
return None
|
653
673
|
if battery_settings is None:
|
@@ -723,7 +743,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
723
743
|
##################################################################################################
|
724
744
|
|
725
745
|
def get_min():
|
726
|
-
global
|
746
|
+
global device_sn, battery_settings, debug_setting
|
727
747
|
if get_device() is None:
|
728
748
|
return None
|
729
749
|
if battery_settings is None:
|
@@ -747,7 +767,7 @@ def get_min():
|
|
747
767
|
##################################################################################################
|
748
768
|
|
749
769
|
def set_min(minSocOnGrid = None, minSoc = None, force = 0):
|
750
|
-
global
|
770
|
+
global device_sn, schedule, battery_settings, debug_setting
|
751
771
|
if get_device() is None:
|
752
772
|
return None
|
753
773
|
if schedule['enable'] == True:
|
@@ -801,160 +821,71 @@ def get_settings():
|
|
801
821
|
# get remote settings
|
802
822
|
##################################################################################################
|
803
823
|
|
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
|
-
}
|
824
|
+
# store for named settings info
|
825
|
+
named_settings = {}
|
833
826
|
|
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
|
827
|
+
def get_remote_settings(name):
|
828
|
+
global device_sn, debug_setting, messages, name_data
|
898
829
|
if get_device() is None:
|
899
830
|
return None
|
900
831
|
output(f"getting remote settings", 2)
|
901
|
-
if
|
832
|
+
if name is None:
|
902
833
|
return None
|
903
|
-
if type(
|
834
|
+
if type(name) is list:
|
904
835
|
values = {}
|
905
|
-
for
|
906
|
-
v = get_remote_settings(
|
836
|
+
for n in name:
|
837
|
+
v = get_remote_settings(n)
|
907
838
|
if v is None:
|
908
|
-
|
839
|
+
continue
|
909
840
|
for x in v.keys():
|
910
841
|
values[x] = v[x]
|
911
842
|
return values
|
912
|
-
|
913
|
-
response =
|
843
|
+
body = {'sn': device_sn, 'key': name}
|
844
|
+
response = signed_post(path="/op/v0/device/setting/get", body=body)
|
914
845
|
if response.status_code != 200:
|
915
846
|
output(f"** get_remote_settings() got response code {response.status_code}: {response.reason}")
|
916
847
|
return None
|
917
848
|
result = response.json().get('result')
|
918
849
|
if result is None:
|
919
850
|
errno = response.json().get('errno')
|
920
|
-
output(f"** get_remote_settings(), no result data, {errno_message(
|
851
|
+
output(f"** get_remote_settings(), no result data, {errno_message(response)}")
|
921
852
|
return None
|
922
|
-
|
923
|
-
|
924
|
-
|
853
|
+
named_settings[name] = result
|
854
|
+
value = result.get('value')
|
855
|
+
if value is None:
|
856
|
+
output(f"** get_remote_settings(), no value for {name}")
|
925
857
|
return None
|
926
|
-
return
|
858
|
+
return value
|
927
859
|
|
928
860
|
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")
|
861
|
+
return get_remote_settings(name)
|
862
|
+
|
863
|
+
def set_named_setting(name, value):
|
864
|
+
global device_sn, debug_setting
|
865
|
+
if get_device() is None:
|
937
866
|
return None
|
938
|
-
|
939
|
-
|
940
|
-
|
867
|
+
if type(name) is list:
|
868
|
+
count = 0
|
869
|
+
for (n, v) in name:
|
870
|
+
result = set_named_settings(name=n, value=v)
|
871
|
+
if result is not None:
|
872
|
+
count += 1
|
873
|
+
return count
|
874
|
+
output(f"\nSetting {name} to {value}", 1)
|
875
|
+
body = {'sn': device_sn, 'key': name, 'value': f"{value}"}
|
876
|
+
setting_delay()
|
877
|
+
response = signed_post(path="/op/v0/device/setting/set", body=body)
|
878
|
+
if response.status_code != 200:
|
879
|
+
output(f"** set_named_settings(): ({name}, {value}) got response code {response.status_code}: {response.reason}")
|
941
880
|
return None
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
881
|
+
errno = response.json().get('errno')
|
882
|
+
if errno != 0:
|
883
|
+
if errno == 44096:
|
884
|
+
output(f"** cannot update settings when schedule is active")
|
885
|
+
else:
|
886
|
+
output(f"** set_named_settings(): ({name}, {value}) {errno_message(response)}")
|
946
887
|
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
|
888
|
+
return 1
|
958
889
|
|
959
890
|
##################################################################################################
|
960
891
|
# wrappers for named settings
|
@@ -964,10 +895,10 @@ work_mode = None
|
|
964
895
|
|
965
896
|
def get_work_mode():
|
966
897
|
global work_mode
|
967
|
-
# print(f"** get_work_mode(): not available via Open API")
|
968
|
-
return None
|
969
898
|
if get_device() is None:
|
970
899
|
return None
|
900
|
+
# not implemented by Open API, skip to avoid error
|
901
|
+
return None
|
971
902
|
work_mode = get_named_settings('WorkMode')
|
972
903
|
return work_mode
|
973
904
|
|
@@ -983,6 +914,8 @@ temp_slots_per_battery = 8
|
|
983
914
|
|
984
915
|
def get_cell_temps(nbat=8):
|
985
916
|
global temp_slots_per_battery
|
917
|
+
print(f"** get_cell_temps(): not available via Open API")
|
918
|
+
return None
|
986
919
|
values = get_named_settings('BatteryTemp')
|
987
920
|
if values is None:
|
988
921
|
return None
|
@@ -1008,9 +941,7 @@ work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
|
|
1008
941
|
settable_modes = work_modes[:3]
|
1009
942
|
|
1010
943
|
def set_work_mode(mode, force = 0):
|
1011
|
-
global
|
1012
|
-
print(f"** set_work_mode(): not available via Open API")
|
1013
|
-
return None
|
944
|
+
global device_sn, work_modes, work_mode, debug_setting
|
1014
945
|
if get_device() is None:
|
1015
946
|
return None
|
1016
947
|
if mode not in settable_modes:
|
@@ -1022,7 +953,7 @@ def set_work_mode(mode, force = 0):
|
|
1022
953
|
return None
|
1023
954
|
set_schedule(enable=0)
|
1024
955
|
output(f"\nSetting work mode: {mode}", 1)
|
1025
|
-
body = {'sn': device_sn, 'key': '
|
956
|
+
body = {'sn': device_sn, 'key': 'WorkMode', 'value': mode}
|
1026
957
|
setting_delay()
|
1027
958
|
response = signed_post(path="/op/v0/device/setting/set", body=body)
|
1028
959
|
if response.status_code != 200:
|
@@ -1140,10 +1071,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1140
1071
|
price = segment.get('price')
|
1141
1072
|
start = time_hours(start)
|
1142
1073
|
# adjust exclusive time to inclusive
|
1143
|
-
end =
|
1074
|
+
end = time_hours(end)
|
1144
1075
|
if start is None or end is None or start >= end:
|
1145
1076
|
output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
|
1146
1077
|
return None
|
1078
|
+
end = round_time(end - 1/60)
|
1147
1079
|
mode = 'SelfUse' if mode is None else mode
|
1148
1080
|
if mode not in work_modes:
|
1149
1081
|
output(f"** mode must be one of {work_modes}")
|
@@ -1180,7 +1112,7 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1180
1112
|
|
1181
1113
|
# set a schedule from a period or list of time segment periods
|
1182
1114
|
def set_schedule(periods=None, enable=True):
|
1183
|
-
global
|
1115
|
+
global device_sn, debug_setting, schedule
|
1184
1116
|
if get_flag() is None:
|
1185
1117
|
return None
|
1186
1118
|
if schedule.get('support') == False:
|
@@ -1200,8 +1132,7 @@ def set_schedule(periods=None, enable=True):
|
|
1200
1132
|
periods = [periods]
|
1201
1133
|
if len(periods) > 8:
|
1202
1134
|
output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
|
1203
|
-
|
1204
|
-
body = {'deviceSN': device_sn, 'groups': periods}
|
1135
|
+
body = {'deviceSN': device_sn, 'groups': periods[-8:]}
|
1205
1136
|
setting_delay()
|
1206
1137
|
response = signed_post(path="/op/v0/device/scheduler/enable", body=body)
|
1207
1138
|
if response.status_code != 200:
|
@@ -1279,7 +1210,7 @@ def get_real(v = None):
|
|
1279
1210
|
# d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
|
1280
1211
|
# v = list of variables to get
|
1281
1212
|
# 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
|
1213
|
+
# save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
|
1283
1214
|
# load = "<file>": load the raw results from <file>
|
1284
1215
|
# plot = 0: no plot, 1: plot variables separately, 2: combine variables
|
1285
1216
|
##################################################################################################
|
@@ -1294,7 +1225,7 @@ sample_time = 5.0 # 5 minutes default
|
|
1294
1225
|
sample_rounding = 2 # round to 30 seconds
|
1295
1226
|
|
1296
1227
|
def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1297
|
-
global
|
1228
|
+
global device_sn, debug_setting, var_list, invert_ct2, tariff, max_power_kw, sample_rounding, sample_time, residual_scale, storage
|
1298
1229
|
if get_device() is None:
|
1299
1230
|
return None
|
1300
1231
|
time_span = time_span.lower()
|
@@ -1546,7 +1477,7 @@ def report_value_profile(result):
|
|
1546
1477
|
# d = day 'YYYY-MM-DD'
|
1547
1478
|
# v = list of report variables to get
|
1548
1479
|
# summary = 0, 1, 2: do a quick total energy report for a day
|
1549
|
-
# save = "xxxxx": save the report results to
|
1480
|
+
# save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
|
1550
1481
|
# load = "<file>": load the report results from <file>
|
1551
1482
|
# plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
|
1552
1483
|
##################################################################################################
|
@@ -1560,7 +1491,7 @@ fix_value_threshold = 200000000.0
|
|
1560
1491
|
fix_value_mask = 0x0000FFFF
|
1561
1492
|
|
1562
1493
|
def get_report(dimension='day', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1563
|
-
global
|
1494
|
+
global device_sn, var_list, debug_setting, report_vars, storage
|
1564
1495
|
if get_device() is None:
|
1565
1496
|
return None
|
1566
1497
|
# process list of days
|
@@ -2392,7 +2323,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2392
2323
|
elif type(strategy) is not list:
|
2393
2324
|
strategy = [strategy]
|
2394
2325
|
output(f"\nStrategy")
|
2395
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0
|
2326
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0) #, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2396
2327
|
output_close(plot=tariff_config['show_plot'])
|
2397
2328
|
if update == 1:
|
2398
2329
|
tariff = use
|
@@ -2469,15 +2400,15 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2469
2400
|
return profile[:run_time]
|
2470
2401
|
|
2471
2402
|
# build the timed work mode profile from the tariff strategy:
|
2472
|
-
def strategy_timed(timed_mode,
|
2403
|
+
def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, current_mode=None):
|
2473
2404
|
global tariff, steps_per_hour
|
2474
2405
|
work_mode_timed = []
|
2475
2406
|
min_soc_now = min_soc
|
2476
2407
|
max_soc_now = max_soc
|
2477
2408
|
current_mode = 'SelfUse' if current_mode is None else current_mode
|
2478
2409
|
strategy = get_strategy(timed_mode=timed_mode)
|
2479
|
-
h = base_hour
|
2480
2410
|
for i in range(0, run_time):
|
2411
|
+
h = time_line[i]
|
2481
2412
|
period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0, 'charge': 0.0,
|
2482
2413
|
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2483
2414
|
if strategy is not None:
|
@@ -2497,11 +2428,10 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2497
2428
|
period['fdpwr'] = d['fdpwr']
|
2498
2429
|
period['duration'] = duration_in(h, d) * steps_per_hour
|
2499
2430
|
work_mode_timed.append(period)
|
2500
|
-
h = round_time(h + 1 / steps_per_hour)
|
2501
2431
|
return work_mode_timed
|
2502
2432
|
|
2503
2433
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2504
|
-
#
|
2434
|
+
# all power values are as measured at the inverter battery connection
|
2505
2435
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2506
2436
|
global charge_config, steps_per_hour
|
2507
2437
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
@@ -2510,7 +2440,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2510
2440
|
discharge_loss = charge_config['discharge_loss']
|
2511
2441
|
charge_limit = charge_config['charge_limit']
|
2512
2442
|
float_charge = charge_config['float_charge']
|
2513
|
-
|
2443
|
+
run_time = len(work_mode_timed)
|
2444
|
+
for i in range(0, run_time):
|
2514
2445
|
w = work_mode_timed[i]
|
2515
2446
|
w['kwh'] = kwh_current
|
2516
2447
|
max_now = w['max_soc'] * capacity / 100
|
@@ -2521,6 +2452,7 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2521
2452
|
if kwh_current > capacity:
|
2522
2453
|
# battery is full
|
2523
2454
|
kwh_current = capacity
|
2455
|
+
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2524
2456
|
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2525
2457
|
reserve_now = capacity * min_soc_now / 100
|
2526
2458
|
if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
|
@@ -2550,7 +2482,7 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2550
2482
|
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2551
2483
|
next_period = work_mode_timed[t]
|
2552
2484
|
h = base_hour + t / steps_per_hour
|
2553
|
-
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
|
2485
|
+
if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold'] or period['min_soc'] != next_period['min_soc']:
|
2554
2486
|
s = {'start': start % 24, 'end': h % 24, 'mode': period['mode'], 'min_soc': period['min_soc']}
|
2555
2487
|
if period['mode'] == 'ForceDischarge':
|
2556
2488
|
s['fdsoc'] = period.get('fdsoc')
|
@@ -2642,7 +2574,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2642
2574
|
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2643
2575
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2644
2576
|
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,
|
2577
|
+
global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
|
2646
2578
|
print(f"\n---------------- charge_needed ----------------")
|
2647
2579
|
# validate parameters
|
2648
2580
|
args = locals()
|
@@ -2749,8 +2681,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2749
2681
|
bat_power = 0.0
|
2750
2682
|
temperature = 30
|
2751
2683
|
bms_charge_current = 15
|
2752
|
-
charge_loss =
|
2753
|
-
discharge_loss =
|
2684
|
+
charge_loss = battery_params[2]['charge_loss']
|
2685
|
+
discharge_loss = battery_params[2]['discharge_loss']
|
2754
2686
|
bat_current = 0.0
|
2755
2687
|
device_power = 6.0
|
2756
2688
|
device_current = 35
|
@@ -2772,8 +2704,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2772
2704
|
output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
|
2773
2705
|
return None
|
2774
2706
|
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
|
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
|
2777
2709
|
device_power = device.get('power')
|
2778
2710
|
device_current = device.get('max_charge_current')
|
2779
2711
|
model = device.get('deviceType')
|
@@ -2943,7 +2875,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2943
2875
|
# produce time lines for charge, discharge and work mode
|
2944
2876
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2945
2877
|
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
2946
|
-
work_mode_timed = strategy_timed(timed_mode,
|
2878
|
+
work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
|
2947
2879
|
for i in range(0, len(work_mode_timed)):
|
2948
2880
|
# get work mode
|
2949
2881
|
work_mode = work_mode_timed[i]['mode']
|
@@ -3071,14 +3003,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3071
3003
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1, enable=update_settings)
|
3072
3004
|
if update_settings == 0:
|
3073
3005
|
output(f"\nNo changes made to charge settings")
|
3006
|
+
start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
|
3074
3007
|
if show_data > 0:
|
3075
3008
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3076
3009
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3077
3010
|
h = base_hour
|
3078
|
-
t =
|
3011
|
+
t = start_t
|
3079
3012
|
while t < len(time_line) and bat_timed[t] is not None:
|
3080
3013
|
col = h % data_wrap
|
3081
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3014
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3082
3015
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3083
3016
|
h += 1
|
3084
3017
|
t += steps_per_hour
|
@@ -3086,8 +3019,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3086
3019
|
if show_plot > 0:
|
3087
3020
|
print()
|
3088
3021
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3089
|
-
x_timed = [i for i in range(
|
3090
|
-
x_ticks = [i for i in range(
|
3022
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3023
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3091
3024
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3092
3025
|
if show_plot == 1:
|
3093
3026
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3196,14 +3129,15 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3196
3129
|
for v in plots.keys():
|
3197
3130
|
for i in range(0, run_time):
|
3198
3131
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3132
|
+
start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
|
3199
3133
|
if show_data > 0 and plots.get('SoC') is not None:
|
3200
3134
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3201
3135
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3202
3136
|
h = base_hour
|
3203
|
-
t =
|
3137
|
+
t = start_t
|
3204
3138
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3205
3139
|
col = h % data_wrap
|
3206
|
-
s += f"\n {hours_time(time_line[t])}" if t ==
|
3140
|
+
s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
|
3207
3141
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3208
3142
|
h += 1
|
3209
3143
|
t += steps_per_hour
|
@@ -3211,8 +3145,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3211
3145
|
if show_plot > 0:
|
3212
3146
|
print()
|
3213
3147
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3214
|
-
x_timed = [i for i in range(
|
3215
|
-
x_ticks = [i for i in range(
|
3148
|
+
x_timed = [i for i in range(start_t, run_time)]
|
3149
|
+
x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
|
3216
3150
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3217
3151
|
if show_plot == 1:
|
3218
3152
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3354,8 +3288,8 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3354
3288
|
s +=f",{v:.0f}"
|
3355
3289
|
return s
|
3356
3290
|
output(f"Current SoC: {current_soc}%")
|
3357
|
-
output(f"Capacity: {capacity:.2f}kWh" + (" (
|
3358
|
-
output(f"Residual: {residual:.2f}kWh" + (" (
|
3291
|
+
output(f"Capacity: {capacity:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [1,3] else ""))
|
3292
|
+
output(f"Residual: {residual:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [2,3] else ""))
|
3359
3293
|
if rated_capacity is not None and bat_soh is not None:
|
3360
3294
|
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3361
3295
|
output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
|
@@ -3767,7 +3701,6 @@ class Solcast :
|
|
3767
3701
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3768
3702
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3769
3703
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3770
|
-
self.data = {}
|
3771
3704
|
now = convert_date(d)
|
3772
3705
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3773
3706
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
@@ -3775,6 +3708,8 @@ class Solcast :
|
|
3775
3708
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
3776
3709
|
self.yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3777
3710
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
3711
|
+
self.data = {}
|
3712
|
+
self.rids = []
|
3778
3713
|
if reload == 1 and os.path.exists(storage + self.save):
|
3779
3714
|
os.remove(storage + self.save)
|
3780
3715
|
if self.save is not None and os.path.exists(storage + self.save):
|
@@ -3783,33 +3718,37 @@ class Solcast :
|
|
3783
3718
|
file.close()
|
3784
3719
|
if len(self.data) == 0:
|
3785
3720
|
print(f"No data in {self.save}")
|
3786
|
-
|
3787
|
-
self.
|
3788
|
-
|
3789
|
-
|
3721
|
+
else:
|
3722
|
+
self.rids = self.data['forecasts'].keys() if self.data.get('forecasts') is not None else []
|
3723
|
+
if reload == 2 and self.data.get('date') is not None and self.data['date'] != self.today:
|
3724
|
+
self.data = {}
|
3725
|
+
elif debug_setting > 0 and not quiet:
|
3726
|
+
print(f"Using data for {self.data['date']} from {self.save}")
|
3790
3727
|
if len(self.data) == 0 :
|
3791
3728
|
if solcast_api_key is None or solcast_api_key == 'my.solcast_api_key>':
|
3792
3729
|
print(f"\nSolcast: solcast_api_key not set, exiting")
|
3793
3730
|
return
|
3794
3731
|
self.credentials = HTTPBasicAuth(solcast_api_key, '')
|
3795
|
-
if
|
3796
|
-
|
3797
|
-
|
3798
|
-
|
3799
|
-
|
3800
|
-
if response.status_code
|
3801
|
-
|
3802
|
-
|
3803
|
-
|
3804
|
-
|
3805
|
-
|
3732
|
+
if len(self.rids) == 0:
|
3733
|
+
if debug_setting > 1 and not quiet:
|
3734
|
+
print(f"Getting rids from solcast.com")
|
3735
|
+
params = {'format' : 'json'}
|
3736
|
+
response = requests.get(solcast_url + 'rooftop_sites', auth = self.credentials, params = params)
|
3737
|
+
if response.status_code != 200:
|
3738
|
+
if response.status_code == 429:
|
3739
|
+
print(f"\nSolcast API call limit reached for today")
|
3740
|
+
else:
|
3741
|
+
print(f"Solcast: response code getting rooftop_sites was {response.status_code}: {response.reason}")
|
3742
|
+
return
|
3743
|
+
sites = response.json().get('sites')
|
3744
|
+
self.rids = [s['resource_id'] for s in sites]
|
3806
3745
|
if debug_setting > 0 and not quiet:
|
3807
3746
|
print(f"Getting forecast for {self.today} from solcast.com")
|
3808
3747
|
self.data['date'] = self.today
|
3809
3748
|
params = {'format' : 'json', 'hours' : 168, 'period' : 'PT30M'} # always get 168 x 30 min values
|
3810
3749
|
for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
|
3811
3750
|
self.data[t] = {}
|
3812
|
-
for rid in
|
3751
|
+
for rid in self.rids:
|
3813
3752
|
response = requests.get(solcast_url + 'rooftop_sites/' + rid + '/' + t, auth = self.credentials, params = params)
|
3814
3753
|
if response.status_code != 200 :
|
3815
3754
|
if response.status_code == 429:
|
@@ -3818,7 +3757,7 @@ class Solcast :
|
|
3818
3757
|
print(f"Solcast: response code getting {t} was {response.status_code}: {response.reason}")
|
3819
3758
|
return
|
3820
3759
|
self.data[t][rid] = response.json().get(t)
|
3821
|
-
if self.save is not None
|
3760
|
+
if self.save is not None:
|
3822
3761
|
file = open(storage + self.save, 'w')
|
3823
3762
|
json.dump(self.data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3824
3763
|
file.close()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.6.
|
3
|
+
Version: 2.6.6
|
4
4
|
Summary: library for accessing Fox ESS cloud data using Open API
|
5
5
|
Author-email: Tony Matthews <tony@quasair.co.uk>
|
6
6
|
Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
|
@@ -111,6 +111,7 @@ f.get_charge()
|
|
111
111
|
f.get_min()
|
112
112
|
f.get_flag()
|
113
113
|
f.get_schedule()
|
114
|
+
f.get_named_settings(name)
|
114
115
|
|
115
116
|
```
|
116
117
|
Each of these calls will return a dictionary or list containing the relevant information.
|
@@ -130,6 +131,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
|
|
130
131
|
|
131
132
|
get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
|
132
133
|
|
134
|
+
get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
|
135
|
+
+ f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
|
136
|
+
+ the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
|
137
|
+
|
133
138
|
|
134
139
|
## Inverter Settings
|
135
140
|
You can change inverter settings using:
|
@@ -140,6 +145,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
|
|
140
145
|
f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
|
141
146
|
f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
|
142
147
|
f.set_schedule(periods, enable)
|
148
|
+
f.set_named_settings(name, value)
|
143
149
|
```
|
144
150
|
|
145
151
|
set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
|
@@ -180,6 +186,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
|
|
180
186
|
+ periods: a time segment or list of time segments created using f.set_period().
|
181
187
|
+ enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
|
182
188
|
|
189
|
+
set_named_settings() sets the 'name' setting to 'value'.
|
190
|
+
+ 'name' may also be a list of (name, value) pairs.
|
191
|
+
+ the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
|
192
|
+
|
183
193
|
|
184
194
|
## Real Time Data
|
185
195
|
Real time data reports the latest values for inverter variables, collected every 5 minutes:
|
@@ -787,6 +797,21 @@ This setting can be:
|
|
787
797
|
|
788
798
|
# Version Info
|
789
799
|
|
800
|
+
2.6.6<br>
|
801
|
+
Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
|
802
|
+
Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
|
803
|
+
Allow unlimited periods in strategy, including overlap with charge periods but warn and limit if the periods sent to inverter would be more than 8.
|
804
|
+
Improve behaviour prediction for schedules when clocks change due to day light saving.
|
805
|
+
Improve schedule generation and prediction when Min Soc changes.
|
806
|
+
Cache Solcast RIDS to reduce API usage (run with reload=1 if arrays are edited and cached RIDs need to be updated).
|
807
|
+
Remove spurious error message when (failing) to get inverter work mode.
|
808
|
+
|
809
|
+
2.6.5<br>
|
810
|
+
Add get_named_settings() and set_named_settings().
|
811
|
+
Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
|
812
|
+
Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
|
813
|
+
Update calibration of 'charge_loss' and 'discharge_loss'.
|
814
|
+
|
790
815
|
2.6.4<br>
|
791
816
|
Increase default plungs_slots from 6 to 8.
|
792
817
|
Correct battery capacity in get_batteries().
|
@@ -0,0 +1,7 @@
|
|
1
|
+
foxesscloud/foxesscloud.py,sha256=lmIa4uFTrwUSyRj2HWFMiuJ1UItOQzDfojqZQv_FjHY,219893
|
2
|
+
foxesscloud/openapi.py,sha256=V7vlbq_Cynde-BMf13UziuKfni8a8aX_lQej1fC6cO0,204609
|
3
|
+
foxesscloud-2.6.6.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
+
foxesscloud-2.6.6.dist-info/METADATA,sha256=2upWkpfGnqFrpRN3dU9Snbpj1hPHdiCenTrHqlD1L74,59461
|
5
|
+
foxesscloud-2.6.6.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
+
foxesscloud-2.6.6.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
+
foxesscloud-2.6.6.dist-info/RECORD,,
|
@@ -1,7 +0,0 @@
|
|
1
|
-
foxesscloud/foxesscloud.py,sha256=kfb3Ard_s3LZ1hN53RfwcRUyEeMGd6TviHtoFmQyYDY,217547
|
2
|
-
foxesscloud/openapi.py,sha256=jFWMAlbhHAFqlKZlg8rALgU-jziUdrTjM0b4I0eTyAQ,207518
|
3
|
-
foxesscloud-2.6.4.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
|
4
|
-
foxesscloud-2.6.4.dist-info/METADATA,sha256=uNjXzpjeHGt65jOvDoLApHmb8-awuWtRg9BRfJKnzFU,57854
|
5
|
-
foxesscloud-2.6.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
6
|
-
foxesscloud-2.6.4.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
|
7
|
-
foxesscloud-2.6.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|