foxesscloud 2.6.5__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 +85 -46
- foxesscloud/openapi.py +75 -45
- {foxesscloud-2.6.5.dist-info → foxesscloud-2.6.6.dist-info}/METADATA +10 -1
- foxesscloud-2.6.6.dist-info/RECORD +7 -0
- foxesscloud-2.6.5.dist-info/RECORD +0 -7
- {foxesscloud-2.6.5.dist-info → foxesscloud-2.6.6.dist-info}/LICENCE +0 -0
- {foxesscloud-2.6.5.dist-info → foxesscloud-2.6.6.dist-info}/WHEEL +0 -0
- {foxesscloud-2.6.5.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
|
@@ -595,11 +595,18 @@ battery_params = {
|
|
595
595
|
'offset': 5,
|
596
596
|
'charge_loss': 0.974,
|
597
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
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']]
|
@@ -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:
|
@@ -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')
|
@@ -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
|
@@ -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
|
@@ -560,11 +560,18 @@ battery_params = {
|
|
560
560
|
'offset': 5,
|
561
561
|
'charge_loss': 0.974,
|
562
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
|
+
'charge_loss': 1.08,
|
567
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
|
@@ -877,6 +897,8 @@ def get_work_mode():
|
|
877
897
|
global work_mode
|
878
898
|
if get_device() is None:
|
879
899
|
return None
|
900
|
+
# not implemented by Open API, skip to avoid error
|
901
|
+
return None
|
880
902
|
work_mode = get_named_settings('WorkMode')
|
881
903
|
return work_mode
|
882
904
|
|
@@ -1049,10 +1071,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
|
|
1049
1071
|
price = segment.get('price')
|
1050
1072
|
start = time_hours(start)
|
1051
1073
|
# adjust exclusive time to inclusive
|
1052
|
-
end =
|
1074
|
+
end = time_hours(end)
|
1053
1075
|
if start is None or end is None or start >= end:
|
1054
1076
|
output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
|
1055
1077
|
return None
|
1078
|
+
end = round_time(end - 1/60)
|
1056
1079
|
mode = 'SelfUse' if mode is None else mode
|
1057
1080
|
if mode not in work_modes:
|
1058
1081
|
output(f"** mode must be one of {work_modes}")
|
@@ -1109,8 +1132,7 @@ def set_schedule(periods=None, enable=True):
|
|
1109
1132
|
periods = [periods]
|
1110
1133
|
if len(periods) > 8:
|
1111
1134
|
output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
|
1112
|
-
|
1113
|
-
body = {'deviceSN': device_sn, 'groups': periods}
|
1135
|
+
body = {'deviceSN': device_sn, 'groups': periods[-8:]}
|
1114
1136
|
setting_delay()
|
1115
1137
|
response = signed_post(path="/op/v0/device/scheduler/enable", body=body)
|
1116
1138
|
if response.status_code != 200:
|
@@ -2301,7 +2323,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2301
2323
|
elif type(strategy) is not list:
|
2302
2324
|
strategy = [strategy]
|
2303
2325
|
output(f"\nStrategy")
|
2304
|
-
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')])
|
2305
2327
|
output_close(plot=tariff_config['show_plot'])
|
2306
2328
|
if update == 1:
|
2307
2329
|
tariff = use
|
@@ -2378,15 +2400,15 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2378
2400
|
return profile[:run_time]
|
2379
2401
|
|
2380
2402
|
# build the timed work mode profile from the tariff strategy:
|
2381
|
-
def strategy_timed(timed_mode,
|
2403
|
+
def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, current_mode=None):
|
2382
2404
|
global tariff, steps_per_hour
|
2383
2405
|
work_mode_timed = []
|
2384
2406
|
min_soc_now = min_soc
|
2385
2407
|
max_soc_now = max_soc
|
2386
2408
|
current_mode = 'SelfUse' if current_mode is None else current_mode
|
2387
2409
|
strategy = get_strategy(timed_mode=timed_mode)
|
2388
|
-
h = base_hour
|
2389
2410
|
for i in range(0, run_time):
|
2411
|
+
h = time_line[i]
|
2390
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,
|
2391
2413
|
'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
|
2392
2414
|
if strategy is not None:
|
@@ -2406,11 +2428,10 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2406
2428
|
period['fdpwr'] = d['fdpwr']
|
2407
2429
|
period['duration'] = duration_in(h, d) * steps_per_hour
|
2408
2430
|
work_mode_timed.append(period)
|
2409
|
-
h = round_time(h + 1 / steps_per_hour)
|
2410
2431
|
return work_mode_timed
|
2411
2432
|
|
2412
2433
|
# build the timed battery residual from the charge / discharge, work mode and min_soc
|
2413
|
-
#
|
2434
|
+
# all power values are as measured at the inverter battery connection
|
2414
2435
|
def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
|
2415
2436
|
global charge_config, steps_per_hour
|
2416
2437
|
allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
|
@@ -2419,7 +2440,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2419
2440
|
discharge_loss = charge_config['discharge_loss']
|
2420
2441
|
charge_limit = charge_config['charge_limit']
|
2421
2442
|
float_charge = charge_config['float_charge']
|
2422
|
-
|
2443
|
+
run_time = len(work_mode_timed)
|
2444
|
+
for i in range(0, run_time):
|
2423
2445
|
w = work_mode_timed[i]
|
2424
2446
|
w['kwh'] = kwh_current
|
2425
2447
|
max_now = w['max_soc'] * capacity / 100
|
@@ -2430,6 +2452,7 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
|
|
2430
2452
|
if kwh_current > capacity:
|
2431
2453
|
# battery is full
|
2432
2454
|
kwh_current = capacity
|
2455
|
+
w = work_mode_timed[i+1] if (i + 1) < run_time else w
|
2433
2456
|
min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
|
2434
2457
|
reserve_now = capacity * min_soc_now / 100
|
2435
2458
|
if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
|
@@ -2459,7 +2482,7 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
|
|
2459
2482
|
period = times[0] if len(times) > 0 else work_mode_timed[0]
|
2460
2483
|
next_period = work_mode_timed[t]
|
2461
2484
|
h = base_hour + t / steps_per_hour
|
2462
|
-
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']:
|
2463
2486
|
s = {'start': start % 24, 'end': h % 24, 'mode': period['mode'], 'min_soc': period['min_soc']}
|
2464
2487
|
if period['mode'] == 'ForceDischarge':
|
2465
2488
|
s['fdsoc'] = period.get('fdsoc')
|
@@ -2852,7 +2875,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2852
2875
|
# produce time lines for charge, discharge and work mode
|
2853
2876
|
charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
|
2854
2877
|
discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
|
2855
|
-
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)
|
2856
2879
|
for i in range(0, len(work_mode_timed)):
|
2857
2880
|
# get work mode
|
2858
2881
|
work_mode = work_mode_timed[i]['mode']
|
@@ -2980,14 +3003,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2980
3003
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1, enable=update_settings)
|
2981
3004
|
if update_settings == 0:
|
2982
3005
|
output(f"\nNo changes made to charge settings")
|
3006
|
+
start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
|
2983
3007
|
if show_data > 0:
|
2984
3008
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
2985
3009
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
2986
3010
|
h = base_hour
|
2987
|
-
t =
|
3011
|
+
t = start_t
|
2988
3012
|
while t < len(time_line) and bat_timed[t] is not None:
|
2989
3013
|
col = h % data_wrap
|
2990
|
-
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 ""
|
2991
3015
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
2992
3016
|
h += 1
|
2993
3017
|
t += steps_per_hour
|
@@ -2995,8 +3019,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2995
3019
|
if show_plot > 0:
|
2996
3020
|
print()
|
2997
3021
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2998
|
-
x_timed = [i for i in range(
|
2999
|
-
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)]
|
3000
3024
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3001
3025
|
if show_plot == 1:
|
3002
3026
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3105,14 +3129,15 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3105
3129
|
for v in plots.keys():
|
3106
3130
|
for i in range(0, run_time):
|
3107
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
|
3108
3133
|
if show_data > 0 and plots.get('SoC') is not None:
|
3109
3134
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3110
3135
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3111
3136
|
h = base_hour
|
3112
|
-
t =
|
3137
|
+
t = start_t
|
3113
3138
|
while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
|
3114
3139
|
col = h % data_wrap
|
3115
|
-
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 ""
|
3116
3141
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3117
3142
|
h += 1
|
3118
3143
|
t += steps_per_hour
|
@@ -3120,8 +3145,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3120
3145
|
if show_plot > 0:
|
3121
3146
|
print()
|
3122
3147
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3123
|
-
x_timed = [i for i in range(
|
3124
|
-
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)]
|
3125
3150
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3126
3151
|
if show_plot == 1:
|
3127
3152
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3263,8 +3288,8 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
|
|
3263
3288
|
s +=f",{v:.0f}"
|
3264
3289
|
return s
|
3265
3290
|
output(f"Current SoC: {current_soc}%")
|
3266
|
-
output(f"Capacity: {capacity:.2f}kWh" + (" (
|
3267
|
-
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 ""))
|
3268
3293
|
if rated_capacity is not None and bat_soh is not None:
|
3269
3294
|
output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
|
3270
3295
|
output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
|
@@ -3676,7 +3701,6 @@ class Solcast :
|
|
3676
3701
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3677
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
|
3678
3703
|
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3679
|
-
self.data = {}
|
3680
3704
|
now = convert_date(d)
|
3681
3705
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3682
3706
|
self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
|
@@ -3684,6 +3708,8 @@ class Solcast :
|
|
3684
3708
|
self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
|
3685
3709
|
self.yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
|
3686
3710
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
3711
|
+
self.data = {}
|
3712
|
+
self.rids = []
|
3687
3713
|
if reload == 1 and os.path.exists(storage + self.save):
|
3688
3714
|
os.remove(storage + self.save)
|
3689
3715
|
if self.save is not None and os.path.exists(storage + self.save):
|
@@ -3692,33 +3718,37 @@ class Solcast :
|
|
3692
3718
|
file.close()
|
3693
3719
|
if len(self.data) == 0:
|
3694
3720
|
print(f"No data in {self.save}")
|
3695
|
-
|
3696
|
-
self.
|
3697
|
-
|
3698
|
-
|
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}")
|
3699
3727
|
if len(self.data) == 0 :
|
3700
3728
|
if solcast_api_key is None or solcast_api_key == 'my.solcast_api_key>':
|
3701
3729
|
print(f"\nSolcast: solcast_api_key not set, exiting")
|
3702
3730
|
return
|
3703
3731
|
self.credentials = HTTPBasicAuth(solcast_api_key, '')
|
3704
|
-
if
|
3705
|
-
|
3706
|
-
|
3707
|
-
|
3708
|
-
|
3709
|
-
if response.status_code
|
3710
|
-
|
3711
|
-
|
3712
|
-
|
3713
|
-
|
3714
|
-
|
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]
|
3715
3745
|
if debug_setting > 0 and not quiet:
|
3716
3746
|
print(f"Getting forecast for {self.today} from solcast.com")
|
3717
3747
|
self.data['date'] = self.today
|
3718
3748
|
params = {'format' : 'json', 'hours' : 168, 'period' : 'PT30M'} # always get 168 x 30 min values
|
3719
3749
|
for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
|
3720
3750
|
self.data[t] = {}
|
3721
|
-
for rid in
|
3751
|
+
for rid in self.rids:
|
3722
3752
|
response = requests.get(solcast_url + 'rooftop_sites/' + rid + '/' + t, auth = self.credentials, params = params)
|
3723
3753
|
if response.status_code != 200 :
|
3724
3754
|
if response.status_code == 429:
|
@@ -3727,7 +3757,7 @@ class Solcast :
|
|
3727
3757
|
print(f"Solcast: response code getting {t} was {response.status_code}: {response.reason}")
|
3728
3758
|
return
|
3729
3759
|
self.data[t][rid] = response.json().get(t)
|
3730
|
-
if self.save is not None
|
3760
|
+
if self.save is not None:
|
3731
3761
|
file = open(storage + self.save, 'w')
|
3732
3762
|
json.dump(self.data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3733
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
|
@@ -797,6 +797,15 @@ This setting can be:
|
|
797
797
|
|
798
798
|
# Version Info
|
799
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
|
+
|
800
809
|
2.6.5<br>
|
801
810
|
Add get_named_settings() and set_named_settings().
|
802
811
|
Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
|
@@ -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=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,,
|
File without changes
|
File without changes
|
File without changes
|