foxesscloud 2.6.9__tar.gz → 2.7.0__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.9
3
+ Version: 2.7.0
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
@@ -341,7 +341,7 @@ The previous section provides functions that can be used to access and control y
341
341
  Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
342
342
 
343
343
  ```
344
- f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
344
+ f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
345
345
  ```
346
346
 
347
347
  All the parameters are optional:
@@ -352,6 +352,7 @@ All the parameters are optional:
352
352
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
353
353
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
354
354
  + show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
355
+ + timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
355
356
 
356
357
  ### Modelling
357
358
 
@@ -394,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
394
395
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
395
396
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
396
397
  ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
398
+ charge_loss: None # loss converting charge energy to stored energy
399
+ discharge_loss: None # loss converting stored energy to discharge energy
397
400
  inverter_power: None # inverter power consumption in W (dynamically set)
398
401
  bms_power: 50 # BMS power consumption in W
399
402
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -804,6 +807,11 @@ This setting can be:
804
807
 
805
808
  # Version Info
806
809
 
810
+ 2.7.0<br>
811
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
812
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
813
+ Correct problem with missing periods of actual data in forecast.compare()
814
+
807
815
  2.6.9<br>
808
816
  Add get and set_named_settings() (for WorkMode and ExportLimit).
809
817
  If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
@@ -327,7 +327,7 @@ The previous section provides functions that can be used to access and control y
327
327
  Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
328
328
 
329
329
  ```
330
- f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
330
+ f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
331
331
  ```
332
332
 
333
333
  All the parameters are optional:
@@ -338,6 +338,7 @@ All the parameters are optional:
338
338
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
339
339
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
340
340
  + show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
341
+ + timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
341
342
 
342
343
  ### Modelling
343
344
 
@@ -380,6 +381,8 @@ export_limit: None # maximum export power in kW. None uses the inver
380
381
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
381
382
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
382
383
  ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
384
+ charge_loss: None # loss converting charge energy to stored energy
385
+ discharge_loss: None # loss converting stored energy to discharge energy
383
386
  inverter_power: None # inverter power consumption in W (dynamically set)
384
387
  bms_power: 50 # BMS power consumption in W
385
388
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -790,6 +793,11 @@ This setting can be:
790
793
 
791
794
  # Version Info
792
795
 
796
+ 2.7.0<br>
797
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
798
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
799
+ Correct problem with missing periods of actual data in forecast.compare()
800
+
793
801
  2.6.9<br>
794
802
  Add get and set_named_settings() (for WorkMode and ExportLimit).
795
803
  If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.6.9"
7
+ version = "2.7.0"
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: 05 November 2024
4
+ Updated: 06 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.0"
13
+ version = "1.8.1"
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.08,
603
+ 'charge_loss': 1.07,
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],
@@ -798,7 +798,7 @@ def get_charge():
798
798
  def time_period(t):
799
799
  result = f"{t['startTime']['hour']:02d}:{t['startTime']['minute']:02d}-{t['endTime']['hour']:02d}:{t['endTime']['minute']:02d}"
800
800
  if t['startTime']['hour'] != t['endTime']['hour'] or t['startTime']['minute'] != t['endTime']['minute']:
801
- result += f" Charge from grid" if t['enableGrid'] else f" Force Charge"
801
+ result += f" Charge from grid" if t['enableGrid'] else f" Battery Hold"
802
802
  return result
803
803
 
804
804
  def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force=0, enable=1):
@@ -1748,9 +1748,7 @@ def plot_raw(result, plot=1, station=0):
1748
1748
  def report_value_profile(result):
1749
1749
  if type(result) is not list or result[0]['type'] != 'day':
1750
1750
  return (None, None)
1751
- data = []
1752
- for h in range(0,24):
1753
- data.append((0.0, 0)) # value sum, count of values
1751
+ data = [(0.0, 0) for h in range(0,24)]
1754
1752
  totals = 0
1755
1753
  n = 0
1756
1754
  for day in result:
@@ -1780,6 +1778,30 @@ def report_value_profile(result):
1780
1778
  # forwards compatibility
1781
1779
  get_history = get_raw
1782
1780
 
