foxesscloud 2.5.3__py3-none-any.whl → 2.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 23 September 2024
4
+ Updated: 26 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.5"
13
+ version = "1.6.7"
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, target_soc=100, start_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': target_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)} ** removed ** (overlaps charge period)", 2)
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
- expires = int((s['valid_to'] - base_time_adjust) * steps_per_hour) if s.get('valid_to') is not None else None
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, 'expires': expires}
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 ""
@@ -2264,11 +2265,6 @@ regions = {'A':'Eastern England', 'B':'East Midlands', 'C':'London', 'D':'Mersey
2264
2265
  'J':'South Eastern England', 'K':'Southern Wales', 'L':'South Western England', 'M':'Yorkshire', 'N':'Southern Scotland', 'P':'Northern Scotland'}
2265
2266
 
2266
2267
 
2267
- # preset weightings for average 30 minute pricing over charging duration:
2268
- front_loaded = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5] # 3 hour average, front loaded
2269
- first_hour = [1.0, 1.0] # lowest average price for first hour
2270
-
2271
-
2272
2268
  tariff_config = {
2273
2269
  'product': "AGILE-24-04-03", # product code to use for Octopus API
2274
2270
  'region': "H", # region code to use for Octopus API
@@ -2277,7 +2273,7 @@ tariff_config = {
2277
2273
  'plunge_price': [1, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
2278
2274
  'plunge_slots': 6, # number of 30 minute slots to use
2279
2275
  'data_wrap': 6, # prices to show per line
2280
- 'show_data': 0, # show pricing data
2276
+ 'show_data': 1, # show pricing data
2281
2277
  'show_plot': 1 # plot pricing data
2282
2278
  }
2283
2279
 
@@ -2322,21 +2318,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
2322
2318
  # extract times and prices. Times are Zulu (UTC)
2323
2319
  prices = [] # ordered list of 30 minute prices
2324
2320
  for i in range(0, len(results)):
2325
- start = (now.hour + i / 2) % 24
2321
+ hour = i / 2
2322
+ start = (now.hour + hour) % 24
2326
2323
  time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
2327
2324
  prices.append({
2328
2325
  'start': start,
2329
2326
  'end': round_time(start + 0.5),
2330
2327
  'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
2331
2328
  'price': results[i]['value_inc_vat'],
2332
- 'valid_to': i / 2 + 0.5})
2329
+ 'hour': hour})
2333
2330
  tariff['agile']['base_time'] = period_from.replace('T', ' ')
2334
2331
  tariff['agile']['prices'] = prices
2335
2332
  plunge = []
2336
2333
  plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
2337
2334
  plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
2338
2335
  plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
2339
- for i in range(0, min([48, len(prices)])):
2336
+ for i in range(0, len(prices)):
2340
2337
  # hour relative index into list of plunge prices, starting at 7am
2341
2338
  x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
2342
2339
  if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
@@ -2361,13 +2358,13 @@ def get_agile_times(tariff=agile_octopus, d=None):
2361
2358
  # show the results
2362
2359
  if tariff_config['show_data'] > 0:
2363
2360
  data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
2364
- t = (now.hour * 2) % data_wrap
2365
- s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
2361
+ col = (now.hour * 2) % data_wrap
2362
+ s = f"\nPrice p/kWh inc VAT on {today}:"
2366
2363
  for i in range(0, len(prices)):
2367
- s += "\n" if i > 0 and t % data_wrap == 0 else ""
2368
- s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
2369
- t += 1
2370
- output(s[:-1])
2364
+ s += (f"\n {prices[i]['time']} " + " " * col * 6) if i == 0 or col == 0 else ""
2365
+ s += f" {prices[i]['price']:4.1f}"
2366
+ col = (col + 1) % data_wrap
2367
+ output(s)
2371
2368
  if tariff_config['show_plot'] > 0:
2372
2369
  plt.figure(figsize=(figure_width, figure_width/2))
2373
2370
  x_timed = [i for i in range(0, len(prices))]
@@ -2394,7 +2391,8 @@ def get_best_charge_period(start, duration):
2394
2391
  key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
2395
2392
  key = key[0] if len(key) > 0 else None
2396
2393
  end = tariff[key]['end'] if key is not None else round_time(start + duration)
