foxesscloud 2.7.0__py3-none-any.whl → 2.7.1__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: 06 November 2024
4
+ Updated: 07 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.8.1"
13
+ version = "1.8.2"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -600,7 +600,7 @@ battery_params = {
600
600
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
601
601
  'step': 5,
602
602
  'offset': 5,
603
- 'charge_loss': 1.07,
603
+ 'charge_loss': 1.08,
604
604
  'discharge_loss': 0.95},
605
605
  # Mira BMS with firmware 1.014 or later
606
606
  3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
@@ -801,7 +801,7 @@ def time_period(t):
801
801
  result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
802
802
  return result
803
803
 
804
- def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force=0, enable=1):
804
+ def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force=0, enable=1):
805
805
  global device_sn, battery_settings, debug_setting, messages, schedule
806
806
  if get_device() is None:
807
807
  return None
@@ -1081,6 +1081,8 @@ def get_remote_settings(key):
1081
1081
 
1082
1082
  def get_named_settings(name):
1083
1083
  global named_settings
1084
+ if get_device() is None:
1085
+ return None
1084
1086
  if type(name) is list:
1085
1087
  result = []
1086
1088
  for n in name:
@@ -2774,8 +2776,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
2774
2776
  strategy = get_strategy(timed_mode=timed_mode)
2775
2777
  for i in range(0, run_time):
2776
2778
  h = time_line[i]
2777
- 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,
2778
- 'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
2779
+ period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
2780
+ 'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
2779
2781
  if strategy is not None:
2780
2782
  period['mode'] = 'SelfUse'
2781
2783
  for d in strategy:
@@ -2810,33 +2812,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2810
2812
  for i in range(0, run_time):
2811
2813
  w = work_mode_timed[i]
2812
2814
  w['kwh'] = kwh_current
2815
+ kwh_next = kwh_current
2813
2816
  max_now = w['max_soc'] * capacity / 100
2814
- if kwh_current < max_now and w['charge'] > 0.0:
2815
- kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2816
- kwh_current = max_now if kwh_current > max_now else kwh_current
2817
- kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2818
- if kwh_current > capacity:
2819
- # battery is full
2820
- kwh_current = capacity
2821
- w = work_mode_timed[i+1] if (i + 1) < run_time else w
2822
- min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
2817
+ min_soc_now = w['min_soc']
2823
2818
  reserve_now = capacity * min_soc_now / 100
2824
- if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
2819
+ reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2820
+ fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
2821
+ if kwh_next < max_now and w['charge'] > 0.0:
2822
+ # charge from grid or force charge
2823
+ kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2824
+ kwh_next = max_now if kwh_next > max_now else kwh_next
2825
+ if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
2826
+ # force discharge
2827
+ kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
2828
+ if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
2829
+ kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
2830
+ else:
2831
+ # normal discharge
2832
+ kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2833
+ if kwh_next > capacity:
2834
+ # battery is full
2835
+ kwh_next = capacity
2836
+ if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
2825
2837
  # battery is empty, check if charge is needed
2826
- reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2827
- reserve_drain = kwh_current if reserve_drain is None or kwh_current > reserve_drain else reserve_drain
2828
- kwh_current = reserve_drain
2838
+ if kwh_current > reserve_now and kwh_next < reserve_now:
2839
+ kwh_next = reserve_now
2840
+ reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
2829
2841
  if reserve_drain <= reserve_limit:
2842
+ # float charge
2830
2843
  reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
2844
+ kwh_next = reserve_drain
2831
2845
  else:
2832
2846
  # BMS power drain
2847
+ kwh_next = reserve_drain
2833
2848
  reserve_drain -= bms_loss / steps_per_hour
2834
2849
  else:
2835
2850
  # reset drain level
2836
2851
  reserve_drain = reserve_now
2837
- if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2838
- kwh_min = kwh_current
2839
- return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
2852
+ if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
2853
+ kwh_min = kwh_next
2854
+ kwh_current = kwh_next
2855
+ return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
2840
2856
 
2841
2857
  # use work_mode_timed to generate time periods for the inverter schedule
2842
2858
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
@@ -3044,7 +3060,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3044
3060
  output(f"full_charge = {full_charge}")
3045
3061
  if test_soc is not None:
3046
3062
  current_soc = test_soc
3047
- capacity = 14.53
3063
+ capacity = 14.46
3048
3064
  residual = test_soc * capacity / 100