1781
+ # rescale history data based on time and steps
1782
+ def rescale_history(data, steps):
1783
+ if data is None or len(data) < 1:
1784
+ return None
1785
+ result = [None for i in range(0, 24 * steps)]
1786
+ bst = 1 if 'BST' in data[0]['time'] else 0
1787
+ average = 0.0
1788
+ n = 0
1789
+ i = 0
1790
+ for d in data:
1791
+ h = round_time(time_hours(d['time'][11:]) + bst)
1792
+ new_i = int(h * steps)
1793
+ if new_i != i and i < len(result):
1794
+ result[i] = average / n if n > 0 else None
1795
+ average = 0.0
1796
+ n = 0
1797
+ i = new_i
1798
+ if d['value'] is not None:
1799
+ average += d['value']
1800
+ n += 1
1801
+ if n > 0 and i < len(result):
1802
+ result[i] = average / n
1803
+ return result
1804
+
1783
1805
  ##################################################################################################
1784
1806
  # get energy report data in kWh
1785
1807
  ##################################################################################################
@@ -2780,8 +2802,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2780
2802
  global charge_config, steps_per_hour
2781
2803
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2782
2804
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2783
- charge_loss = charge_config['charge_loss']
2784
- discharge_loss = charge_config['discharge_loss']
2805
+ charge_loss = charge_config['_charge_loss']
2806
+ discharge_loss = charge_config['_discharge_loss']
2785
2807
  charge_limit = charge_config['charge_limit']
2786
2808
  float_charge = charge_config['float_charge']
2787
2809
  run_time = len(work_mode_timed)