2397
- span = int(duration * 2 + 0.99)
2394
+ span = int(duration * 2 + 0.99) # number of slots needed for charging
2395
+ last = (duration * 2) % 1 # amount of last slot used for charging
2398
2396
  coverage = max([round_time(end - start), duration])
2399
2397
  period = {'start': start, 'end': round_time(start + coverage)}
2400
2398
  prices = tariff['agile']['prices']
@@ -2403,13 +2401,14 @@ def get_best_charge_period(start, duration):
2403
2401
  return None
2404
2402
  elif len(slots) == 1:
2405
2403
  best = slots
2406
- price = prices[slots[0]]['price']
2407
2404
  best_start = start
2405
+ price = prices[best[0]]['price']
2408
2406
  else:
2409
2407
  # best charge time for duration
2410
2408
  weighting = tariff_config.get('weighting')
2411
2409
  times = []
2412
- weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
2410
+ weights = ([1.0] * (span)) if weighting is None else (weighting + [1.0] * span)[:span]
2411
+ weights[-1] *= last if last > 0.0 else 1.0
2413
2412
  best = None
2414
2413
  price = None
2415
2414
  for i in range(0, len(slots) - span + 1):
@@ -2594,7 +2593,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
2594
2593
  if strategy is not None:
2595
2594
  period['mode'] = 'SelfUse'
2596
2595
  for d in strategy:
2597
- if hour_in(h, d) and (d.get('expires') is None or i < d['expires']):
2596
+ if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
2598
2597
  mode = d['mode']
2599
2598
  period['mode'] = mode
2600
2599
  min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
@@ -2964,7 +2963,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2964
2963
  solcast_value = fsolcast.daily[forecast_day]['kwh']
2965
2964
  solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
2966
2965
  solcast_from = time_hours(fsolcast.daily[today]['from']) if fsolcast.daily[today].get('from') is not None else 0
2967
- output(f"\nSolcast forecast for {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}") # get forecast.solar data and produce time line
2966
+ output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh") # get forecast.solar data and produce time line
2968
2967
  solar_value = None
2969
2968
  solar_profile = None
2970
2969
  if forecast is None and solar_arrays is not None and (system_time.hour in forecast_times or run_after == 0):
@@ -2972,7 +2971,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2972
2971
  if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
2973
2972
  solar_value = fsolar.daily[forecast_day]['kwh']
2974
2973
  solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
2975
- output(f"\nSolar forecast for {tomorrow}: {fsolar.daily[tomorrow]['kwh']:.1f}")
2974
+ output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
2976
2975
  if solcast_value is None and solar_value is None and debug_setting > 1:
2977
2976
  output(f"\nNo forecasts available at this time")
2978
2977
  # get generation data
@@ -3061,19 +3060,18 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3061
3060
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
3062
3061
  start_soc = int(start_residual / capacity * 100 + 0.5)
3063
3062
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
3064
- target_soc = charge_config['target_soc'] if charge_config.get('target_soc') is not None else None
3065
- 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
3066
3065
  if target_kwh > (end_residual + kwh_needed):
3067
3066
  kwh_needed = target_kwh - end_residual
3068
- elif full_charge is not None or force_charge == 2:
3069
- kwh_needed = capacity - start_residual
3070
3067
  elif test_charge is not None:
3071
3068
  output(f"\nTest charge of {test_charge}kWh")
3072
3069
  kwh_needed = test_charge
3073
3070
  charge_message = "** test charge **"
3074
3071
  # work out charge needed
3075
- if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh']:
3076
- output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
3072
+ if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and test_charge is None:
3073
+ output(f"\nNo charging needed:")
3074
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3077
3075
  charge_message = "no charge needed"
3078
3076
  kwh_needed = 0.0
3079
3077
  hours = 0.0
@@ -3086,26 +3084,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3086
3084
  work_mode_timed[t]['min_soc'] = start_soc
3087
3085
  else:
3088
3086
  if test_charge is None:
3089
- output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
3087
+ output(f"\nCharge needed {kwh_needed:.2f}kWh:")
3090
3088
  charge_message = "with charge added"
3091
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
3089
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3092
3090
  # work out time to add kwh_needed to battery