3049
3065
  bat_volt = 317.4
3050
3066
  bat_power = 0.0
@@ -3195,27 +3211,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3195
3211
  output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
3196
3212
  if solcast_value is None and solar_value is None and debug_setting > 1:
3197
3213
  output(f"\nNo forecasts available at this time")
3198
- # get generation data
3199
- generation = None
3200
- last_date = today if hour_now >= charge_config['use_today'] else yesterday
3201
- gen_days = charge_config['generation_days']
3202
- history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
3203
- pv_history = {}
3204
- if history is not None and len(history) > 0:
3205
- for day in history:
3206
- date = day['date']
3207
- if pv_history.get(date) is None:
3208
- pv_history[date] = 0.0
3209
- if day.get('kwh') is not None and day.get('kwh_neg') is not None:
3210
- pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
3211
- pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
3212
- output(f"\nGeneration (kWh):")
3213
- s = ""
3214
- for d in sorted(pv_history.keys())[-gen_days:]:
3215
- s += f" {d} {pv_history[d]:4.1f},"
3216
- output(' ' + s[:-1])
3217
- generation = pv_sum / gen_days
3218
- output(f" Average of last {gen_days} days: {generation:.1f}kWh")
3219
3214
  # choose expected value and produce generation time line
3220
3215
  quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
3221
3216
  sun_name = seasonal_sun[quarter]['name']
@@ -3233,11 +3228,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3233
3228
  elif solar_value is not None:
3234
3229
  expected = solar_value
3235
3230
  generation_timed = solar_timed
3236
- elif generation is None or generation == 0.0:
3237
- output(f"\nNo generation data available")
3238
- output_close()
3239
- return None
3240
3231
  else:
3232
+ # no forecast, use generation history
3233
+ generation = None
3234
+ last_date = today if hour_now >= charge_config['use_today'] else yesterday
3235
+ gen_days = charge_config['generation_days']
3236
+ history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
3237
+ pv_history = {}
3238
+ if history is not None and len(history) > 0:
3239
+ for day in history:
3240
+ date = day['date']
3241
+ if pv_history.get(date) is None:
3242
+ pv_history[date] = 0.0
3243
+ if day.get('kwh') is not None and day.get('kwh_neg') is not None:
3244
+ pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
3245
+ pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
3246
+ output(f"\nGeneration (kWh):")
3247
+ s = ""
3248
+ for d in sorted(pv_history.keys())[-gen_days:]:
3249
+ s += f" {d} {pv_history[d]:4.1f},"
3250
+ output(' ' + s[:-1])
3251
+ generation = pv_sum / gen_days
3252
+ output(f" Average of last {gen_days} days: {generation:.1f}kWh")
3253
+ if generation is None or generation == 0.0:
3254
+ output(f"\nNo generation data available")
3255
+ output_close()
3256
+ return None
3241
3257
  expected = generation
3242
3258
  generation_timed = [expected * x / sun_sum for x in sun_timed]
3243
3259
  if charge_config['forecast_selection'] == 1 and update_settings > 0:
@@ -3245,7 +3261,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3245
3261
  update_settings = 0
3246
3262
  # produce time lines for charge, discharge and work mode
3247
3263
  charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
3248
- discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
3264
+ discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
3249
3265
  work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
3250
3266
  for i in range(0, len(work_mode_timed)):
3251
3267
  # get work mode
@@ -3257,19 +3273,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3257
3273
  work_mode_timed[i]['charge'] = charge_power * duration
3258
3274
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
3259
3275
  fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
3260
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
3261
- discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
3276
+ work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
3262
3277
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
3263
- discharge_timed[i] = bms_loss
3264
- if timed_mode > 1:
3265
- work_mode_timed[i]['hold'] = 1
3278
+ discharge_timed[i] = operating_loss
3279
+ work_mode_timed[i]['hold'] = 1
3266
3280
  elif timed_mode > 0 and work_mode == 'Backup':
3267
- discharge_timed[i] = bms_loss if charge_timed[i] == 0.0 else 0.0
3281
+ discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
3268
3282
  elif timed_mode > 0 and work_mode == 'Feedin':