@@ -2874,6 +2896,8 @@ charge_config = {
2874
2896
  'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
2875
2897
  'pv_loss': 0.950, # loss converting PV power to DC battery charge power
2876
2898
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2899
+ 'charge_loss': None, # loss converting charge energy to stored energy
2900
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2877
2901
  'inverter_power': 101, # Inverter power consumption in W
2878
2902
  'bms_power': 50, # BMS power consumption in W
2879
2903
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -3026,8 +3050,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3026
3050
  bat_power = 0.0
3027
3051
  temperature = 30
3028
3052
  bms_charge_current = 15
3029
- charge_loss = battery_params[2]['charge_loss']
3030
- discharge_loss = battery_params[2]['discharge_loss']
3053
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
3054
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
3031
3055
  bat_current = 0.0
3032
3056
  device_power = 6.0
3033
3057
  device_current = 35
@@ -3049,8 +3073,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3049
3073
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
3050
3074
  return None
3051
3075
  bms_charge_current = battery.get('charge_rate')
3052
- charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
3053
- discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
3076
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
3077
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
3054
3078
  device_power = device.get('power')
3055
3079
  device_current = device.get('max_charge_current')
3056
3080
  model = device.get('deviceType')
@@ -3079,7 +3103,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3079
3103
  output(f" Temperature: {temperature:.1f}°C")
3080
3104
  output(f" Resistance: {bat_resistance:.2f} ohms")
3081
3105
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
3082
- output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
3106
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
3083
3107
  # inverter losses
3084
3108
  inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
3085
3109
  operating_loss = inverter_power / 1000
@@ -3108,8 +3132,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3108
3132
  charge_config['charge_limit'] = charge_limit
3109
3133
  charge_config['charge_power'] = charge_power
3110
3134
  charge_config['float_charge'] = float_charge
3111
- charge_config['charge_loss'] = charge_loss
3112
- charge_config['discharge_loss'] = discharge_loss
3135
+ charge_config['_charge_loss'] = charge_loss
3136
+ charge_config['_discharge_loss'] = discharge_loss
3113
3137
  # display what we have
3114
3138
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
3115
3139
  output(f"\nDevice Info:")
@@ -4304,17 +4328,9 @@ class Solcast :
4304
4328
  total_actual = None
4305
4329
  self.actual = get_history('day', d=day, v=v)
4306
4330
  plots = {}
4331
+ times = [i/2 for i in range(0, 48)]
4307
4332
  for v in self.actual:
4308
- times = []
4309
- actual_values = []
4310
- average = 0.0
4311
- for i in range(0, len(v.get('data'))):
4312
- average += v['data'][i]['value'] / 6
4313
- if i % 6 == 5:
4314
- times.append(round_time((i - 5) / 12))
4315
- actual_values.append(average)
4316
- average = 0
4317
- plots[v['variable']] = actual_values
4333
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4318
4334
  if v['variable'] == 'pvPower':
4319
4335
  total_actual = v.get('kwh')
4320
4336
  if total_actual is None:
@@ -4346,10 +4362,10 @@ class Solcast :
4346
4362
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
4347
4363
  total_forecast = sum(forecast_values) / 2
4348
4364
  plots['forecast'] = forecast_values
4349
- if total_actual is not None:
4350
- print(f" Total actual: {total_actual:.3f}kWh")
4351
4365
  if total_forecast is not None:
4352
4366
  print(f" Total forecast: {total_forecast:.3f}kWh")
4367
+ if total_actual is not None:
4368
+ print(f" Total actual: {total_actual:.3f}kWh")
4353
4369
  print()
4354
4370
  title = f"Forecast / Actual PV Power on {day}"
4355
4371
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4637,17 +4653,9 @@ class Solar :
4637
4653
  total_actual = None
4638
4654
  self.actual = get_history('day', d=day, v=v)
4639
4655
  plots = {}
4656
+ times = [i/2 for i in range(0, 48)]
4640
4657
  for v in self.actual:
4641
- times = []
4642
- actual_values = []
4643
- average = 0.0
4644
- for i in range(0, len(v.get('data'))):
4645
- average += v['data'][i]['value'] / 6
4646
- if i % 6 == 5:
4647
- times.append(round_time((i - 5) / 12))
4648
- actual_values.append(average)
4649
- average = 0
4650
- plots[v['variable']] = actual_values
4658
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4651
4659
  if v['variable'] == 'pvPower':
4652
4660
  total_actual = v.get('kwh')
4653
4661
  if total_actual is None:
@@ -4677,10 +4685,10 @@ class Solar :
4677
4685
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4678
4686
  total_forecast = sum(forecast_values) / 2
4679
4687
  plots['forecast'] = forecast_values
4680
- if total_actual is not None:
4681
- print(f" Total actual: {total_actual:.3f}kWh")
4682
4688
  if total_forecast is not None:
4683
4689
  print(f" Total forecast: {total_forecast:.3f}kWh")
4690
+ if total_actual is not None:
4691
+ print(f" Total actual: {total_actual:.3f}kWh")
4684
4692
  print()
4685
4693
  title = f"Forecast / Actual PV Power on {day}"
4686
4694
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 05 November 2024
4
+ Updated: 06 November 2024
5
5
  By: Tony Matthews
6
6
  """
7
7
  ##################################################################################################
@@ -10,7 +10,7 @@ By: Tony Matthews
10
10
  # ALL RIGHTS ARE RESERVED © Tony Matthews 2024
11
11
  ##################################################################################################
12
12
 
13
- version = "2.6.9"
13
+ version = "2.7.0"
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.08,
567
+ 'charge_loss': 1.07,
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],
@@ -671,7 +671,7 @@ def time_period(t, n):
671
671
  (enable, start, end) = (t['enable1'], t['startTime1'], t['endTime1']) if n == 1 else (t['enable2'], t['startTime2'], t['endTime2'])
672
672
  result = f"{start['hour']:02d}:{start['minute']:02d}-{end['hour']:02d}:{end['minute']:02d}"
673
673
  if start['hour'] != end['hour'] or start['minute'] != end['minute']:
674
- result += f" Charge from grid" if enable else f" Force Charge"
674
+ result += f" Charge from grid" if enable else f" Battery Hold"
675
675
  return result
676
676
 
677
677
  def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
@@ -1448,9 +1448,7 @@ get_raw = get_history
1448
1448
  def report_value_profile(result):
1449
1449
  if type(result) is not list or result[0]['type'] != 'day':
1450
1450
  return (None, None)
1451
- data = []
1452
- for h in range(0,24):
1453
- data.append((0.0, 0)) # value sum, count of values
1451
+ data = [(0.0, 0) for h in range(0,24)]
1454
1452
  totals = 0
1455
1453
  n = 0
1456
1454
  for day in result:
@@ -1477,6 +1475,30 @@ def report_value_profile(result):
1477
1475
  result.append(by_hour[t] * daily_average / current_total)
1478
1476
  return (daily_average, result)
1479
1477
 
1478
+ # rescale history data based on time and steps
1479
+ def rescale_history(data, steps):
1480
+ if data is None:
1481
+ return None
1482
+ result = [None for i in range(0, 24 * steps)]
1483
+ bst = 1 if 'BST' in data[0]['time'] else 0
1484
+ average = 0.0
1485
+ n = 0
1486
+ i = 0
1487
+ for d in data:
1488
+ h = round_time(time_hours(d['time'][11:]) + bst)
1489
+ new_i = int(h * steps)
1490
+ if new_i != i and i < len(result):
1491
+ result[i] = average / n if n > 0 else None
1492
+ average = 0.0
1493
+ n = 0
1494
+ i = new_i
1495
+ if d['value'] is not None:
1496
+ average += d['value']
1497
+ n += 1
1498
+ if n > 0 and i < len(result):
1499
+ result[i] = average / n
1500
+ return result
1501
+
1480
1502
 
1481
1503
  ##################################################################################################
1482
1504
  # get production report in kWh
@@ -2444,8 +2466,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2444
2466
  global charge_config, steps_per_hour
2445
2467
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2446
2468
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2447
- charge_loss = charge_config['charge_loss']
2448
- discharge_loss = charge_config['discharge_loss']
2469
+ charge_loss = charge_config['_charge_loss']
2470
+ discharge_loss = charge_config['_discharge_loss']
2449
2471
  charge_limit = charge_config['charge_limit']
2450
2472
  float_charge = charge_config['float_charge']
2451
2473
  run_time = len(work_mode_timed)
@@ -2538,6 +2560,8 @@ charge_config = {
2538
2560
  'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
2539
2561
  'pv_loss': 0.95, # loss converting PV power to DC battery charge power
2540
2562
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2563
+ 'charge_loss': None, # loss converting charge energy to stored energy
2564
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2541
2565
  'inverter_power': 101, # Inverter power consumption in W
2542
2566
  'bms_power': 50, # BMS power consumption in W
2543
2567
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2690,8 +2714,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2690
2714
  bat_power = 0.0
2691
2715
  temperature = 30
2692
2716
  bms_charge_current = 15
2693
- charge_loss = battery_params[2]['charge_loss']
2694
- discharge_loss = battery_params[2]['discharge_loss']
2717
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
2718
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
2695
2719
  bat_current = 0.0
2696
2720
  device_power = 6.0
2697
2721
  device_current = 35
@@ -2713,8 +2737,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2713
2737
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
2714
2738
  return None
2715
2739
  bms_charge_current = battery.get('charge_rate')
2716
- charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
2717
- discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
2740
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
2741
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
2718
2742
  device_power = device.get('power')
2719
2743
  device_current = device.get('max_charge_current')
2720
2744
  model = device.get('deviceType')
@@ -2743,6 +2767,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2743
2767
  output(f" Max Charge: {charge_current:.1f}A")
2744
2768
  output(f" Resistance: {bat_resistance:.2f} ohms")
2745
2769
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
2770
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
2746
2771
  # charge current may be derated based on temperature
2747
2772
  charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
2748
2773
  if charge_current > bms_charge_current:
@@ -2775,8 +2800,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2775
2800
  charge_config['charge_limit'] = charge_limit
2776
2801
  charge_config['charge_power'] = charge_power
2777
2802
  charge_config['float_charge'] = float_charge
2778
- charge_config['charge_loss'] = charge_loss
2779
- charge_config['discharge_loss'] = discharge_loss
2803
+ charge_config['_charge_loss'] = charge_loss
2804
+ charge_config['_discharge_loss'] = discharge_loss
2780
2805
  # display what we have
2781
2806
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
2782
2807
  output(f"\nDevice Info:")
@@ -3967,17 +3992,9 @@ class Solcast :
3967
3992
  total_actual = None
3968
3993
  self.actual = get_history('day', d=day, v=v)
3969
3994
  plots = {}
3995
+ times = [i/2 for i in range(0, 48)]
3970
3996
  for v in self.actual:
3971
- times = []
3972
- actual_values = []
3973
- average = 0.0
3974
- for i in range(0, len(v.get('data'))):
3975
- average += v['data'][i]['value'] / 6
3976
- if i % 6 == 5:
3977
- times.append(round_time((i - 5) / 12))
3978
- actual_values.append(average)
3979
- average = 0
3980
- plots[v['variable']] = actual_values
3997
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
3981
3998
  if v['variable'] == 'pvPower':
3982
3999
  total_actual = v.get('kwh')
3983
4000
  if total_actual is None:
@@ -4009,10 +4026,10 @@ class Solcast :
4009
4026
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
4010
4027
  total_forecast = sum(forecast_values) / 2
4011
4028
  plots['forecast'] = forecast_values
4012
- if total_actual is not None:
4013
- print(f" Total actual: {total_actual:.3f}kWh")
4014
4029
  if total_forecast is not None:
4015
4030
  print(f" Total forecast: {total_forecast:.3f}kWh")
4031
+ if total_actual is not None:
4032
+ print(f" Total actual: {total_actual:.3f}kWh")
4016
4033
  print()
4017
4034
  title = f"Forecast / Actual PV Power on {day}"
4018
4035
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4300,17 +4317,9 @@ class Solar :
4300
4317
  total_actual = None
4301
4318
  self.actual = get_history('day', d=day, v=v)
4302
4319
  plots = {}
4320
+ times = [i/2 for i in range(0, 48)]
4303
4321
  for v in self.actual:
4304
- times = []
4305
- actual_values = []
4306
- average = 0.0
4307
- for i in range(0, len(v.get('data'))):
4308
- average += v['data'][i]['value'] / 6
4309
- if i % 6 == 5:
4310
- times.append(round_time((i - 5) / 12))
4311
- actual_values.append(average)
4312
- average = 0
4313
- plots[v['variable']] = actual_values
4322
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4314
4323
  if v['variable'] == 'pvPower':
4315
4324
  total_actual = v.get('kwh')
4316
4325
  if total_actual is None:
@@ -4340,10 +4349,10 @@ class Solar :
4340
4349
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4341
4350
  total_forecast = sum(forecast_values) / 2
4342
4351
  plots['forecast'] = forecast_values
4343
- if total_actual is not None:
4344
- print(f" Total actual: {total_actual:.3f}kWh")
4345
4352
  if total_forecast is not None:
4346
4353
  print(f" Total forecast: {total_forecast:.3f}kWh")
4354
+ if total_actual is not None:
4355
+ print(f" Total actual: {total_actual:.3f}kWh")
4347
4356
  print()
4348
4357
  title = f"Forecast / Actual PV Power on {day}"
4349
4358
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.6.9
3
+ Version: 2.7.0
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
@@ -341,7 +341,7 @@ The previous section provides functions that can be used to access and control y
341
341
  Uses forecast PV yield for tomorrow to work out if charging from grid is needed tonight to deliver the expected consumption for tomorrow. If charging is needed, the charge times are configured. If charging is not needed, the charge times are cleared. The results are sent to the inverter.
342
342
 
343
343
  ```
344
- f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot)
344
+ f.charge_needed(forecast, force_charge, forecast_selection, forecast_times, update_setings, show_data, show_plot, timed_mode)
345
345
  ```
346
346
 
347
347
  All the parameters are optional:
@@ -352,6 +352,7 @@ All the parameters are optional:
352
352
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
353
353
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
354
354
  + show_plot: 1 plot battery SoC data. 2 plot battery Residual, Generation and Consumption. 3 plot 2 + Charge and Discharge The default is 3
355
+ + timed_mode: 0 use charge times, 1 use charge times and follow strategy, 2 use Mode Scheduler
355
356
 
356
357
  ### Modelling
357
358
 
@@ -394,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
394
395
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
395
396
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
396
397
  ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
398
+ charge_loss: None # loss converting charge energy to stored energy
399
+ discharge_loss: None # loss converting stored energy to discharge energy
397
400
  inverter_power: None # inverter power consumption in W (dynamically set)
398
401
  bms_power: 50 # BMS power consumption in W
399
402
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -804,6 +807,11 @@ This setting can be:
804
807
 
805
808
  # Version Info
806
809
 
810
+ 2.7.0<br>
811
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
812
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
813
+ Correct problem with missing periods of actual data in forecast.compare()
814
+
807
815
  2.6.9<br>
808
816
  Add get and set_named_settings() (for WorkMode and ExportLimit).
809
817
  If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
File without changes
File without changes