3093
- taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
3094
- hours = round_time(kwh_needed / (charge_power * charge_loss) + taper_time)
3095
- # charge time exceeded or charge needed exceeds capacity
3096
- if hours > charge_time or (start_residual + kwh_needed) > capacity:
3097
- kwh_needed = capacity - start_residual
3098
- hours = charge_time
3099
- elif hours < charge_config['min_hours']:
3091
+ charge_rate = charge_power * charge_loss
3092
+ hours = round_time(kwh_needed / charge_rate)
3093
+ # check if charge time exceeded or charge needed exceeds capacity
3094
+ hours_to_full = round_time((capacity - start_residual) / (charge_rate) + 10)
3095
+ if hours < charge_config['min_hours']:
3100
3096
  hours = charge_config['min_hours']
3101
- end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
3097
+ elif hours > charge_time:
3098
+ hours = charge_time
3099
+ elif hours > hours_to_full:
3100
+ kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
3101
+ required = hours_to_full + charge_time * kwh_shortfall / (end_residual - start_residual) # time to recover energy not added
3102
+ hours = required if required < charge_time else charge_time
3102
3103
  # rework charge and discharge
3103
3104
  charge_period = get_best_charge_period(start_at, hours)
3104
3105
  charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
3105
3106
  price = charge_period.get('price') if charge_period is not None else None
3106
3107
  start_timed = time_to_start + charge_offset * steps_per_hour
3107
3108
  end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
3108
- output(f" Charge: {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 ""))
3109
+ start_residual = interpolate(start_timed, bat_timed)
3110
+ end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
3111
+ output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3112
+ 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 ""))
3109
3113
  for i in range(int(time_to_start), int(end_timed) + 1):
3110
3114
  j = i + 1
3111
3115
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3136,17 +3140,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3136
3140
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
3137
3141
  if show_data > 0:
3138
3142
  data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3139
- s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
3143
+ s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3140
3144
  h = base_hour + 1
3141
3145
  t = steps_per_hour
3142
- s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
3143
3146
  while t < len(time_line) and bat_timed[t] is not None:
3144
- s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
3145
- s += f" {hours_time(time_line[t])}"
3146
- s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
3147
+ col = h % data_wrap
3148
+ s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
3149
+ s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
3147
3150
  h += 1
3148
3151
  t += steps_per_hour
3149
- output(s[:-1])
3152
+ output(s)
3150
3153
  if show_plot > 0:
3151
3154
  print()
3152
3155
  plt.figure(figsize=(figure_width, figure_width/2))
@@ -3198,7 +3201,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3198
3201
  end1 = start1 if force_charge == 0 else start2
3199
3202
  end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
3200
3203
  if timed_mode > 1:
3201
- periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, target_soc=end_soc, start_soc=start_soc)
3204
+ periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
3202
3205
  set_schedule(periods = periods)
3203
3206
  else:
3204
3207
  set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
@@ -3274,17 +3277,16 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3274
3277
  plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
3275
3278
  if show_data > 0 and plots.get('SoC') is not None:
3276
3279
  data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3277
- s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
3280
+ s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3278
3281
  h = base_hour + 1
3279
3282
  t = steps_per_hour
3280
- s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
3281
- while t < len(time_line) and plots['SoC'][t] is not None:
3282
- s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
3283
- s += f" {hours_time(time_line[t])}"
3284
- s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
3283
+ while t < len(time_line) and bat_timed[t] is not None:
3284
+ col = h % data_wrap
3285
+ s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
3286
+ s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
3285
3287
  h += 1
3286
3288
  t += steps_per_hour
3287
- print(s[:-1])
3289
+ print(s)
3288
3290
  if show_plot > 0:
3289
3291
  print()
3290
3292
  plt.figure(figsize=(figure_width, figure_width/2))
foxesscloud/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 23 September 2024
4
+ Updated: 26 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.3"
13
+ version = "2.5.5"
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, target_soc=100, start_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': target_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)} ** removed ** (overlaps charge period)", 2)
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
- expires = int((s['valid_to'] - base_time_adjust) * steps_per_hour) if s.get('valid_to') is not None else None
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, 'expires': expires}
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 ""
@@ -2128,11 +2129,6 @@ regions = {'A':'Eastern England', 'B':'East Midlands', 'C':'London', 'D':'Mersey
2128
2129
  'J':'South Eastern England', 'K':'Southern Wales', 'L':'South Western England', 'M':'Yorkshire', 'N':'Southern Scotland', 'P':'Northern Scotland'}
2129
2130
 
2130
2131
 
