foxesscloud 2.6.1__tar.gz → 2.6.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.6.1
3
+ Version: 2.6.3
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
@@ -787,6 +787,15 @@ This setting can be:
787
787
 
788
788
  # Version Info
789
789
 
790
+ 2.6.3<br>
791
+ Increase default plungs_slots from 6 to 8.
792
+ Correct battery capacity in get_batteries().
793
+
794
+ 2.6.2<br>
795
+ Update battery calibration for charge_needed() when residual_handling is 2.
796
+ Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
797
+ Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
798
+
790
799
  2.6.1<br>
791
800
  Fix problem where battery discharges below min_soc while waiting for charging to start.
792
801
  Update calibration for Force Charge with BMS 1.014 and later.
@@ -773,6 +773,15 @@ This setting can be:
773
773
 
774
774
  # Version Info
775
775
 
776
+ 2.6.3<br>
777
+ Increase default plungs_slots from 6 to 8.
778
+ Correct battery capacity in get_batteries().
779
+
780
+ 2.6.2<br>
781
+ Update battery calibration for charge_needed() when residual_handling is 2.
782
+ Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
783
+ Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
784
+
776
785
  2.6.1<br>
777
786
  Fix problem where battery discharges below min_soc while waiting for charging to start.
778
787
  Update calibration for Force Charge with BMS 1.014 and later.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.6.1"
7
+ version = "2.6.3"
8
8
  authors = [
9
9
  {name="Tony Matthews", email="tony@quasair.co.uk"},
10
10
  ]
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 09 October 2024
4
+ Updated: 13 October 2024
5
5
  By: Tony Matthews