3269
- (discharge_timed[i], charge_timed[i]) = (bms_loss if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
3283
+ (discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
3270
3284
  0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
3271
3285
  else: # work_mode == 'SelfUse'
3272
- (discharge_timed[i], charge_timed[i]) = (bms_loss if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
3286
+ (discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
3273
3287
  0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
3274
3288
  work_mode_timed[i]['pv'] = charge_timed[i]
3275
3289
  work_mode_timed[i]['discharge'] = discharge_timed[i]
@@ -3282,8 +3296,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3282
3296
  kwh_contingency = consumption * contingency / 100
3283
3297
  kwh_needed = reserve + kwh_contingency - kwh_min
3284
3298
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
3285
- start_soc = int(start_residual / capacity * 100 + 0.5)
3286
- end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
3299
+ end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends (without charging)
3287
3300
  target_soc = charge_config.get('target_soc')
3288
3301
  target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
3289
3302
  if target_kwh > (end_residual + kwh_needed):
@@ -3302,7 +3315,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3302
3315
  hours = 0.0
3303
3316
  start_timed = time_to_end
3304
3317
  end_timed = time_to_end
3305
- end_soc = int(end_residual / capacity * 100 + 0.5)
3306
3318
  else:
3307
3319
  # work out time to add kwh_needed to battery
3308
3320
  charge_rate = charge_power * charge_loss
@@ -3313,30 +3325,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3313
3325
  charge_message = "with charge added"
3314
3326
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3315
3327
  # check if charge time exceeded or charge needed exceeds capacity
3316
- hours_to_full = (capacity - start_residual) / charge_rate
3317
- if hours > charge_time:
3328
+ hours_to_full = (capacity - end_residual) / charge_rate
3329
+ if hours > charge_time or bat_hold == 2:
3318
3330
  hours = charge_time
3319
3331
  elif hours > hours_to_full:
3320
- kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
3332
+ kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
3321
3333
  required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
3322
3334
  hours = required if required > hours and required < charge_time else charge_time
3323
- # round charge time and work out what will actually be added
3335
+ # round charge time
3324
3336
  min_hours = charge_config['min_hours']
3325
3337
  hours = int(hours / min_hours + 0.99) * min_hours
3326
- kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
3327
- kwh_added += discharge_rate * hours # discharge saved by charging
3328
- kwh_spare = kwh_min - reserve + kwh_added
3329
3338
  # rework charge and discharge
3330
3339
  charge_period = get_best_charge_period(start_at, hours)
3331
- charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
3340
+ charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
3332
3341
  price = charge_period.get('price') if charge_period is not None else None
3333
3342
  start_timed = time_to_start + charge_offset * steps_per_hour
3334
3343
  end_timed = start_timed + hours * steps_per_hour
3335
3344
  start_residual = interpolate(start_timed, bat_timed)
3336
- end_soc = min([int((start_residual + kwh_added) / capacity * 100 + 0.5), 100])
3337
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3338
- output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
3339
- + (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh)")
3345
+ start_soc = start_residual / capacity * 100
3346
+ kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
3347
+ kwh_added += discharge_rate * hours # discharge saved by charging
3348
+ kwh_spare = kwh_min - reserve + kwh_added
3349
+ output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3350
+ output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
3351
+ + (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
3340
3352
  for i in range(int(time_to_start), int(time_to_end)):
3341
3353
  j = i + 1
3342
3354
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3359,8 +3371,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3359
3371
  # rebuild the battery residual with the charge added and min_soc
3360
3372
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
3361
3373
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3374
+ end_soc = end_residual / capacity * 100
3362
3375
  # show the results
3363
- output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3376
+ output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3364
3377
  output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
3365
3378
  if not charge_today:
3366
3379
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
@@ -4356,7 +4369,7 @@ class Solcast :
4356
4369
  estimate_values = [self.estimate[r][hours_time(t)] for t in times]
4357
4370
  plots[r] = estimate_values
4358
4371
  total_forecast = 0.0
4359
- if self.daily.get(day) is not None:
4372
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4360
4373
  sun_times = get_suntimes(day)
4361
4374
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4362
4375
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
@@ -4679,7 +4692,7 @@ class Solar :
4679
4692
  estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
4680
4693
  plots[r] = estimate_values
4681
4694
  total_forecast = 0.0
4682
- if self.daily.get(day) is not None:
4695
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4683
4696
  sun_times = get_suntimes(day)
4684
4697
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4685
4698
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
foxesscloud/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 06 November 2024
4
+ Updated: 07 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.7.0"
13
+ version = "2.7.1"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -564,7 +564,7 @@ battery_params = {
564
564
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
565
565
  'step': 5,
566
566
  'offset': 5,
567
- 'charge_loss': 1.07,
567
+ 'charge_loss': 1.08,
568
568
  'discharge_loss': 0.95},
569
569
  # Mira BMS with firmware 1.014 or later
570
570
  3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
@@ -674,7 +674,7 @@ def time_period(t, n):
674
674
  result += f" Charge from grid" if enable else f" Battery Hold"
675
675
  return result
676
676
 
677
- def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
677
+ def set_charge(ch1=0, st1=0, en1=True, ch2=0, st2=0, en2=True, force = 0, enable=1):
678
678
  global device_sn, battery_settings, debug_setting, time_period_vars
679
679
  if get_device() is None:
680
680
  return None
@@ -2439,8 +2439,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
2439
2439
  strategy = get_strategy(timed_mode=timed_mode)
2440
2440
  for i in range(0, run_time):
2441
2441
  h = time_line[i]
2442
- 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,
2443
- 'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
2442
+ period = {'mode': current_mode, 'min_soc': min_soc_now, 'max_soc': max_soc, 'fdpwr': 0, 'fdsoc': min_soc_now, 'duration': 1.0,
2443
+ 'pv': 0.0, 'charge': 0.0, 'discharge': 0.0, 'fd_kwh': 0.0, 'hold': 0, 'kwh': None}
2444
2444
  if strategy is not None:
2445
2445
  period['mode'] = 'SelfUse'
2446
2446
  for d in strategy:
@@ -2474,33 +2474,47 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2474
2474
  for i in range(0, run_time):
2475
2475
  w = work_mode_timed[i]
2476
2476
  w['kwh'] = kwh_current
2477
+ kwh_next = kwh_current
2477
2478
  max_now = w['max_soc'] * capacity / 100
2478
- if kwh_current < max_now and w['charge'] > 0.0:
2479
- kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2480
- kwh_current = max_now if kwh_current > max_now else kwh_current
2481
- kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2482
- if kwh_current > capacity:
2483
- # battery is full
2484
- kwh_current = capacity
2485
- w = work_mode_timed[i+1] if (i + 1) < run_time else w
2486
- min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
2479
+ min_soc_now = w['min_soc']
2487
2480
  reserve_now = capacity * min_soc_now / 100
2488
- if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
2481
+ reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2482
+ fdsoc_limit = (capacity * w['fdsoc'] / 100) if w['mode'] =='ForceDischarge' else capacity
2483
+ if kwh_next < max_now and w['charge'] > 0.0:
2484
+ # charge from grid or force charge
2485
+ kwh_next += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2486
+ kwh_next = max_now if kwh_next > max_now else kwh_next
2487
+ if kwh_next > fdsoc_limit and w['fd_kwh'] > 0.0:
2488
+ # force discharge
2489
+ kwh_next += (w['pv' * charge_loss - w['fd_kwh'] / discharge_loss]) / steps_per_hour
2490
+ if kwh_current > fdsoc_limit and kwh_next < fdsoc_limit:
2491
+ kwh_next = fdsoc_limit - w['discharge'] * (1.0 - w['duration']) / discharge_loss / steps_per_hour
2492
+ else:
2493
+ # normal discharge
2494
+ kwh_next += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2495
+ if kwh_next > capacity:
2496
+ # battery is full
2497
+ kwh_next = capacity
2498
+ if kwh_next < reserve_now and (i < time_to_next or kwh_min is None):
2489
2499
  # battery is empty, check if charge is needed
2490
- reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2491
- reserve_drain = kwh_current if reserve_drain is None or kwh_current > reserve_drain else reserve_drain
2492
- kwh_current = reserve_drain
2500
+ if kwh_current > reserve_now and kwh_next < reserve_now:
2501
+ kwh_next = reserve_now
2502
+ reserve_drain = kwh_next if reserve_drain is None or kwh_next > reserve_drain else reserve_drain
2493
2503
  if reserve_drain <= reserve_limit:
2504
+ # float charge
2494
2505
  reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
2506
+ kwh_next = reserve_drain
2495
2507
  else:
2496
2508
  # BMS power drain
2509
+ kwh_next = reserve_drain
2497
2510
  reserve_drain -= bms_loss / steps_per_hour
2498
2511
  else:
2499
2512
  # reset drain level
2500
2513
  reserve_drain = reserve_now
2501
- if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2502
- kwh_min = kwh_current
2503
- return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
2514
+ if kwh_min is not None and kwh_next < kwh_min and i >= time_to_next: # track minimum without charge
2515
+ kwh_min = kwh_next
2516
+ kwh_current = kwh_next
2517
+ return ([work_mode_timed[i]['kwh'] for i in range(0, run_time)], kwh_min)
2504
2518
 
2505
2519
  # use work_mode_timed to generate time periods for the inverter schedule
2506
2520
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
@@ -2708,7 +2722,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2708
2722
  output(f"full_charge = {full_charge}")
2709
2723
  if test_soc is not None:
2710
2724
  current_soc = test_soc
2711
- capacity = 14.54
2725
+ capacity = 14.46
2712
2726
  residual = test_soc * capacity / 100
2713
2727
  bat_volt = 317.4
2714
2728
  bat_power = 0.0
@@ -2862,27 +2876,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2862
2876
  output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
2863
2877
  if solcast_value is None and solar_value is None and debug_setting > 1:
2864
2878
  output(f"\nNo forecasts available at this time")
2865
- # get generation data
2866
- generation = None
2867
- last_date = today if hour_now >= charge_config['use_today'] else yesterday
2868
- gen_days = charge_config['generation_days']
2869
- history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
2870
- pv_history = {}
2871
- if history is not None and len(history) > 0:
2872
- for day in history:
2873
- date = day['date']
2874
- if pv_history.get(date) is None:
2875
- pv_history[date] = 0.0
2876
- if day.get('kwh') is not None and day.get('kwh_neg') is not None:
2877
- pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
2878
- pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
2879
- output(f"\nGeneration (kWh):")
2880
- s = ""
2881
- for d in sorted(pv_history.keys())[-gen_days:]:
2882
- s += f" {d}: {pv_history[d]:4.1f},"
2883
- output(' ' + s[:-1])
2884
- generation = pv_sum / gen_days
2885
- output(f" Average of last {gen_days} days: {generation:.1f}kWh")
2886
2879
  # choose expected value and produce generation time line
2887
2880
  quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
2888
2881
  sun_name = seasonal_sun[quarter]['name']
@@ -2900,11 +2893,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2900
2893
  elif solar_value is not None:
2901
2894
  expected = solar_value
2902
2895
  generation_timed = solar_timed
2903
- elif generation is None or generation == 0.0:
2904
- output(f"\nNo generation data available")
2905
- output_close()
2906
- return None
2907
2896
  else:
2897
+ # no forecast, use generation data
2898
+ generation = None
2899
+ last_date = today if hour_now >= charge_config['use_today'] else yesterday
2900
+ gen_days = charge_config['generation_days']
2901
+ history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
2902
+ pv_history = {}
2903
+ if history is not None and len(history) > 0:
2904
+ for day in history:
2905
+ date = day['date']
2906
+ if pv_history.get(date) is None:
2907
+ pv_history[date] = 0.0
2908
+ if day.get('kwh') is not None and day.get('kwh_neg') is not None:
2909
+ pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
2910
+ pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
2911
+ output(f"\nGeneration (kWh):")
2912
+ s = ""
2913
+ for d in sorted(pv_history.keys())[-gen_days:]:
2914
+ s += f" {d} {pv_history[d]:4.1f},"
2915
+ output(' ' + s[:-1])
2916
+ generation = pv_sum / gen_days
2917
+ output(f" Average of last {gen_days} days: {generation:.1f}kWh")
2918
+ if generation is None or generation == 0.0:
2919
+ output(f"\nNo generation data available")
2920
+ output_close()
2921
+ return None
2908
2922
  expected = generation
2909
2923
  generation_timed = [expected * x / sun_sum for x in sun_timed]
2910
2924
  if charge_config['forecast_selection'] == 1 and update_settings > 0:
@@ -2912,7 +2926,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2912
2926
  update_settings = 0
2913
2927
  # produce time lines for charge, discharge and work mode
2914
2928
  charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
2915
- discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
2929
+ discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + operating_loss for x in consumption_timed]
2916
2930
  work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
2917
2931
  for i in range(0, len(work_mode_timed)):
2918
2932
  # get work mode
@@ -2924,19 +2938,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2924
2938
  work_mode_timed[i]['charge'] = charge_power * duration
2925
2939
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
2926
2940
  fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
2927
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
2928
- discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
2941
+ work_mode_timed[i]['fd_kwh'] = min([discharge_limit, export_limit + discharge_timed[i], fdpwr]) * duration
2929
2942
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
2930
- discharge_timed[i] = bms_loss
2931
- if timed_mode > 1:
2932
- work_mode_timed[i]['hold'] = 1
2943
+ discharge_timed[i] = operating_loss
2944
+ work_mode_timed[i]['hold'] = 1
2933
2945
  elif timed_mode > 0 and work_mode == 'Backup':
2934
- discharge_timed[i] = bms_loss if charge_timed[i] == 0.0 else 0.0
2946
+ discharge_timed[i] = operating_loss if charge_timed[i] == 0.0 else 0.0
2935
2947
  elif timed_mode > 0 and work_mode == 'Feedin':
2936
- (discharge_timed[i], charge_timed[i]) = (bms_loss if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
2948
+ (discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
2937
2949
  0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
2938
2950
  else: # work_mode == 'SelfUse'
2939
- (discharge_timed[i], charge_timed[i]) = (bms_loss if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
2951
+ (discharge_timed[i], charge_timed[i]) = (0.0 if (charge_timed[i] >= discharge_timed[i]) else (discharge_timed[i] - charge_timed[i]),
2940
2952
  0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
2941
2953
  work_mode_timed[i]['pv'] = charge_timed[i]
2942
2954
  work_mode_timed[i]['discharge'] = discharge_timed[i]
@@ -2949,7 +2961,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2949
2961
  kwh_contingency = consumption * contingency / 100
2950
2962
  kwh_needed = reserve + kwh_contingency - kwh_min
2951
2963
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
2952
- start_soc = int(start_residual / capacity * 100 + 0.5)
2953
2964
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
2954
2965
  target_soc = charge_config.get('target_soc')
2955
2966
  target_kwh = capacity if full_charge is not None or bat_hold == 2 else (target_soc / 100 * capacity) if target_soc is not None else 0
@@ -2969,7 +2980,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2969
2980
  hours = 0.0
2970
2981
  start_timed = time_to_end
2971
2982
  end_timed = time_to_end
2972
- end_soc = int(end_residual / capacity * 100 + 0.5)
2973
2983
  else:
2974
2984
  # work out time to add kwh_needed to battery
2975
2985
  charge_rate = charge_power * charge_loss
@@ -2980,30 +2990,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2980
2990
  charge_message = "with charge added"
2981
2991
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2982
2992
  # check if charge time exceeded or charge needed exceeds capacity
2983
- hours_to_full = (capacity - start_residual) / charge_rate
2984
- if hours > charge_time:
2993
+ hours_to_full = (capacity - end_residual) / charge_rate
2994
+ if hours > charge_time or bat_hold == 2:
2985
2995
  hours = charge_time
2986
2996
  elif hours > hours_to_full:
2987
- kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
2997
+ kwh_shortfall = kwh_needed - (capacity - end_residual) # amount of energy that won't be added
2988
2998
  required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
2989
2999
  hours = required if required > hours and required < charge_time else charge_time
2990
- # round charge time and work out what will actually be added
3000
+ # round charge time
2991
3001
  min_hours = charge_config['min_hours']
2992
3002
  hours = int(hours / min_hours + 0.99) * min_hours
2993
- kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
2994
- kwh_added += discharge_rate * hours # discharge saved during charging
2995
- kwh_spare = kwh_min - reserve + kwh_added
2996
3003
  # rework charge and discharge
2997
3004
  charge_period = get_best_charge_period(start_at, hours)
2998
- charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
3005
+ charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else charge_time - hours
2999
3006
  price = charge_period.get('price') if charge_period is not None else None
3000
3007
  start_timed = time_to_start + charge_offset * steps_per_hour
3001
3008
  end_timed = start_timed + hours * steps_per_hour
3002
3009
  start_residual = interpolate(start_timed, bat_timed)
3003
- end_soc = min([int((start_residual + kwh_added) / capacity * 100 + 0.5), 100])
3004
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3005
- output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
3006
- + (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh)")
3010
+ start_soc = start_residual / capacity * 100
3011
+ kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
3012
+ kwh_added += discharge_rate * hours # discharge saved by charging
3013
+ kwh_spare = kwh_min - reserve + kwh_added
3014
+ output(f" Start SoC: {start_soc:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3015
+ output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
3016
+ + (f" at {price:.2f}p" if price is not None else "") + f" ({kwh_added:.2f}kWh added)")
3007
3017
  for i in range(int(time_to_start), int(time_to_end)):
3008
3018
  j = i + 1
3009
3019
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3026,8 +3036,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3026
3036
  # rebuild the battery residual with any charge added and min_soc
3027
3037
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
3028
3038
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3039
+ end_soc = end_residual / capacity * 100
3029
3040
  # show the results
3030
- output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3041
+ output(f" End SoC: {end_soc:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3031
3042
  output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
3032
3043
  if not charge_today:
3033
3044
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
@@ -4020,7 +4031,7 @@ class Solcast :
4020
4031
  estimate_values = [self.estimate[r][hours_time(t)] for t in times]
4021
4032
  plots[r] = estimate_values
4022
4033
  total_forecast = 0.0
4023
- if self.daily.get(day) is not None:
4034
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4024
4035
  sun_times = get_suntimes(day)
4025
4036
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4026
4037
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
@@ -4343,7 +4354,7 @@ class Solar :
4343
4354
  estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
4344
4355
  plots[r] = estimate_values
4345
4356
  total_forecast = 0.0
4346
- if self.daily.get(day) is not None:
4357
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4347
4358
  sun_times = get_suntimes(day)
4348
4359
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4349
4360
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.7.0
3
+ Version: 2.7.1
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
@@ -158,12 +158,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
158
158
  + minSoc: min Soc setting e.g. 10 = 10%
159
159
 
160
160
  set_charge() takes the charge times from the battery_settings and applies these to the inverter. The parameters are optional and will update battery_settings. You should specify all 3 parameter for a time period:
161
- + ch1: enable charge from grid for period 1 (True or False)
162
- + st1: the start time for period 1
163
- + en1: the end time for period 1
164
- + ch2: enable charge from grid for period 2 (True or False)
165
- + st2: the start time for period 2
166
- + en2: the end time for period 2
161
+ + ch1: enable charge from grid for period 1 (default True)
162
+ + st1: the start time for period 1 (default 0)
163
+ + en1: the end time for period 1 (default 0)
164
+ + ch2: enable charge from grid for period 2 (default True)
165
+ + st2: the start time for period 2 (default 0)
166
+ + en2: the end time for period 2 (default 0)
167
167
  + enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
168
168
 
169
169
  set_period() returns a period structure that can be used to build a list for set_schedule()
@@ -807,6 +807,16 @@ This setting can be:
807
807
 
808
808
  # Version Info
809
809
 
810
+ 2.7.1<br>
811
+ Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
812
+ Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
813
+ Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
814
+ Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
815
+ Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
816
+ Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
817
+ Correct exception in Solcast and Solar when a forecast is not available.
818
+
819
+
810
820
  2.7.0<br>
811
821
  Allow charge_loss / discharge_loss to be configured for charge_needed().
812
822
  Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
@@ -0,0 +1,7 @@
1
+ foxesscloud/foxesscloud.py,sha256=rV8qX1sJyXEvmyie8kvJSFVd3sU-a-9Zcy-i-ag_gTQ,222996
2
+ foxesscloud/openapi.py,sha256=0oxiSNedH_9UnSDQmNaeXgPIaTvwqzg3fa1Dupdi7yQ,206923
3
+ foxesscloud-2.7.1.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
+ foxesscloud-2.7.1.dist-info/METADATA,sha256=Bl4xmInDDpLjnt1pp-Zn_N8dkBzxncEPlq-HPB2ZHts,61833
5
+ foxesscloud-2.7.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
+ foxesscloud-2.7.1.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
+ foxesscloud-2.7.1.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- foxesscloud/foxesscloud.py,sha256=rr9famUpnu4L8sSyBgkcR1GfQyANvbfK05hw4p4d9LI,222420
2
- foxesscloud/openapi.py,sha256=EvgmhA1zq70xx1YqpcifBa9AepRsqM7gt0x3M9M3Vrg,206406
3
- foxesscloud-2.7.0.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
- foxesscloud-2.7.0.dist-info/METADATA,sha256=NqT_e_whf-RIU3rvVqAUzNN7N09WPctPJOp7xhyRCuE,61042
5
- foxesscloud-2.7.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- foxesscloud-2.7.0.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
- foxesscloud-2.7.0.dist-info/RECORD,,