2131
- # preset weightings for average 30 minute pricing over charging duration:
2132
- front_loaded = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5] # 3 hour average, front loaded
2133
- first_hour = [1.0, 1.0] # lowest average price for first hour
2134
-
2135
-
2136
2132
  tariff_config = {
2137
2133
  'product': "AGILE-24-04-03", # product code to use for Octopus API
2138
2134
  'region': "H", # region code to use for Octopus API
@@ -2141,7 +2137,7 @@ tariff_config = {
2141
2137
  'plunge_price': [1, 10], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
2142
2138
  'plunge_slots': 6, # number of 30 minute slots to use
2143
2139
  'data_wrap': 6, # prices to show per line
2144
- 'show_data': 0, # show pricing data
2140
+ 'show_data': 1, # show pricing data
2145
2141
  'show_plot': 1 # plot pricing data
2146
2142
  }
2147
2143
 
@@ -2186,21 +2182,22 @@ def get_agile_times(tariff=agile_octopus, d=None):
2186
2182
  # extract times and prices. Times are Zulu (UTC)
2187
2183
  prices = [] # ordered list of 30 minute prices
2188
2184
  for i in range(0, len(results)):
2189
- start = (now.hour + i / 2) % 24
2185
+ hour = i / 2
2186
+ start = (now.hour + hour) % 24
2190
2187
  time_offset = daylight_saving(results[i]['valid_from'][:16]) if daylight_saving is not None else 0
2191
2188
  prices.append({
2192
2189
  'start': start,
2193
2190
  'end': round_time(start + 0.5),
2194
2191
  'time': hours_time(time_hours(results[i]['valid_from'][11:16]) + time_offset + time_shift),
2195
2192
  'price': results[i]['value_inc_vat'],
2196
- 'valid_to': i / 2 + 0.5})
2193
+ 'hour': hour})
2197
2194
  tariff['agile']['base_time'] = period_from.replace('T', ' ')
2198
2195
  tariff['agile']['prices'] = prices
2199
2196
  plunge = []
2200
2197
  plunge_price = tariff_config['plunge_price'] if tariff_config.get('plunge_price') is not None else 2
2201
2198
  plunge_price = [plunge_price] if type(plunge_price) is not list else plunge_price
2202
2199
  plunge_slots = tariff_config['plunge_slots'] if tariff_config.get('plunge_slots') is not None else 6
2203
- for i in range(0, min([48, len(prices)])):
2200
+ for i in range(0, len(prices)):
2204
2201
  # hour relative index into list of plunge prices, starting at 7am
2205
2202
  x = int(((now.hour - 7 + i / 2) % 24) * len(plunge_price) / 24)
2206
2203
  if prices[i] is not None and prices[i]['price'] < plunge_price[x]:
@@ -2225,13 +2222,13 @@ def get_agile_times(tariff=agile_octopus, d=None):
2225
2222
  # show the results
2226
2223
  if tariff_config['show_data'] > 0:
2227
2224
  data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
2228
- t = (now.hour * 2) % data_wrap
2229
- s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
2225
+ col = (now.hour * 2) % data_wrap
2226
+ s = f"\nPrice p/kWh inc VAT on {today}:"
2230
2227
  for i in range(0, len(prices)):
2231
- s += "\n" if i > 0 and t % data_wrap == 0 else ""
2232
- s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
2233
- t += 1
2234
- output(s[:-1])
2228
+ s += (f"\n {prices[i]['time']} " + " " * col * 6) if i == 0 or col == 0 else ""
2229
+ s += f" {prices[i]['price']:4.1f}"
2230
+ col = (col + 1) % data_wrap
2231
+ output(s)
2235
2232
  if tariff_config['show_plot'] > 0:
2236
2233
  plt.figure(figsize=(figure_width, figure_width/2))
2237
2234
  x_timed = [i for i in range(0, len(prices))]
@@ -2258,7 +2255,8 @@ def get_best_charge_period(start, duration):
2258
2255
  key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
2259
2256
  key = key[0] if len(key) > 0 else None
2260
2257
  end = tariff[key]['end'] if key is not None else round_time(start + duration)
2261
- span = int(duration * 2 + 0.99)
2258
+ span = int(duration * 2 + 0.99) # number of slots needed for charging
2259
+ last = (duration * 2) % 1 # amount of last slot used for charging
2262
2260
  coverage = max([round_time(end - start), duration])
