foxesscloud 2.5.4__tar.gz → 2.5.6__tar.gz
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-2.5.4/src/foxesscloud.egg-info → foxesscloud-2.5.6}/PKG-INFO +11 -1
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/README.md +10 -0
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/pyproject.toml +1 -1
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/src/foxesscloud/foxesscloud.py +47 -41
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/src/foxesscloud/openapi.py +47 -41
- {foxesscloud-2.5.4 → foxesscloud-2.5.6/src/foxesscloud.egg-info}/PKG-INFO +11 -1
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/LICENCE +0 -0
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/setup.cfg +0 -0
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.5.4 → foxesscloud-2.5.6}/src/foxesscloud.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.5.
|
3
|
+
Version: 2.5.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
|
@@ -783,6 +783,16 @@ This setting can be:
|
|
783
783
|
|
784
784
|
# Version Info
|
785
785
|
|
786
|
+
2.5.6<br>
|
787
|
+
Change plunge slots to 8 and plungs pricing to [3,10].
|
788
|
+
Change min_hours setting in charge_needed to 0.5 (30 minutes) and round up charge times to increments of this.
|
789
|
+
Show data and plot starting at t=0.
|
790
|
+
|
791
|
+
2.5.5<br>
|
792
|
+
Improve validation of plunge price periods so they don't repeat across days.
|
793
|
+
Correct start and end soc times and values when charging using best Agile time periods.
|
794
|
+
Extend charge times when charge needed exceeds battery capacity.
|
795
|
+
|
786
796
|
2.5.4<br>
|
787
797
|
Remove preset 'weighting' that were not used.
|
788
798
|
Update weighting to apply the requested charge duration correctly.
|
@@ -769,6 +769,16 @@ This setting can be:
|
|
769
769
|
|
770
770
|
# Version Info
|
771
771
|
|
772
|
+
2.5.6<br>
|
773
|
+
Change plunge slots to 8 and plungs pricing to [3,10].
|
774
|
+
Change min_hours setting in charge_needed to 0.5 (30 minutes) and round up charge times to increments of this.
|
775
|
+
Show data and plot starting at t=0.
|
776
|
+
|
777
|
+
2.5.5<br>
|
778
|
+
Improve validation of plunge price periods so they don't repeat across days.
|
779
|
+
Correct start and end soc times and values when charging using best Agile time periods.
|
780
|
+
Extend charge times when charge needed exceeds battery capacity.
|
781
|
+
|
772
782
|
2.5.4<br>
|
773
783
|
Remove preset 'weighting' that were not used.
|
774
784
|
Update weighting to apply the requested charge duration correctly.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 27 September 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.6.
|
13
|
+
version = "1.6.8"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -721,7 +721,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
721
721
|
output(f"success", 2)
|
722
722
|
return battery_settings
|
723
723
|
|
724
|
-
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10,
|
724
|
+
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, end_soc=100, start_soc=10):
|
725
725
|
output(f"\nConfiguring schedule",1)
|
726
726
|
charge = []
|
727
727
|
st1 = time_hours(st1)
|
@@ -730,7 +730,7 @@ def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, target_so
|
|
730
730
|
en2 = time_hours(en2)
|
731
731
|
span = None
|
732
732
|
if st2 is not None and en2 is not None and st2 != en2:
|
733
|
-
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc':
|
733
|
+
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc': end_soc})
|
734
734
|
span = {'start': st2, 'end': en2}
|
735
735
|
if st1 is not None and en1 is not None and st1 != en1:
|
736
736
|
charge.append({'start': st1, 'end': en1, 'mode': 'SelfUse', 'min_soc': start_soc})
|
@@ -2221,6 +2221,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2221
2221
|
if use.get('agile') is not None and use['agile'].get('strategy') is not None:
|
2222
2222
|
base_time_adjust = hours_difference(base_time, use['agile'].get('base_time') )
|
2223
2223
|
for s in use['agile']['strategy']:
|
2224
|
+
s['valid_for'] = [int((s['hour'] - base_time_adjust) * steps_per_hour + i) for i in range(0, steps_per_hour // 2)] if s.get('hour') is not None else None
|
2224
2225
|
strategy.append(s)
|
2225
2226
|
if strategy is None or len(strategy) == 0:
|
2226
2227
|
return []
|
@@ -2230,7 +2231,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2230
2231
|
start = s['start']
|
2231
2232
|
end = s['end']
|
2232
2233
|
if hour_overlap(s, remove):
|
2233
|
-
output(f" {hours_time(start)}-{hours_time(end)}
|
2234
|
+
output(f" {hours_time(start)}-{hours_time(end)} was removed from strategy", 2)
|
2234
2235
|
continue
|
2235
2236
|
# add segment
|
2236
2237
|
min_soc_now = s['min_soc'] if s.get('min_soc') is not None and s['min_soc'] > 10 else 10
|
@@ -2239,9 +2240,9 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2239
2240
|
fdsoc = s.get('fdsoc')
|
2240
2241
|
fdpwr = s.get('fdpwr')
|
2241
2242
|
price = s.get('price')
|
2242
|
-
|
2243
|
+
valid_for = s.get('valid_for')
|
2243
2244
|
segment = {'start': start, 'end': end, 'mode': mode, 'min_soc': min_soc_now, 'max_soc': max_soc,
|
2244
|
-
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, '
|
2245
|
+
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, 'valid_for': valid_for}
|
2245
2246
|
if quiet == 0:
|
2246
2247
|
s = f" {hours_time(start)}-{hours_time(end)} {mode}, min_soc {min_soc_now}%"
|
2247
2248
|
s += f", max_soc {max_soc}%" if max_soc is not None else ""
|
@@ -2269,8 +2270,8 @@ tariff_config = {
|
|
2269
2270
|
'region': "H", # region code to use for Octopus API
|
2270
2271
|
'update_time': 16.5, # time in hours when tomrow's data can be fetched
|
2271
2272
|
'weighting': None, # weights for weighted average
|
2272
|
-
'plunge_price': [
|
2273
|
-
'plunge_slots':
|
2273
|
+
'plunge_price': [3, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2274
|
+
'plunge_slots': 8, # number of 30 minute slots to use
|
2274
2275
|
'data_wrap': 6, # prices to show per line
|
2275
2276
|
'show_data': 1, # show pricing data
|
2276
2277
|
'show_plot': 1 # plot pricing data
|
@@ -2317,21 +2318,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2317
2318
|
# extract times and prices. Times are Zulu (UTC)
|
2318
2319
|
prices = [] # ordered list of 30 minute prices
|
2319
2320
|
for i in range(0, len(results)):
|
2320
|
-
|
2321
|
+
hour = i / 2
|
2322
|
+
start = (now.hour + hour) % 24
|
2321
2323
|
time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
|
2322
2324
|
prices.append({
|
2323
2325
|
'start': start,
|
2324
2326
|
'end': round_time(start + 0.5),
|
2325
2327
|
'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
|
2326
2328
|
'price': results[i]['value_inc_vat'],
|
2327
|
-
'
|
2329
|
+
'hour': hour})
|
2328
2330
|
tariff['agile']['base_time'] = period_from.replace('T', ' ')
|
2329
2331
|
tariff['agile']['prices'] = prices
|
2330
2332
|
plunge = []
|
2331
2333
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2332
2334
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2333
2335
|
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
|
2334
|
-
for i in range(0,
|
2336
|
+
for i in range(0, len(prices)):
|
2335
2337
|
# hour relative index into list of plunge prices, starting at 7am
|
2336
2338
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
2337
2339
|
if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
|
@@ -2591,7 +2593,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2591
2593
|
if strategy is not None:
|
2592
2594
|
period['mode'] = 'SelfUse'
|
2593
2595
|
for d in strategy:
|
2594
|
-
if hour_in(h, d) and (d.get('
|
2596
|
+
if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
|
2595
2597
|
mode = d['mode']
|
2596
2598
|
period['mode'] = mode
|
2597
2599
|
min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
|
@@ -2679,7 +2681,7 @@ charge_config = {
|
|
2679
2681
|
'consumption_days': 3, # number of days to use for average consumption (1-7)
|
2680
2682
|
'consumption_span': 'week', # 'week' = last n days or 'weekday' = last n weekdays
|
2681
2683
|
'use_today': 21.0, # hour when todays consumption and generation can be used
|
2682
|
-
'min_hours': 0.
|
2684
|
+
'min_hours': 0.5, # minimum charge time in decimal hours
|
2683
2685
|
'min_kwh': 0.5, # minimum to add in kwh
|
2684
2686
|
'forecast_selection': 1, # 0 = use available forecast / generation, 1 only update settings with forecast
|
2685
2687
|
'annual_consumption': None, # optional annual consumption in kWh
|
@@ -2776,7 +2778,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2776
2778
|
for t in times:
|
2777
2779
|
if hour_in(hour_now, t) and update_settings > 0:
|
2778
2780
|
update_settings = 0
|
2779
|
-
output(f"\nSettings will not be updated during a charge period")
|
2781
|
+
output(f"\nSettings will not be updated during a charge period {format_period(t)}")
|
2780
2782
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2781
2783
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2782
2784
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -3058,18 +3060,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3058
3060
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
3059
3061
|
start_soc = int(start_residual / capacity * 100 + 0.5)
|
3060
3062
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
3061
|
-
target_soc = charge_config
|
3062
|
-
target_kwh = target_soc / 100 * capacity if target_soc is not None else 0
|
3063
|
+
target_soc = charge_config.get('target_soc')
|
3064
|
+
target_kwh = capacity if full_charge is not None or force_charge == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
3063
3065
|
if target_kwh > (end_residual + kwh_needed):
|
3064
3066
|
kwh_needed = target_kwh - end_residual
|
3065
|
-
elif full_charge is not None or force_charge == 2:
|
3066
|
-
kwh_needed = capacity - start_residual
|
3067
3067
|
elif test_charge is not None:
|
3068
3068
|
output(f"\nTest charge of {test_charge}kWh")
|
3069
3069
|
kwh_needed = test_charge
|
3070
3070
|
charge_message = "** test charge **"
|
3071
3071
|
# work out charge needed
|
3072
|
-
if kwh_min >
|
3072
|
+
if kwh_min > reserve and kwh_needed < charge_config['min_kwh'] and test_charge is None:
|
3073
3073
|
output(f"\nNo charging needed:")
|
3074
3074
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3075
3075
|
charge_message = "no charge needed"
|
@@ -3087,24 +3087,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3087
3087
|
output(f"\nCharge needed {kwh_needed:.2f}kWh:")
|
3088
3088
|
charge_message = "with charge added"
|
3089
3089
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
3090
|
-
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
3091
3090
|
# work out time to add kwh_needed to battery
|
3092
|
-
|
3093
|
-
hours =
|
3094
|
-
# charge time exceeded or charge needed exceeds capacity
|
3095
|
-
|
3096
|
-
|
3091
|
+
charge_rate = charge_power * charge_loss
|
3092
|
+
hours = kwh_needed / charge_rate
|
3093
|
+
# check if charge time exceeded or charge needed exceeds capacity
|
3094
|
+
hours_to_full = (capacity - start_residual) / charge_rate
|
3095
|
+
if hours > charge_time:
|
3097
3096
|
hours = charge_time
|
3098
|
-
elif hours
|
3099
|
-
|
3100
|
-
|
3097
|
+
elif hours > hours_to_full:
|
3098
|
+
kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
|
3099
|
+
required = hours_to_full + charge_time * kwh_shortfall / abs(start_residual - end_residual) # time to recover energy not added
|
3100
|
+
hours = required if required > hours and required < charge_time else charge_time
|
3101
|
+
# round charge time
|
3102
|
+
min_hours = charge_config['min_hours']
|
3103
|
+
hours = int(hours / min_hours + 0.99) * min_hours
|
3101
3104
|
# rework charge and discharge
|
3102
3105
|
charge_period = get_best_charge_period(start_at, hours)
|
3103
3106
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
3104
3107
|
price = charge_period.get('price') if charge_period is not None else None
|
3105
3108
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3106
3109
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
3107
|
-
|
3110
|
+
start_residual = interpolate(start_timed, bat_timed)
|
3111
|
+
end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
|
3112
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
3113
|
+
output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}" + (f" at {price:5.2f}p/kWh" if price is not None else ""))
|
3108
3114
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
3109
3115
|
j = i + 1
|
3110
3116
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3136,11 +3142,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3136
3142
|
if show_data > 0:
|
3137
3143
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3138
3144
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3139
|
-
h = base_hour
|
3140
|
-
t =
|
3145
|
+
h = base_hour
|
3146
|
+
t = 0
|
3141
3147
|
while t < len(time_line) and bat_timed[t] is not None:
|
3142
3148
|
col = h % data_wrap
|
3143
|
-
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t ==
|
3149
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == 0 or col == 0 else ""
|
3144
3150
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3145
3151
|
h += 1
|
3146
3152
|
t += steps_per_hour
|
@@ -3148,8 +3154,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3148
3154
|
if show_plot > 0:
|
3149
3155
|
print()
|
3150
3156
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3151
|
-
x_timed = [i for i in range(
|
3152
|
-
x_ticks = [i for i in range(
|
3157
|
+
x_timed = [i for i in range(0, run_time)]
|
3158
|
+
x_ticks = [i for i in range(0, run_time, steps_per_hour)]
|
3153
3159
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3154
3160
|
if show_plot == 1:
|
3155
3161
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3196,7 +3202,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3196
3202
|
end1 = start1 if force_charge == 0 else start2
|
3197
3203
|
end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
|
3198
3204
|
if timed_mode > 1:
|
3199
|
-
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc,
|
3205
|
+
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
|
3200
3206
|
set_schedule(periods = periods)
|
3201
3207
|
else:
|
3202
3208
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
|
@@ -3273,11 +3279,11 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3273
3279
|
if show_data > 0 and plots.get('SoC') is not None:
|
3274
3280
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3275
3281
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3276
|
-
h = base_hour
|
3277
|
-
t =
|
3282
|
+
h = base_hour
|
3283
|
+
t = 0
|
3278
3284
|
while t < len(time_line) and bat_timed[t] is not None:
|
3279
3285
|
col = h % data_wrap
|
3280
|
-
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t ==
|
3286
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == 0 or col == 0 else ""
|
3281
3287
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3282
3288
|
h += 1
|
3283
3289
|
t += steps_per_hour
|
@@ -3285,8 +3291,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3285
3291
|
if show_plot > 0:
|
3286
3292
|
print()
|
3287
3293
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3288
|
-
x_timed = [i for i in range(
|
3289
|
-
x_ticks = [i for i in range(
|
3294
|
+
x_timed = [i for i in range(0, run_time)]
|
3295
|
+
x_ticks = [i for i in range(0, run_time, steps_per_hour)]
|
3290
3296
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3291
3297
|
if show_plot == 1:
|
3292
3298
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
4
|
-
Updated:
|
4
|
+
Updated: 27 September 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.5.
|
13
|
+
version = "2.5.6"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -669,7 +669,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
|
|
669
669
|
output(f"success", 2)
|
670
670
|
return battery_settings
|
671
671
|
|
672
|
-
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10,
|
672
|
+
def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, end_soc=100, start_soc=10):
|
673
673
|
output(f"\nConfiguring schedule",1)
|
674
674
|
charge = []
|
675
675
|
st1 = time_hours(st1)
|
@@ -678,7 +678,7 @@ def charge_periods(st1=None, en1=None, st2=None, en2=None, min_soc=10, target_so
|
|
678
678
|
en2 = time_hours(en2)
|
679
679
|
span = None
|
680
680
|
if st2 is not None and en2 is not None and st2 != en2:
|
681
|
-
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc':
|
681
|
+
charge.append({'start': st2, 'end': en2, 'mode': 'ForceCharge', 'min_soc': min_soc, 'max_soc': end_soc})
|
682
682
|
span = {'start': st2, 'end': en2}
|
683
683
|
if st1 is not None and en1 is not None and st1 != en1:
|
684
684
|
charge.append({'start': st1, 'end': en1, 'mode': 'SelfUse', 'min_soc': start_soc})
|
@@ -2085,6 +2085,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2085
2085
|
if use.get('agile') is not None and use['agile'].get('strategy') is not None:
|
2086
2086
|
base_time_adjust = hours_difference(base_time, use['agile'].get('base_time') )
|
2087
2087
|
for s in use['agile']['strategy']:
|
2088
|
+
s['valid_for'] = [int((s['hour'] - base_time_adjust) * steps_per_hour + i) for i in range(0, steps_per_hour // 2)] if s.get('hour') is not None else None
|
2088
2089
|
strategy.append(s)
|
2089
2090
|
if strategy is None or len(strategy) == 0:
|
2090
2091
|
return []
|
@@ -2094,7 +2095,7 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2094
2095
|
start = s['start']
|
2095
2096
|
end = s['end']
|
2096
2097
|
if hour_overlap(s, remove):
|
2097
|
-
output(f" {hours_time(start)}-{hours_time(end)}
|
2098
|
+
output(f" {hours_time(start)}-{hours_time(end)} was removed from strategy", 2)
|
2098
2099
|
continue
|
2099
2100
|
# add segment
|
2100
2101
|
min_soc_now = s['min_soc'] if s.get('min_soc') is not None and s['min_soc'] > 10 else 10
|
@@ -2103,9 +2104,9 @@ def get_strategy(use=None, strategy=None, quiet=1, remove=None, reserve=0):
|
|
2103
2104
|
fdsoc = s.get('fdsoc')
|
2104
2105
|
fdpwr = s.get('fdpwr')
|
2105
2106
|
price = s.get('price')
|
2106
|
-
|
2107
|
+
valid_for = s.get('valid_for')
|
2107
2108
|
segment = {'start': start, 'end': end, 'mode': mode, 'min_soc': min_soc_now, 'max_soc': max_soc,
|
2108
|
-
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, '
|
2109
|
+
'fdsoc': fdsoc, 'fdpwr': fdpwr, 'price': price, 'valid_for': valid_for}
|
2109
2110
|
if quiet == 0:
|
2110
2111
|
s = f" {hours_time(start)}-{hours_time(end)} {mode}, min_soc {min_soc_now}%"
|
2111
2112
|
s += f", max_soc {max_soc}%" if max_soc is not None else ""
|
@@ -2133,8 +2134,8 @@ tariff_config = {
|
|
2133
2134
|
'region': "H", # region code to use for Octopus API
|
2134
2135
|
'update_time': 16.5, # time in hours when tomrow's data can be fetched
|
2135
2136
|
'weighting': None, # weights for weighted average
|
2136
|
-
'plunge_price': [
|
2137
|
-
'plunge_slots':
|
2137
|
+
'plunge_price': [3, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
|
2138
|
+
'plunge_slots': 8, # number of 30 minute slots to use
|
2138
2139
|
'data_wrap': 6, # prices to show per line
|
2139
2140
|
'show_data': 1, # show pricing data
|
2140
2141
|
'show_plot': 1 # plot pricing data
|
@@ -2181,21 +2182,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2181
2182
|
# extract times and prices. Times are Zulu (UTC)
|
2182
2183
|
prices = [] # ordered list of 30 minute prices
|
2183
2184
|
for i in range(0, len(results)):
|
2184
|
-
|
2185
|
+
hour = i / 2
|
2186
|
+
start = (now.hour + hour) % 24
|
2185
2187
|
time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
|
2186
2188
|
prices.append({
|
2187
2189
|
'start': start,
|
2188
2190
|
'end': round_time(start + 0.5),
|
2189
2191
|
'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
|
2190
2192
|
'price': results[i]['value_inc_vat'],
|
2191
|
-
'
|
2193
|
+
'hour': hour})
|
2192
2194
|
tariff['agile']['base_time'] = period_from.replace('T', ' ')
|
2193
2195
|
tariff['agile']['prices'] = prices
|
2194
2196
|
plunge = []
|
2195
2197
|
plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
|
2196
2198
|
plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
|
2197
2199
|
plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
|
2198
|
-
for i in range(0,
|
2200
|
+
for i in range(0, len(prices)):
|
2199
2201
|
# hour relative index into list of plunge prices, starting at 7am
|
2200
2202
|
x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
|
2201
2203
|
if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
|
@@ -2455,7 +2457,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
|
|
2455
2457
|
if strategy is not None:
|
2456
2458
|
period['mode'] = 'SelfUse'
|
2457
2459
|
for d in strategy:
|
2458
|
-
if hour_in(h, d) and (d.get('
|
2460
|
+
if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
|
2459
2461
|
mode = d['mode']
|
2460
2462
|
period['mode'] = mode
|
2461
2463
|
min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
|
@@ -2543,7 +2545,7 @@ charge_config = {
|
|
2543
2545
|
'consumption_days': 3, # number of days to use for average consumption (1-7)
|
2544
2546
|
'consumption_span': 'week', # 'week' = last n days or 'weekday' = last n weekdays
|
2545
2547
|
'use_today': 21.0, # hour when todays consumption and generation can be used
|
2546
|
-
'min_hours': 0.
|
2548
|
+
'min_hours': 0.5, # minimum charge time in decimal hours
|
2547
2549
|
'min_kwh': 0.5, # minimum to add in kwh
|
2548
2550
|
'forecast_selection': 1, # 0 = use available forecast / generation, 1 only update settings with forecast
|
2549
2551
|
'annual_consumption': None, # optional annual consumption in kWh
|
@@ -2640,7 +2642,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2640
2642
|
for t in times:
|
2641
2643
|
if hour_in(hour_now, t) and update_settings > 0:
|
2642
2644
|
update_settings = 0
|
2643
|
-
output(f"\nSettings will not be updated during a charge period")
|
2645
|
+
output(f"\nSettings will not be updated during a charge period {format_period(t)}")
|
2644
2646
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2645
2647
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2646
2648
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -2922,18 +2924,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2922
2924
|
start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
|
2923
2925
|
start_soc = int(start_residual / capacity * 100 + 0.5)
|
2924
2926
|
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
|
2925
|
-
target_soc = charge_config
|
2926
|
-
target_kwh = target_soc / 100 * capacity if target_soc is not None else 0
|
2927
|
+
target_soc = charge_config.get('target_soc')
|
2928
|
+
target_kwh = capacity if full_charge is not None or force_charge == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
|
2927
2929
|
if target_kwh > (end_residual + kwh_needed):
|
2928
2930
|
kwh_needed = target_kwh - end_residual
|
2929
|
-
elif full_charge is not None or force_charge == 2:
|
2930
|
-
kwh_needed = capacity - start_residual
|
2931
2931
|
elif test_charge is not None:
|
2932
2932
|
output(f"\nTest charge of {test_charge}kWh")
|
2933
2933
|
kwh_needed = test_charge
|
2934
2934
|
charge_message = "** test charge **"
|
2935
2935
|
# work out charge needed
|
2936
|
-
if kwh_min >
|
2936
|
+
if kwh_min > reserve and kwh_needed < charge_config['min_kwh'] and test_charge is None:
|
2937
2937
|
output(f"\nNo charging needed:")
|
2938
2938
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2939
2939
|
charge_message = "no charge needed"
|
@@ -2951,24 +2951,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2951
2951
|
output(f"\nCharge needed {kwh_needed:.2f}kWh:")
|
2952
2952
|
charge_message = "with charge added"
|
2953
2953
|
output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
|
2954
|
-
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
2955
2954
|
# work out time to add kwh_needed to battery
|
2956
|
-
|
2957
|
-
hours =
|
2958
|
-
# charge time exceeded or charge needed exceeds capacity
|
2959
|
-
|
2960
|
-
|
2955
|
+
charge_rate = charge_power * charge_loss
|
2956
|
+
hours = kwh_needed / charge_rate
|
2957
|
+
# check if charge time exceeded or charge needed exceeds capacity
|
2958
|
+
hours_to_full = (capacity - start_residual) / charge_rate
|
2959
|
+
if hours > charge_time:
|
2961
2960
|
hours = charge_time
|
2962
|
-
elif hours
|
2963
|
-
|
2964
|
-
|
2961
|
+
elif hours > hours_to_full:
|
2962
|
+
kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
|
2963
|
+
required = hours_to_full + charge_time * kwh_shortfall / abs(start_residual - end_residual) # hold time to recover energy not added
|
2964
|
+
hours = required if required > hours and required < charge_time else charge_time
|
2965
|
+
# round charge time
|
2966
|
+
min_hours = charge_config['min_hours']
|
2967
|
+
hours = int(hours / min_hours + 0.99) * min_hours
|
2965
2968
|
# rework charge and discharge
|
2966
2969
|
charge_period = get_best_charge_period(start_at, hours)
|
2967
2970
|
charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
|
2968
2971
|
price = charge_period.get('price') if charge_period is not None else None
|
2969
2972
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
2970
2973
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
2971
|
-
|
2974
|
+
start_residual = interpolate(start_timed, bat_timed)
|
2975
|
+
end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
|
2976
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
|
2977
|
+
output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}" + (f" at {price:5.2f}p/kWh" if price is not None else ""))
|
2972
2978
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
2973
2979
|
j = i + 1
|
2974
2980
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3000,11 +3006,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3000
3006
|
if show_data > 0:
|
3001
3007
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3002
3008
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3003
|
-
h = base_hour
|
3004
|
-
t =
|
3009
|
+
h = base_hour
|
3010
|
+
t = 0
|
3005
3011
|
while t < len(time_line) and bat_timed[t] is not None:
|
3006
3012
|
col = h % data_wrap
|
3007
|
-
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t ==
|
3013
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == 0 or col == 0 else ""
|
3008
3014
|
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
|
3009
3015
|
h += 1
|
3010
3016
|
t += steps_per_hour
|
@@ -3012,8 +3018,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3012
3018
|
if show_plot > 0:
|
3013
3019
|
print()
|
3014
3020
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3015
|
-
x_timed = [i for i in range(
|
3016
|
-
x_ticks = [i for i in range(
|
3021
|
+
x_timed = [i for i in range(0, run_time)]
|
3022
|
+
x_ticks = [i for i in range(0, run_time, steps_per_hour)]
|
3017
3023
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3018
3024
|
if show_plot == 1:
|
3019
3025
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -3060,7 +3066,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3060
3066
|
end1 = start1 if force_charge == 0 else start2
|
3061
3067
|
end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
|
3062
3068
|
if timed_mode > 1:
|
3063
|
-
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc,
|
3069
|
+
periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
|
3064
3070
|
set_schedule(periods = periods)
|
3065
3071
|
else:
|
3066
3072
|
set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
|
@@ -3136,11 +3142,11 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3136
3142
|
if show_data > 0 and plots.get('SoC') is not None:
|
3137
3143
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3138
3144
|
s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
|
3139
|
-
h = base_hour
|
3140
|
-
t =
|
3145
|
+
h = base_hour
|
3146
|
+
t = 0
|
3141
3147
|
while t < len(time_line) and bat_timed[t] is not None:
|
3142
3148
|
col = h % data_wrap
|
3143
|
-
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t ==
|
3149
|
+
s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == 0 or col == 0 else ""
|
3144
3150
|
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
|
3145
3151
|
h += 1
|
3146
3152
|
t += steps_per_hour
|
@@ -3148,8 +3154,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3148
3154
|
if show_plot > 0:
|
3149
3155
|
print()
|
3150
3156
|
plt.figure(figsize=(figure_width, figure_width/2))
|
3151
|
-
x_timed = [i for i in range(
|
3152
|
-
x_ticks = [i for i in range(
|
3157
|
+
x_timed = [i for i in range(0, run_time)]
|
3158
|
+
x_ticks = [i for i in range(0, run_time, steps_per_hour)]
|
3153
3159
|
plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
|
3154
3160
|
if show_plot == 1:
|
3155
3161
|
title = f"Predicted Battery SoC % at {base_time}({charge_message})"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: foxesscloud
|
3
|
-
Version: 2.5.
|
3
|
+
Version: 2.5.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
|
@@ -783,6 +783,16 @@ This setting can be:
|
|
783
783
|
|
784
784
|
# Version Info
|
785
785
|
|
786
|
+
2.5.6<br>
|
787
|
+
Change plunge slots to 8 and plungs pricing to [3,10].
|
788
|
+
Change min_hours setting in charge_needed to 0.5 (30 minutes) and round up charge times to increments of this.
|
789
|
+
Show data and plot starting at t=0.
|
790
|
+
|
791
|
+
2.5.5<br>
|
792
|
+
Improve validation of plunge price periods so they don't repeat across days.
|
793
|
+
Correct start and end soc times and values when charging using best Agile time periods.
|
794
|
+
Extend charge times when charge needed exceeds battery capacity.
|
795
|
+
|
786
796
|
2.5.4<br>
|
787
797
|
Remove preset 'weighting' that were not used.
|
788
798
|
Update weighting to apply the requested charge duration correctly.
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|