6
6
  """
7
7
  ##################################################################################################
@@ -10,7 +10,7 @@ By: Tony Matthews
10
10
  # ALL RIGHTS ARE RESERVED © Tony Matthews 2023
11
11
  ##################################################################################################
12
12
 
13
- version = "1.7.3"
13
+ version = "1.7.5"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -65,7 +65,7 @@ def plot_show():
65
65
  ##################################################################################################
66
66
  ##################################################################################################
67
67
 
68
- def query_date(d, offset = None):
68
+ def convert_date(d):
69
69
  if d is not None and len(d) < 18:
70
70
  if len(d) == 10:
71
71
  d += ' 00:00:00'
@@ -76,8 +76,12 @@ def query_date(d, offset = None):
76
76
  try:
77
77
  t = datetime.now() if d is None else datetime.strptime(d, "%Y-%m-%d %H:%M:%S")
78
78
  except Exception as e:
79
- output(f"** query_date(): {str(e)}")
79
+ output(f"** convert_date(): {str(e)}")
80
80
  return None
81
+ return t
82
+
83
+ def query_date(d, offset = None):
84
+ t = convert_date(d)
81
85
  if offset is not None:
82
86
  t += timedelta(days = offset)
83
87
  return {'year': t.year, 'month': t.month, 'day': t.day, 'hour': t.hour, 'minute': t.minute, 'second': t.second}
@@ -594,8 +598,8 @@ battery_params = {
594
598
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
595
599
  'step': 5,
596
600
  'offset': 5,
597
- 'charge_loss': 1.080,
598
- 'discharge_loss': 0.975},
601
+ 'charge_loss': 1.08,
602
+ 'discharge_loss': 0.85},
599
603
  }
600
604
 
601
605
  def get_battery(info=1):
@@ -628,11 +632,15 @@ def get_battery(info=1):
628
632
  output(f"** get_battery().info, no result data, {errno_message(errno)}")
629
633
  else:
630
634
  battery['info'] = result['batteries'][0]
631
- if battery['info']['masterVersion'] >= '1.014':
635
+ if battery['info']['masterVersion'] >= '1.014' and battery['info']['masterSN'][:7] == '60BBHV2':
632
636
  residual_handling = 2
637
+ battery['residual_handling'] = residual_handling
638
+ battery['rated_capacity'] = None
639
+ battery['soh'] = None
640
+ battery['soh_supported'] = False
633
641
  if battery.get('residual') is not None:
634
642
  battery['residual'] /= 1000
635
- if residual_handling == 2:
643
+ if battery['residual_handling'] == 2:
636
644
  capacity = battery.get('residual')
637
645
  soc = battery.get('soc')
638
646
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
@@ -643,7 +651,7 @@ def get_battery(info=1):
643
651
  battery['capacity'] = round(capacity, 3)
644
652
  battery['residual'] = round(residual, 3)
645
653
  battery['charge_rate'] = 50
646
- params = battery_params[residual_handling]
654
+ params = battery_params[battery['residual_handling']]
647
655
  battery['charge_loss'] = params['charge_loss']
648
656
  battery['discharge_loss'] = params['discharge_loss']
649
657
  if battery.get('temperature') is not None:
@@ -679,20 +687,38 @@ def get_batteries(info=1):
679
687
  for i in range(0, len(batteries)):
680
688
  batteries[i]['info'] = result['batteries'][i]
681
689
  for b in batteries:
682
- if b.get('info') is not None and b['info']['masterVersion'] >= '1.014':
683
- residual_handling = 2
684
- capacity = b['ratedCapacity'] / 1000 * int(b['soh']) / 100
685
- soc = b.get('soc')
686
- residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
687
- b['capacity'] = round(capacity, 3)
688
- b['residual'] = round(residual, 3)
690
+ b['residual_handling'] = residual_handling
691
+ if b.get('info') is not None and b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
692
+ b['residual_handling'] = 2
693
+ if b.get('soh') is not None and b['soh'].isnumeric():
694
+ b['soh'] = int(b['soh'])
695
+ b['soh_supported'] = True
696
+ else:
697
+ b['rated_capacity'] = None
698
+ b['soh'] = None
699
+ b['soh_supported'] = False
700
+ for i, b in enumerate(batteries):
701
+ if i == 0:
702
+ residual_handling = b['residual_handling']
703
+ get_battery(info=0)
704
+ battery['ratedCapacity'] = b.get('ratedCapacity')
705
+ b['capacity'] = battery.get('capacity')
706
+ b['residual'] = battery.get('residual')
707
+ b['soc'] = battery.get('soc')
708
+ if b.get('capacity') is None and b.get('soh') is not None:
709
+ capacity = (b['ratedCapacity'] / 1000 * b['soh'] / 100)
710
+ soc = b.get('soc')
711
+ residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
712
+ b['capacity'] = round(capacity, 3)
713
+ b['residual'] = round(residual, 3)
714
+ if b.get('capacity') is not None and b.get('ratedCapacity') is not None:
715
+ b['soh'] = round(b['capacity'] / b['ratedCapacity'] * 1000 * 100, 1)
689
716
  b['charge_rate'] = 50
690
- params = battery_params[residual_handling]
717
+ params = battery_params[b['residual_handling']]
691
718
  b['charge_loss'] = params['charge_loss']
692
719
  b['discharge_loss'] = params['discharge_loss']
693
720
  if b.get('temperature') is not None:
694
721
  b['charge_rate'] = params['table'][int((b['temperature'] - params['offset']) / params['step'])]
695
- battery = batteries[0]
696
722
  return batteries
697
723
 
698
724
  ##################################################################################################
@@ -2165,9 +2191,9 @@ def hours_difference(t1, t2):
2165
2191
  if t1 == t2:
2166
2192
  return 0.0
2167
2193
  if type(t1) is str:
2168
- t1 = datetime.strptime(t1, '%Y-%m-%d %H:%M')
2194
+ t1 = convert_date(t1)
2169
2195
  if type(t2) is str:
2170
- t2 = datetime.strptime(t2, '%Y-%m-%d %H:%M')
2196
+ t2 = convert_date(t2)
2171
2197
  return round((t1 - t2).total_seconds() / 3600,1)
2172
2198
 
2173
2199
  ##################################################################################################
@@ -2331,7 +2357,7 @@ tariff_config = {
2331
2357
  'update_time': 16.5, # time in hours when tomrow's data can be fetched
2332
2358
  'weighting': None, # weights for weighted average
2333
2359
  'plunge_price': [3, 3], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
2334
- 'plunge_slots': 6, # number of 30 minute slots to use
2360
+ 'plunge_slots': 8, # number of 30 minute slots to use
2335
2361
  'data_wrap': 6, # prices to show per line
2336
2362
  'show_data': 1, # show pricing data
2337
2363
  'show_plot': 1 # plot pricing data
@@ -2344,7 +2370,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
2344
2370
  if d is not None and len(d) < 11:
2345
2371
  d += " 18:00"
2346
2372
  # get dates and times
2347
- system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
2373
+ system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
2348
2374
  time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
2349
2375
  # adjust system to get local time now
2350
2376
  now = system_time + timedelta(hours=time_offset)
@@ -2815,7 +2841,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
2815
2841
  def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
2816
2842
  forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
2817
2843
  global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
2818
- global timed_strategy, steps_per_hour, base_time, storage, battery
2844
+ global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
2819
2845
  print(f"\n---------------- charge_needed ----------------")
2820
2846
  # validate parameters
2821
2847
  args = locals()
@@ -2843,7 +2869,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2843
2869
  if type(forecast_times) is not list:
2844
2870
  forecast_times = [forecast_times]
2845
2871
  # get dates and times
2846
- system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else datetime.strptime(test_time, '%Y-%m-%d %H:%M')
2872
+ system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
2847
2873
  time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
2848
2874
  now = system_time + timedelta(hours=time_offset)
2849
2875
  today = datetime.strftime(now, '%Y-%m-%d')
@@ -2916,14 +2942,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2916
2942
  output(f"full_charge = {full_charge}")
2917
2943
  if test_soc is not None:
2918
2944
  current_soc = test_soc
2919
- capacity = 14.54
2945
+ capacity = 14.53
2920
2946
  residual = test_soc * capacity / 100
2921
2947
  bat_volt = 317.4
2922
2948
  bat_power = 0.0
2923
2949
  temperature = 30
2924
2950
  bms_charge_current = 15
2925
- charge_loss = 1.080
2926
- discharge_loss = 0.975
2951
+ charge_loss = charge_rates[2]['charge_loss']
2952
+ discharge_loss = charge_rates[2]['discharge_loss']
2927
2953
  bat_current = 0.0
2928
2954
  device_power = 6.0
2929
2955
  device_current = 35
@@ -2971,6 +2997,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2971
2997
  output(f" Temperature: {temperature:.1f}°C")
2972
2998
  output(f" Resistance: {bat_resistance:.2f} ohms")
2973
2999
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
3000
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
2974
3001
  # charge current may be derated based on temperature
2975
3002
  charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
2976
3003
  if charge_current > bms_charge_current:
@@ -3309,10 +3336,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3309
3336
 
3310
3337
  def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
3311
3338
  global charge_config, storage
3312
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
3339
+ now = convert_date(d)
3313
3340
  yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
3314
3341
  if save is None and charge_config.get('save') is not None:
3315
- save = charge_config.get('save').replace('###', yesterday)
3342
+ save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
3316
3343
  if not os.path.exists(storage + save):
3317
3344
  save = None
3318
3345
  if save is None:
@@ -3339,7 +3366,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
3339
3366
  base_hour = int(time_hours(base_time[11:16]))
3340
3367
  start_day = base_time[:10]
3341
3368
  print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
3342
- now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
3369
+ now = convert_date(base_time)
3343
3370
  end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
3344
3371
  if v is None:
3345
3372
  v = ['pvPower', 'loadsPower', 'SoC']
@@ -3372,13 +3399,14 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
3372
3399
  for i in range(0, run_time):
3373
3400
  plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
3374
3401
  if show_data > 0 and plots.get('SoC') is not None:
3375
- data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3376
- s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3402
+ data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
3403
+ s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
3377
3404
  h = base_hour
3378
3405
  t = 0
3379
3406
  while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
3380
3407
  col = h % data_wrap
3381
3408
  s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
3409
+ s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
3382
3410
  s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
3383
3411
  h += 1
3384
3412
  t += steps_per_hour
@@ -3529,12 +3557,12 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3529
3557
  for v in cell_temps:
3530
3558
  s +=f",{v:.0f}"
3531
3559
  return s
3532
- if rated_capacity is not None:
3533
- output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
3534
- output(f"SoH: {bat_soh}%")
3535
3560
  output(f"Current SoC: {current_soc}%")
3536
- output(f"Capacity: {capacity:.2f}kWh")
3537
- output(f"Residual: {residual:.2f}kWh")
3561
+ output(f"Capacity: {capacity:.2f}kWh" + (" (Residual / SoC x 100)" if bat['residual_handling'] == 1 else ""))
3562
+ output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity / 100)" if bat['residual_handling'] == 2 else ""))
3563
+ if rated_capacity is not None and bat_soh is not None:
3564
+ output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
3565
+ output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
3538
3566
  output(f"InvBatVolt: {bat_volt:.1f}V")
3539
3567
  output(f"InvBatCurrent: {bat_current:.1f}A")
3540
3568
  output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
@@ -3944,7 +3972,7 @@ class Solcast :
3944
3972
  # The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
3945
3973
  global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
3946
3974
  self.data = {}
3947
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
3975
+ now = convert_date(d)
3948
3976
  self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
3949
3977
  self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
3950
3978
  self.quarter = int(self.today[5:7]) // 3 % 4
@@ -4288,7 +4316,7 @@ class Solar :
4288
4316
  def __init__(self, reload=0, quiet=False, shading=None, d=None):
4289
4317
  global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
4290
4318
  self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
4291
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
4319
+ now = convert_date(d)
4292
4320
  self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
4293
4321
  self.quarter = int(self.today[5:7]) // 3 % 4
4294
4322
  self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 09 October 2024
4
+ Updated: 13 October 2024
5
5
  By: Tony Matthews
6
6
  """