2263
2261
  period = {'start': start, 'end': round_time(start + coverage)}
2264
2262
  prices = tariff['agile']['prices']
@@ -2267,13 +2265,14 @@ def get_best_charge_period(start, duration):
2267
2265
  return None
2268
2266
  elif len(slots) == 1:
2269
2267
  best = slots
2270
- price = prices[slots[0]]['price']
2271
2268
  best_start = start
2269
+ price = prices[best[0]]['price']
2272
2270
  else:
2273
2271
  # best charge time for duration
2274
2272
  weighting = tariff_config.get('weighting')
2275
2273
  times = []
2276
- weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
2274
+ weights = ([1.0] * (span)) if weighting is None else (weighting + [1.0] * span)[:span]
2275
+ weights[-1] *= last if last > 0.0 else 1.0
2277
2276
  best = None
2278
2277
  price = None
2279
2278
  for i in range(0, len(slots) - span + 1):
@@ -2458,7 +2457,7 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
2458
2457
  if strategy is not None:
2459
2458
  period['mode'] = 'SelfUse'
2460
2459
  for d in strategy:
2461
- if hour_in(h, d) and (d.get('expires') is None or i < d['expires']):
2460
+ if hour_in(h, d) and (d.get('valid_for') is None or i in d['valid_for']):
2462
2461
  mode = d['mode']
2463
2462
  period['mode'] = mode
2464
2463
  min_soc_now = d['min_soc'] if d.get('min_soc') is not None else min_soc
@@ -2827,7 +2826,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2827
2826
  if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
2828
2827
  solcast_value = fsolcast.daily[forecast_day]['kwh']
2829
2828
  solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
2830
- output(f"\nSolcast forecast for {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}")
2829
+ output(f"\nSolcast: {tomorrow} {fsolcast.daily[tomorrow]['kwh']:.1f}kWh")
2831
2830
  # get forecast.solar data and produce time line
2832
2831
  solar_value = None
2833
2832
  solar_profile = None
@@ -2836,7 +2835,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2836
2835
  if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
2837
2836
  solar_value = fsolar.daily[forecast_day]['kwh']
2838
2837
  solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
2839
- output(f"\nSolar forecast for {tomorrow}: {fsolar.daily[tomorrow]['kwh']:.1f}")
2838
+ output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
2840
2839
  if solcast_value is None and solar_value is None and debug_setting > 1:
2841
2840
  output(f"\nNo forecasts available at this time")
2842
2841
  # get generation data
@@ -2925,19 +2924,18 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2925
2924
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
2926
2925
  start_soc = int(start_residual / capacity * 100 + 0.5)
2927
2926
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
2928
- target_soc = charge_config['target_soc'] if charge_config.get('target_soc') is not None else None
2929
- 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
2930
2929
  if target_kwh > (end_residual + kwh_needed):
2931
2930
  kwh_needed = target_kwh - end_residual
2932
- elif full_charge is not None or force_charge == 2:
2933
- kwh_needed = capacity - start_residual
2934
2931
  elif test_charge is not None:
2935
2932
  output(f"\nTest charge of {test_charge}kWh")
2936
2933
  kwh_needed = test_charge
2937
2934
  charge_message = "** test charge **"
2938
2935
  # work out charge needed
2939
- if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and full_charge is None and test_charge is None and force_charge != 2:
2940
- output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
2936
+ if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and test_charge is None:
2937
+ output(f"\nNo charging needed:")
2938
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2941
2939
  charge_message = "no charge needed"
2942
2940
  kwh_needed = 0.0
2943
2941
  hours = 0.0
@@ -2950,26 +2948,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2950
2948
  work_mode_timed[t]['min_soc'] = start_soc
2951
2949
  else:
2952
2950
  if test_charge is None:
2953
- output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
2951
+ output(f"\nCharge needed {kwh_needed:.2f}kWh:")
2954
2952
  charge_message = "with charge added"
2955
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
2953
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2956
2954
  # work out time to add kwh_needed to battery
2957
- taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
2958
- hours = round_time(kwh_needed / (charge_power * charge_loss) + taper_time)
2959
- # charge time exceeded or charge needed exceeds capacity
2960
- if hours > charge_time or (start_residual + kwh_needed) > (capacity * 1.01):
2961
- kwh_needed = capacity - start_residual
2962
- hours = charge_time
2963
- elif hours < charge_config['min_hours']:
2955
+ charge_rate = charge_power * charge_loss
2956
+ hours = round_time(kwh_needed / charge_rate)
2957
+ # check if charge time exceeded or charge needed exceeds capacity
2958
+ hours_to_full = round_time((capacity - start_residual) / (charge_rate) + 10)
2959
+ if hours < charge_config['min_hours']:
2964
2960
  hours = charge_config['min_hours']
2965
- end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
2961
+ elif hours > charge_time:
2962
+ hours = charge_time
2963
+ elif hours > hours_to_full:
2964
+ kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
2965
+ required = hours_to_full + charge_time * kwh_shortfall / (end_residual - start_residual) # time to recover energy not added
2966
+ hours = required if required < charge_time else charge_time
2966
2967
  # rework charge and discharge
2967
2968
  charge_period = get_best_charge_period(start_at, hours)
2968
2969
  charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
2969
2970
  price = charge_period.get('price') if charge_period is not None else None
2970
2971
  start_timed = time_to_start + charge_offset * steps_per_hour
2971
2972
  end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
2972
- output(f" Charge: {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 ""))
2973
+ start_residual = interpolate(start_timed, bat_timed)
2974
+ end_soc = min([int((start_residual + kwh_needed) / capacity * 100 + 0.5), 100])
2975
+ output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
2976
+ 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 ""))
2973
2977
  for i in range(int(time_to_start), int(end_timed) + 1):
2974
2978
  j = i + 1
2975
2979
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3000,17 +3004,16 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3000
3004
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
3001
3005
  if show_data > 0:
3002
3006
  data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3003
- s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
3007
+ s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3004
3008
  h = base_hour + 1
3005
3009
  t = steps_per_hour
3006
- s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
3007
3010
  while t < len(time_line) and bat_timed[t] is not None:
3008
- s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
3009
- s += f" {hours_time(time_line[t])}"
3010
- s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
3011
+ col = h % data_wrap
3012
+ s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
3013
+ s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
3011
3014
  h += 1
3012
3015
  t += steps_per_hour
3013
- output(s[:-1])
3016
+ output(s)
3014
3017
  if show_plot > 0:
3015
3018
  print()
3016
3019
  plt.figure(figsize=(figure_width, figure_width/2))
@@ -3062,7 +3065,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3062
3065
  end1 = start1 if force_charge == 0 else start2
3063
3066
  end2 = round_time(base_hour + (end_timed if force_charge == 0 else time_to_end) / steps_per_hour)
3064
3067
  if timed_mode > 1:
3065
- periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, target_soc=end_soc, start_soc=start_soc)
3068
+ periods = charge_periods(st1=start1, en1=end1, st2=start2, en2=end2, min_soc=min_soc, end_soc=end_soc, start_soc=start_soc)
3066
3069
  set_schedule(periods = periods)
3067
3070
  else:
3068
3071
  set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1)
@@ -3137,17 +3140,16 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3137
3140
  plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
3138
3141
  if show_data > 0 and plots.get('SoC') is not None:
3139
3142
  data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3140
- s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
3143
+ s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3141
3144
  h = base_hour + 1
3142
3145
  t = steps_per_hour
3143
- s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
3144
- while t < len(time_line) and plots['SoC'][t] is not None:
3145
- s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
3146
- s += f" {hours_time(time_line[t])}"
3147
- s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
3146
+ while t < len(time_line) and bat_timed[t] is not None:
3147
+ col = h % data_wrap
3148
+ s += (f"\n {hours_time(time_line[t])}" + " " * col * 6) if t == steps_per_hour or col == 0 else ""
3149
+ s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
3148
3150
  h += 1
3149
3151
  t += steps_per_hour
3150
- print(s[:-1])
3152
+ print(s)
3151
3153
  if show_plot > 0:
3152
3154
  print()
3153
3155
  plt.figure(figsize=(figure_width, figure_width/2))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.5.3
3
+ Version: 2.5.5
4
4
  Summary: library for accessing Fox ESS cloud data using Open API
5
5
  Author-email: Tony Matthews <tony@quasair.co.uk>
6
6
  Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
@@ -528,11 +528,11 @@ This gets the latest 30 minute pricing and uses this to work out the best off pe
528
528
  + forecast_times: a list of times when a forecast can be obtained from Solcast / forecast.solar, aligned with the host system time