7
7
  ##################################################################################################
@@ -10,7 +10,7 @@ By: Tony Matthews
10
10
  # ALL RIGHTS ARE RESERVED © Tony Matthews 2024
11
11
  ##################################################################################################
12
12
 
13
- version = "2.6.1"
13
+ version = "2.6.3"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -66,8 +66,7 @@ def plot_show():
66
66
  ##################################################################################################
67
67
  ##################################################################################################
68
68
 
69
- # return query date as a dictionary with year, month, day, hour, minute, second
70
- def query_date(d, offset = None):
69
+ def convert_date(d):
71
70
  if d is not None and len(d) < 18:
72
71
  if len(d) == 10:
73
72
  d += ' 00:00:00'
@@ -78,8 +77,13 @@ def query_date(d, offset = None):
78
77
  try:
79
78
  t = datetime.now() if d is None else datetime.strptime(d, "%Y-%m-%d %H:%M:%S")
80
79
  except Exception as e:
81
- output(f"** query_date(): {str(e)}")
80
+ output(f"** convert_date(): {str(e)}")
82
81
  return None
82
+ return t
83
+
84
+ # return query date as a dictionary with year, month, day, hour, minute, second
85
+ def query_date(d, offset = None):
86
+ t = convert_date(d)
83
87
  if offset is not None:
84
88
  t += timedelta(days = offset)
85
89
  return {'year': t.year, 'month': t.month, 'day': t.day, 'hour': t.hour, 'minute': t.minute, 'second': t.second}
@@ -94,7 +98,7 @@ def query_time(d, time_span):
94
98
  else:
95
99
  d += ':00'
96
100
  try:
97
- t = datetime.now().replace(minute=0, second=0, microsecond=0) if d is None else datetime.strptime(d, "%Y-%m-%d %H:%M:%S")
101
+ t = datetime.now().replace(minute=0, second=0, microsecond=0) if d is None else convert_date(d)
98
102
  except Exception as e:
99
103
  output(f"** query_time(): {str(e)}")
100
104
  return (None, None)
@@ -560,7 +564,7 @@ battery_params = {
560
564
  'step': 5,
561
565
  'offset': 5,
562
566
  'charge_loss': 1.080,
563
- 'discharge_loss': 0.975},
567
+ 'discharge_loss': 0.85},
564
568
  }
565
569
 
566
570
  def get_battery(info=0, v=None):
@@ -575,7 +579,11 @@ def get_battery(info=0, v=None):
575
579
  battery = {}
576
580
  for i in range(0, len(battery_vars)):
577
581
  battery[battery_data[i]] = result[i].get('value')