529
529
  + strategy: an optional list of times and work modes (see below)
530
530
  + update: optional, 1 (the default) sets the current tariff to Agile Octopus. Setting to 0 does not change the current tariff
531
- + weighting: optional, default is None (see below)
531
+ + weighting: optional, default is None / flat (see below)
532
532
  + time_shift: optional system time shift in hours. The default is for system time to be UTC and to apply the current day light saving time (e.g. GMT/BST)
533
533
  + plunge_price: list of prices in p/kWh when plunge pricing is used (see below). The default is [0, 5].
534
534
  + plunge_slots: the number of 30 minute slots to use for plunge pricing. The default is 6, allowing up to 3 hours.
535
- + show_data: show 30 minute Agile pricing data. Default is 0.
535
+ + show_data: show 30 minute Agile pricing data. Default is 1.
536
536
  + show_plot: plot 30 minute Agile pricing data. Default is 1.
537
537
 
538
538
  Product codes include:
@@ -562,9 +562,7 @@ Region codes include:
562
562
  Pricing for tomorrow is updated around 5pm each day. If run before this time, prices from yesterday are used. By default, prices for tomorrow are fetched after 5pm. The setting for this is:
563
563
  + f.agile_update_time = 17
564
564
 
565
- The best charging period is determined based on the weighted average of the 30 minute prices over the duration. The default is flat (all prices are weighted equally). You can change the weighting by providing 'weighting'. The following preset weightings are provided:
566
- + f.front_loaded: [1.0, 0.9, 0.8, 0.7, 0.6, 0.5]
567
- + f.first_hour: [1.0, 1.0]
565
+ The best charging period is determined based on the weighted average of the 30 minute prices over the duration. The default is flat (all prices are weighted equally, except the last slot, which is pro rata to the charge duration used). You can over-ride the default weighting by providing a list of 30 minute values to apply.
568
566
 
569
567
  set_tariff() can configure multiple off-peak and peak periods for any tariff using the 'times' parameter. Times is a list of tuples:
570
568
  + containing values for key, 'start', 'end' and optional 'force'.
@@ -785,6 +783,18 @@ This setting can be:
785
783
 
786
784
  # Version Info
787
785
 
786
+ 2.5.5<br>
787
+ Improve validation of plunge price periods so they don't repeat across days.
788
+ Correct start and end soc times and values when charging using best Agile time periods.
789
+ Extend charge times when charge needed exceeds battery capacity.
790
+
791
+
792
+ 2.5.4<br>
793
+ Remove preset 'weighting' that were not used.
794
+ Update weighting to apply the requested charge duration correctly.
795
+ Reformat price and SoC tables to reduce wrapping and make them easier to read on small screens.
796
+ Change default for set_tariff() to show Agile 30 minute prices.
797
+
788
798
  2.5.3<br>
789
799
  Reverted change to allow updates during a charge period to avoid removing charge in progress.
790
800
  Update contingency and show how this relates to battery SoC.
@@ -0,0 +1,7 @@
1
+ foxesscloud/foxesscloud.py,sha256=biC30gvvQHFTu3CCDhuK6KRGNi2hFbgFxHjSykj-UpE,211062
2
+ foxesscloud/openapi.py,sha256=MEzJPleAV8-2gR17e2qmQezVeotiqkHMzeCBDVJOfsY,204698
3
+ foxesscloud-2.5.5.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
+ foxesscloud-2.5.5.dist-info/METADATA,sha256=kvZCDYUyu7v1OkE_dik_f_4V4OXQTaZKwOYWYFTircU,55462
5
+ foxesscloud-2.5.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
+ foxesscloud-2.5.5.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
+ foxesscloud-2.5.5.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- foxesscloud/foxesscloud.py,sha256=LkRglTcW4sAVPRgXxkhqyt7fcDcDk00ui_nnsdSU51Y,210817
2
- foxesscloud/openapi.py,sha256=2hyHrGpYNUQybmKCLRZmujW8CfnyS0bjKdxZwPpkjCU,204531
3
- foxesscloud-2.5.3.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
- foxesscloud-2.5.3.dist-info/METADATA,sha256=iXU6pABWNwPSUXW4-NrJWCiy3ZecYsiJPIKNAamHR-g,54937
5
- foxesscloud-2.5.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- foxesscloud-2.5.3.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
- foxesscloud-2.5.3.dist-info/RECORD,,