578
- if residual_handling == 2:
582
+ battery['residual_handling'] = residual_handling
583
+ battery['rated_capacity'] = None
584
+ battery['soh'] = None
585
+ battery['soh_supported'] = False
586
+ if battery['residual_handling'] == 2:
579
587
  capacity = battery.get('residual')
580
588
  soc = battery.get('soc')
581
589
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
@@ -587,7 +595,7 @@ def get_battery(info=0, v=None):
587
595
  battery['residual'] = round(residual, 3)
588
596
  battery['status'] = 1
589
597
  battery['charge_rate'] = 50
590
- params = battery_params[residual_handling]
598
+ params = battery_params[battery['residual_handling']]
591
599
  battery['charge_loss'] = params['charge_loss']
592
600
  battery['discharge_loss'] = params['discharge_loss']
593
601
  if battery.get('temperature') is not None:
@@ -597,8 +605,8 @@ def get_battery(info=0, v=None):
597
605
  def get_batteries(info=0):
598
606
  global battery, batteries
599
607
  get_battery(info=info)
600
- battery['ratedCapacity'] = None
601
- battery['soh'] = None
608
+ if battery is None:
609
+ return None
602
610
  batteries = [battery]
603
611
  return batteries
604
612
 
@@ -1984,9 +1992,9 @@ def hours_difference(t1, t2):
1984
1992
  if t1 == t2:
1985
1993
  return 0.0
1986
1994
  if type(t1) is str:
1987
- t1 = datetime.strptime(t1, '%Y-%m-%d %H:%M')
1995
+ t1 = convert_date(t1)
1988
1996
  if type(t2) is str:
1989
- t2 = datetime.strptime(t2, '%Y-%m-%d %H:%M')
1997
+ t2 = convert_date(t2)
1990
1998
  return round((t1 - t2).total_seconds() / 3600,1)
1991
1999
 
1992
2000
  ##################################################################################################
@@ -2150,7 +2158,7 @@ tariff_config = {
2150
2158
  'update_time': 16.5, # time in hours when tomrow's data can be fetched
2151
2159
  'weighting': None, # weights for weighted average
2152
2160
  'plunge_price': [3, 3], # plunge price in p/kWh inc VAT over 24 hours from 7am, 7pm
2153
- 'plunge_slots': 6, # number of 30 minute slots to use
2161
+ 'plunge_slots': 8, # number of 30 minute slots to use
2154
2162
  'data_wrap': 6, # prices to show per line
2155
2163
  'show_data': 1, # show pricing data
2156
2164
  'show_plot': 1 # plot pricing data
@@ -2163,7 +2171,7 @@ def get_agile_times(tariff=agile_octopus, d=None):
2163
2171
  if d is not None and len(d) < 11:
2164
2172
  d += " 18:00"
2165
2173
  # get dates and times
2166
- system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
2174
+ system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if d is None else convert_date(d)
2167
2175
  time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
2168
2176
  # adjust system to get local time now
2169
2177
  now = system_time + timedelta(hours=time_offset)
@@ -2634,7 +2642,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
2634
2642
  def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
2635
2643
  forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
2636
2644
  global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
2637
- global timed_strategy, steps_per_hour, base_time, storage, battery
2645
+ global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
2638
2646
  print(f"\n---------------- charge_needed ----------------")
2639
2647
  # validate parameters
2640
2648
  args = locals()
@@ -2662,7 +2670,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2662
2670
  if type(forecast_times) is not list:
2663
2671
  forecast_times = [forecast_times]
2664
2672
  # get dates and times
2665
- system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else datetime.strptime(test_time, '%Y-%m-%d %H:%M')
2673
+ system_time = (datetime.now(tz=timezone.utc) + timedelta(hours=time_shift)) if test_time is None else convert_date(test_time)
2666
2674
  time_offset = daylight_saving(system_time) if daylight_saving is not None else 0
2667
2675
  now = system_time + timedelta(hours=time_offset)
2668
2676
  today = datetime.strftime(now, '%Y-%m-%d')
@@ -2741,8 +2749,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2741
2749
  bat_power = 0.0
2742
2750
  temperature = 30
2743
2751
  bms_charge_current = 15
2744
- charge_loss = 1.080
2745
- discharge_loss = 0.975
2752
+ charge_loss = charge_rates[2]['charge_loss']
2753
+ discharge_loss = charge_rates[2]['discharge_loss']
2746
2754
  bat_current = 0.0
2747
2755
  device_power = 6.0
2748
2756
  device_current = 35
@@ -3126,10 +3134,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3126
3134
 
3127
3135
  def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3128
3136
  global charge_config, storage
3129
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
3137
+ now = convert_date(d)
3130
3138
  yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
3131
3139
  if save is None and charge_config.get('save') is not None:
3132
- save = charge_config.get('save').replace('###', yesterday)
3140
+ save = charge_config.get('save').replace('###', yesterday if d is None else d[:10])
3133
3141
  if not os.path.exists(storage + save):
3134
3142
  save = None
3135
3143
  if save is None:
@@ -3156,7 +3164,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3156
3164
  base_hour = int(time_hours(base_time[11:16]))
3157
3165
  start_day = base_time[:10]
3158
3166
  print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
3159
- now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
3167
+ now = convert_date(base_time)
3160
3168
  end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
3161
3169
  if v is None:
3162
3170
  v = ['pvPower', 'loadsPower', 'SoC']
@@ -3345,12 +3353,12 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3345
3353
  for v in cell_temps:
3346
3354
  s +=f",{v:.0f}"
3347
3355
  return s
3348
- if rated_capacity is not None:
3349
- output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
3350
- output(f"SoH: {bat_soh}%")
3351
3356
  output(f"Current SoC: {current_soc}%")
3352
- output(f"Capacity: {capacity:.2f}kWh")
3353
- output(f"Residual: {residual:.2f}kWh")
3357
+ output(f"Capacity: {capacity:.2f}kWh" + (" (Residual / SoC x 100)" if bat['residual_handling'] == 1 else ""))
3358
+ output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity / 100)" if bat['residual_handling'] == 2 else ""))
3359
+ if rated_capacity is not None and bat_soh is not None:
3360
+ output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
3361
+ output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
3354
3362
  output(f"InvBatVolt: {bat_volt:.1f}V")
3355
3363
  output(f"InvBatCurrent: {bat_current:.1f}A")
3356
3364
  output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
@@ -3760,7 +3768,7 @@ class Solcast :
3760
3768
  # The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
3761
3769
  global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
3762
3770
  self.data = {}
3763
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
3771
+ now = convert_date(d)
3764
3772
  self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
3765
3773
  self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
3766
3774
  self.quarter = int(self.today[5:7]) // 3 % 4
@@ -4104,7 +4112,7 @@ class Solar :
4104
4112
  def __init__(self, reload=0, quiet=False, shading=None, d=None):
4105
4113
  global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
4106
4114
  self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
4107
- now = datetime.now() if d is None else datetime.strptime(d, '%Y-%m-%d %H:%M')
4115
+ now = convert_date(d)
4108
4116
  self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
4109
4117
  self.quarter = int(self.today[5:7]) // 3 % 4
4110
4118
  self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.6.1
3
+ Version: 2.6.3
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
@@ -787,6 +787,15 @@ This setting can be:
787
787
 
788
788
  # Version Info
789
789
 
790
+ 2.6.3<br>
791
+ Increase default plungs_slots from 6 to 8.
792
+ Correct battery capacity in get_batteries().
793
+
794
+ 2.6.2<br>
795
+ Update battery calibration for charge_needed() when residual_handling is 2.
796
+ Update get_battery() and get_batteries() to include states for ratedCapacity, soh, residual_handling and soh_supported.
797
+ Update charge_compare(), Solcast() and Solar() so date (d) parameter is more flexible.
798
+
790
799
  2.6.1<br>
791
800
  Fix problem where battery discharges below min_soc while waiting for charging to start.
792
801
  Update calibration for Force Charge with BMS 1.014 and later.
File without changes
File without changes