foxesscloud 2.6.9__tar.gz → 2.7.1__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.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()
@@ -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,21 @@ This setting can be:
804
807
 
805
808
  # Version Info
806
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
+
820
+ 2.7.0<br>
821
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
822
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
823
+ Correct problem with missing periods of actual data in forecast.compare()
824
+
807
825
  2.6.9<br>
808
826
  Add get and set_named_settings() (for WorkMode and ExportLimit).
809
827
  If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.6.9
4
- Summary: library for accessing Fox ESS cloud data using Open API
5
- Author-email: Tony Matthews <tony@quasair.co.uk>
6
- Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
7
- Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENCE
14
-
15
1
  # FoxESS-Cloud
16
2
 
17
3
  <a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
@@ -158,12 +144,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
158
144
  + minSoc: min Soc setting e.g. 10 = 10%
159
145
 
160
146
  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
147
+ + ch1: enable charge from grid for period 1 (default True)
148
+ + st1: the start time for period 1 (default 0)
149
+ + en1: the end time for period 1 (default 0)
150
+ + ch2: enable charge from grid for period 2 (default True)
151
+ + st2: the start time for period 2 (default 0)
152
+ + en2: the end time for period 2 (default 0)
167
153
  + enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
168
154
 
169
155
  set_period() returns a period structure that can be used to build a list for set_schedule()
@@ -341,7 +327,7 @@ The previous section provides functions that can be used to access and control y
341
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.
342
328
 
343
329
  ```
344
- 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)
345
331
  ```
346
332
 
347
333
  All the parameters are optional:
@@ -352,6 +338,7 @@ All the parameters are optional:
352
338
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
353
339
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
354
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
355
342
 
356
343
  ### Modelling
357
344
 
@@ -394,6 +381,8 @@ export_limit: None # maximum export power in kW. None uses the inver
394
381
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
395
382
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
396
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
397
386
  inverter_power: None # inverter power consumption in W (dynamically set)
398
387
  bms_power: 50 # BMS power consumption in W
399
388
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -804,6 +793,21 @@ This setting can be:
804
793
 
805
794
  # Version Info
806
795
 
796
+ 2.7.1<br>
797
+ Update charge_needed() so it only gets generation history if there is no forecast to reduce API calls and save time.
798
+ Update default parameter values for set_charge() so the other time period is cleared if you only set 1 time.
799
+ Fix problem where a full charge was being set when charge_needed() is called with force_charge=1.
800
+ Move charging to the end of the charge time when force_charge=1 so the charge time completes with the required charge.
801
+ Update battery predictions to more accurately reflect what happens when SoC gets to min_soc or fd_soc.
802
+ Correct model to use inverter operating losses instead of BMS losses when the battery is above min_soc.
803
+ Correct exception in Solcast and Solar when a forecast is not available.
804
+
805
+
806
+ 2.7.0<br>
807
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
808
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
809
+ Correct problem with missing periods of actual data in forecast.compare()
810
+
807
811
  2.6.9<br>
808
812
  Add get and set_named_settings() (for WorkMode and ExportLimit).
809
813
  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.1"
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: 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.0"
13
+ version = "1.8.2"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -798,10 +798,10 @@ 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
- 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:
@@ -1748,9 +1750,7 @@ def plot_raw(result, plot=1, station=0):
1748
1750
  def report_value_profile(result):
1749
1751
  if type(result) is not list or result[0]['type'] != 'day':
1750
1752
  return (None, None)
1751
- data = []
1752
- for h in range(0,24):
1753
- data.append((0.0, 0)) # value sum, count of values
1753
+ data = [(0.0, 0) for h in range(0,24)]
1754
1754
  totals = 0
1755
1755
  n = 0
1756
1756
  for day in result:
@@ -1780,6 +1780,30 @@ def report_value_profile(result):
1780
1780
  # forwards compatibility
1781
1781
  get_history = get_raw
1782
1782
 
1783
+ # rescale history data based on time and steps
1784
+ def rescale_history(data, steps):
1785
+ if data is None or len(data) < 1:
1786
+ return None
1787
+ result = [None for i in range(0, 24 * steps)]
1788
+ bst = 1 if 'BST' in data[0]['time'] else 0
1789
+ average = 0.0
1790
+ n = 0
1791
+ i = 0
1792
+ for d in data:
1793
+ h = round_time(time_hours(d['time'][11:]) + bst)
1794
+ new_i = int(h * steps)
1795
+ if new_i != i and i < len(result):
1796
+ result[i] = average / n if n > 0 else None
1797
+ average = 0.0
1798
+ n = 0
1799
+ i = new_i
1800
+ if d['value'] is not None:
1801
+ average += d['value']
1802
+ n += 1
1803
+ if n > 0 and i < len(result):
1804
+ result[i] = average / n
1805
+ return result
1806
+
1783
1807
  ##################################################################################################
1784
1808
  # get energy report data in kWh
1785
1809
  ##################################################################################################
@@ -2752,8 +2776,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
2752
2776
  strategy = get_strategy(timed_mode=timed_mode)
2753
2777
  for i in range(0, run_time):
2754
2778
  h = time_line[i]
2755
- 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,
2756
- '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}
2757
2781
  if strategy is not None:
2758
2782
  period['mode'] = 'SelfUse'
2759
2783
  for d in strategy:
@@ -2780,41 +2804,55 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2780
2804
  global charge_config, steps_per_hour
2781
2805
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2782
2806
  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']
2807
+ charge_loss = charge_config['_charge_loss']
2808
+ discharge_loss = charge_config['_discharge_loss']
2785
2809
  charge_limit = charge_config['charge_limit']
2786
2810
  float_charge = charge_config['float_charge']
2787
2811
  run_time = len(work_mode_timed)
2788
2812
  for i in range(0, run_time):
2789
2813
  w = work_mode_timed[i]
2790
2814
  w['kwh'] = kwh_current
2815
+ kwh_next = kwh_current
2791
2816
  max_now = w['max_soc'] * capacity / 100
2792
- if kwh_current < max_now and w['charge'] > 0.0:
2793
- kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2794
- kwh_current = max_now if kwh_current > max_now else kwh_current
2795
- kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2796
- if kwh_current > capacity:
2797
- # battery is full
2798
- kwh_current = capacity
2799
- w = work_mode_timed[i+1] if (i + 1) < run_time else w
2800
- min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
2817
+ min_soc_now = w['min_soc']
2801
2818
  reserve_now = capacity * min_soc_now / 100
2802
- 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):
2803
2837
  # battery is empty, check if charge is needed
2804
- reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2805
- reserve_drain = kwh_current if reserve_drain is None or kwh_current > reserve_drain else reserve_drain
2806
- 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
2807
2841
  if reserve_drain <= reserve_limit:
2842
+ # float charge
2808
2843
  reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
2844
+ kwh_next = reserve_drain
2809
2845
  else:
2810
2846
  # BMS power drain
2847
+ kwh_next = reserve_drain
2811
2848
  reserve_drain -= bms_loss / steps_per_hour
2812
2849
  else:
2813
2850
  # reset drain level
2814
2851
  reserve_drain = reserve_now
2815
- if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2816
- kwh_min = kwh_current
2817
- 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)
2818
2856
 
2819
2857
  # use work_mode_timed to generate time periods for the inverter schedule
2820
2858
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
@@ -2874,6 +2912,8 @@ charge_config = {
2874
2912
  'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
2875
2913
  'pv_loss': 0.950, # loss converting PV power to DC battery charge power
2876
2914
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2915
+ 'charge_loss': None, # loss converting charge energy to stored energy
2916
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2877
2917
  'inverter_power': 101, # Inverter power consumption in W
2878
2918
  'bms_power': 50, # BMS power consumption in W
2879
2919
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -3020,14 +3060,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3020
3060
  output(f"full_charge = {full_charge}")
3021
3061
  if test_soc is not None:
3022
3062
  current_soc = test_soc
3023
- capacity = 14.53
3063
+ capacity = 14.46
3024
3064
  residual = test_soc * capacity / 100
3025
3065
  bat_volt = 317.4
3026
3066
  bat_power = 0.0
3027
3067
  temperature = 30
3028
3068
  bms_charge_current = 15
3029
- charge_loss = battery_params[2]['charge_loss']
3030
- discharge_loss = battery_params[2]['discharge_loss']
3069
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
3070
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
3031
3071
  bat_current = 0.0
3032
3072
  device_power = 6.0
3033
3073
  device_current = 35
@@ -3049,8 +3089,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3049
3089
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
3050
3090
  return None
3051
3091
  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
3092
+ 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
3093
+ 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
3094
  device_power = device.get('power')
3055
3095
  device_current = device.get('max_charge_current')
3056
3096
  model = device.get('deviceType')
@@ -3079,7 +3119,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3079
3119
  output(f" Temperature: {temperature:.1f}°C")
3080
3120
  output(f" Resistance: {bat_resistance:.2f} ohms")
3081
3121
  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")
3122
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
3083
3123
  # inverter losses
3084
3124
  inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
3085
3125
  operating_loss = inverter_power / 1000
@@ -3108,8 +3148,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3108
3148
  charge_config['charge_limit'] = charge_limit
3109
3149
  charge_config['charge_power'] = charge_power
3110
3150
  charge_config['float_charge'] = float_charge
3111
- charge_config['charge_loss'] = charge_loss
3112
- charge_config['discharge_loss'] = discharge_loss
3151
+ charge_config['_charge_loss'] = charge_loss
3152
+ charge_config['_discharge_loss'] = discharge_loss
3113
3153
  # display what we have
3114
3154
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
3115
3155
  output(f"\nDevice Info:")
@@ -3171,27 +3211,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3171
3211
  output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
3172
3212
  if solcast_value is None and solar_value is None and debug_setting > 1:
3173
3213
  output(f"\nNo forecasts available at this time")
3174
- # get generation data
3175
- generation = None
3176
- last_date = today if hour_now >= charge_config['use_today'] else yesterday
3177
- gen_days = charge_config['generation_days']
3178
- history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
3179
- pv_history = {}
3180
- if history is not None and len(history) > 0:
3181
- for day in history:
3182
- date = day['date']
3183
- if pv_history.get(date) is None:
3184
- pv_history[date] = 0.0
3185
- if day.get('kwh') is not None and day.get('kwh_neg') is not None:
3186
- pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
3187
- pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
3188
- output(f"\nGeneration (kWh):")
3189
- s = ""
3190
- for d in sorted(pv_history.keys())[-gen_days:]:
3191
- s += f" {d} {pv_history[d]:4.1f},"
3192
- output(' ' + s[:-1])
3193
- generation = pv_sum / gen_days
3194
- output(f" Average of last {gen_days} days: {generation:.1f}kWh")
3195
3214
  # choose expected value and produce generation time line
3196
3215
  quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
3197
3216
  sun_name = seasonal_sun[quarter]['name']
@@ -3209,11 +3228,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3209
3228
  elif solar_value is not None:
3210
3229
  expected = solar_value
3211
3230
  generation_timed = solar_timed
3212
- elif generation is None or generation == 0.0:
3213
- output(f"\nNo generation data available")
3214
- output_close()
3215
- return None
3216
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
3217
3257
  expected = generation
3218
3258
  generation_timed = [expected * x / sun_sum for x in sun_timed]
3219
3259
  if charge_config['forecast_selection'] == 1 and update_settings > 0:
@@ -3221,7 +3261,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3221
3261
  update_settings = 0
3222
3262
  # produce time lines for charge, discharge and work mode
3223
3263
  charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
3224
- 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]
3225
3265
  work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
3226
3266
  for i in range(0, len(work_mode_timed)):
3227
3267
  # get work mode
@@ -3233,19 +3273,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3233
3273
  work_mode_timed[i]['charge'] = charge_power * duration
3234
3274
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
3235
3275
  fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
3236
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
3237
- 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
3238
3277
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
3239
- discharge_timed[i] = bms_loss
3240
- if timed_mode > 1:
3241
- work_mode_timed[i]['hold'] = 1
3278
+ discharge_timed[i] = operating_loss
3279
+ work_mode_timed[i]['hold'] = 1
3242
3280
  elif timed_mode > 0 and work_mode == 'Backup':
3243
- 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
3244
3282
  elif timed_mode > 0 and work_mode == 'Feedin':
3245
- (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]),
3246
3284
  0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
3247
3285
  else: # work_mode == 'SelfUse'
3248
- (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]),
3249
3287
  0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
3250
3288
  work_mode_timed[i]['pv'] = charge_timed[i]
3251
3289
  work_mode_timed[i]['discharge'] = discharge_timed[i]
@@ -3258,8 +3296,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3258
3296
  kwh_contingency = consumption * contingency / 100
3259
3297
  kwh_needed = reserve + kwh_contingency - kwh_min
3260
3298
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
3261
- start_soc = int(start_residual / capacity * 100 + 0.5)
3262
- 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)
3263
3300
  target_soc = charge_config.get('target_soc')
3264
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
3265
3302
  if target_kwh > (end_residual + kwh_needed):
@@ -3278,7 +3315,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3278
3315
  hours = 0.0
3279
3316
  start_timed = time_to_end
3280
3317
  end_timed = time_to_end
3281
- end_soc = int(end_residual / capacity * 100 + 0.5)
3282
3318
  else:
3283
3319
  # work out time to add kwh_needed to battery
3284
3320
  charge_rate = charge_power * charge_loss
@@ -3289,30 +3325,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3289
3325
  charge_message = "with charge added"
3290
3326
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3291
3327
  # check if charge time exceeded or charge needed exceeds capacity
3292
- hours_to_full = (capacity - start_residual) / charge_rate
3293
- if hours > charge_time:
3328
+ hours_to_full = (capacity - end_residual) / charge_rate
3329
+ if hours > charge_time or bat_hold == 2:
3294
3330
  hours = charge_time
3295
3331
  elif hours > hours_to_full:
3296
- 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
3297
3333
  required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
3298
3334
  hours = required if required > hours and required < charge_time else charge_time
3299
- # round charge time and work out what will actually be added
3335
+ # round charge time
3300
3336
  min_hours = charge_config['min_hours']
3301
3337
  hours = int(hours / min_hours + 0.99) * min_hours
3302
- kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
3303
- kwh_added += discharge_rate * hours # discharge saved by charging
3304
- kwh_spare = kwh_min - reserve + kwh_added
3305
3338
  # rework charge and discharge
3306
3339
  charge_period = get_best_charge_period(start_at, hours)
3307
- 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
3308
3341
  price = charge_period.get('price') if charge_period is not None else None
3309
3342
  start_timed = time_to_start + charge_offset * steps_per_hour
3310
3343
  end_timed = start_timed + hours * steps_per_hour
3311
3344
  start_residual = interpolate(start_timed, bat_timed)
3312
- end_soc = min([int((start_residual + kwh_added) / capacity * 100 + 0.5), 100])
3313
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
3314
- output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
3315
- + (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)")
3316
3352
  for i in range(int(time_to_start), int(time_to_end)):
3317
3353
  j = i + 1
3318
3354
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3335,8 +3371,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3335
3371
  # rebuild the battery residual with the charge added and min_soc
3336
3372
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
3337
3373
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3374
+ end_soc = end_residual / capacity * 100
3338
3375
  # show the results
3339
- 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)")
3340
3377
  output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
3341
3378
  if not charge_today:
3342
3379
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
@@ -4304,17 +4341,9 @@ class Solcast :
4304
4341
  total_actual = None
4305
4342
  self.actual = get_history('day', d=day, v=v)
4306
4343
  plots = {}
4344
+ times = [i/2 for i in range(0, 48)]
4307
4345
  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
4346
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4318
4347
  if v['variable'] == 'pvPower':
4319
4348
  total_actual = v.get('kwh')
4320
4349
  if total_actual is None:
@@ -4340,16 +4369,16 @@ class Solcast :
4340
4369
  estimate_values = [self.estimate[r][hours_time(t)] for t in times]
4341
4370
  plots[r] = estimate_values
4342
4371
  total_forecast = 0.0
4343
- if self.daily.get(day) is not None:
4372
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4344
4373
  sun_times = get_suntimes(day)
4345
4374
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4346
4375
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
4347
4376
  total_forecast = sum(forecast_values) / 2
4348
4377
  plots['forecast'] = forecast_values
4349
- if total_actual is not None:
4350
- print(f" Total actual: {total_actual:.3f}kWh")
4351
4378
  if total_forecast is not None:
4352
4379
  print(f" Total forecast: {total_forecast:.3f}kWh")
4380
+ if total_actual is not None:
4381
+ print(f" Total actual: {total_actual:.3f}kWh")
4353
4382
  print()
4354
4383
  title = f"Forecast / Actual PV Power on {day}"
4355
4384
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4637,17 +4666,9 @@ class Solar :
4637
4666
  total_actual = None
4638
4667
  self.actual = get_history('day', d=day, v=v)
4639
4668
  plots = {}
4669
+ times = [i/2 for i in range(0, 48)]
4640
4670
  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
4671
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4651
4672
  if v['variable'] == 'pvPower':
4652
4673
  total_actual = v.get('kwh')
4653
4674
  if total_actual is None:
@@ -4671,16 +4692,16 @@ class Solar :
4671
4692
  estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
4672
4693
  plots[r] = estimate_values
4673
4694
  total_forecast = 0.0
4674
- if self.daily.get(day) is not None:
4695
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4675
4696
  sun_times = get_suntimes(day)
4676
4697
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4677
4698
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4678
4699
  total_forecast = sum(forecast_values) / 2
4679
4700
  plots['forecast'] = forecast_values
4680
- if total_actual is not None:
4681
- print(f" Total actual: {total_actual:.3f}kWh")
4682
4701
  if total_forecast is not None:
4683
4702
  print(f" Total forecast: {total_forecast:.3f}kWh")
4703
+ if total_actual is not None:
4704
+ print(f" Total actual: {total_actual:.3f}kWh")
4684
4705
  print()
4685
4706
  title = f"Forecast / Actual PV Power on {day}"
4686
4707
  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: 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.6.9"
13
+ version = "2.7.1"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -671,10 +671,10 @@ 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
- 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
@@ -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
@@ -2417,8 +2439,8 @@ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, cur
2417
2439
  strategy = get_strategy(timed_mode=timed_mode)
2418
2440
  for i in range(0, run_time):
2419
2441
  h = time_line[i]
2420
- 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,
2421
- '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}
2422
2444
  if strategy is not None:
2423
2445
  period['mode'] = 'SelfUse'
2424
2446
  for d in strategy:
@@ -2444,41 +2466,55 @@ 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)
2452
2474
  for i in range(0, run_time):
2453
2475
  w = work_mode_timed[i]
2454
2476
  w['kwh'] = kwh_current
2477
+ kwh_next = kwh_current
2455
2478
  max_now = w['max_soc'] * capacity / 100
2456
- if kwh_current < max_now and w['charge'] > 0.0:
2457
- kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2458
- kwh_current = max_now if kwh_current > max_now else kwh_current
2459
- kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2460
- if kwh_current > capacity:
2461
- # battery is full
2462
- kwh_current = capacity
2463
- w = work_mode_timed[i+1] if (i + 1) < run_time else w
2464
- min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
2479
+ min_soc_now = w['min_soc']
2465
2480
  reserve_now = capacity * min_soc_now / 100
2466
- 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):
2467
2499
  # battery is empty, check if charge is needed
2468
- reserve_limit = capacity * (min_soc_now - allowed_drain) / 100
2469
- reserve_drain = kwh_current if reserve_drain is None or kwh_current > reserve_drain else reserve_drain
2470
- 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
2471
2503
  if reserve_drain <= reserve_limit:
2504
+ # float charge
2472
2505
  reserve_drain = min([reserve_now, reserve_drain + float_charge * charge_loss / steps_per_hour])
2506
+ kwh_next = reserve_drain
2473
2507
  else:
2474
2508
  # BMS power drain
2509
+ kwh_next = reserve_drain
2475
2510
  reserve_drain -= bms_loss / steps_per_hour
2476
2511
  else:
2477
2512
  # reset drain level
2478
2513
  reserve_drain = reserve_now
2479
- if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2480
- kwh_min = kwh_current
2481
- 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)
2482
2518
 
2483
2519
  # use work_mode_timed to generate time periods for the inverter schedule
2484
2520
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
@@ -2538,6 +2574,8 @@ charge_config = {
2538
2574
  'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
2539
2575
  'pv_loss': 0.95, # loss converting PV power to DC battery charge power
2540
2576
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2577
+ 'charge_loss': None, # loss converting charge energy to stored energy
2578
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2541
2579
  'inverter_power': 101, # Inverter power consumption in W
2542
2580
  'bms_power': 50, # BMS power consumption in W
2543
2581
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2684,14 +2722,14 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2684
2722
  output(f"full_charge = {full_charge}")
2685
2723
  if test_soc is not None:
2686
2724
  current_soc = test_soc
2687
- capacity = 14.54
2725
+ capacity = 14.46
2688
2726
  residual = test_soc * capacity / 100
2689
2727
  bat_volt = 317.4
2690
2728
  bat_power = 0.0
2691
2729
  temperature = 30
2692
2730
  bms_charge_current = 15
2693
- charge_loss = battery_params[2]['charge_loss']
2694
- discharge_loss = battery_params[2]['discharge_loss']
2731
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
2732
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
2695
2733
  bat_current = 0.0
2696
2734
  device_power = 6.0
2697
2735
  device_current = 35
@@ -2713,8 +2751,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2713
2751
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
2714
2752
  return None
2715
2753
  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
2754
+ 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
2755
+ 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
2756
  device_power = device.get('power')
2719
2757
  device_current = device.get('max_charge_current')
2720
2758
  model = device.get('deviceType')
@@ -2743,6 +2781,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2743
2781
  output(f" Max Charge: {charge_current:.1f}A")
2744
2782
  output(f" Resistance: {bat_resistance:.2f} ohms")
2745
2783
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
2784
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
2746
2785
  # charge current may be derated based on temperature
2747
2786
  charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
2748
2787
  if charge_current > bms_charge_current:
@@ -2775,8 +2814,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2775
2814
  charge_config['charge_limit'] = charge_limit
2776
2815
  charge_config['charge_power'] = charge_power
2777
2816
  charge_config['float_charge'] = float_charge
2778
- charge_config['charge_loss'] = charge_loss
2779
- charge_config['discharge_loss'] = discharge_loss
2817
+ charge_config['_charge_loss'] = charge_loss
2818
+ charge_config['_discharge_loss'] = discharge_loss
2780
2819
  # display what we have
2781
2820
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
2782
2821
  output(f"\nDevice Info:")
@@ -2837,27 +2876,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2837
2876
  output(f"\nSolar: {tomorrow} {fsolar.daily[tomorrow]['kwh']:.1f}kWh")
2838
2877
  if solcast_value is None and solar_value is None and debug_setting > 1:
2839
2878
  output(f"\nNo forecasts available at this time")
2840
- # get generation data
2841
- generation = None
2842
- last_date = today if hour_now >= charge_config['use_today'] else yesterday
2843
- gen_days = charge_config['generation_days']
2844
- history = get_raw('week', d=last_date, v=['pvPower','meterPower2'], summary=2)
2845
- pv_history = {}
2846
- if history is not None and len(history) > 0:
2847
- for day in history:
2848
- date = day['date']
2849
- if pv_history.get(date) is None:
2850
- pv_history[date] = 0.0
2851
- if day.get('kwh') is not None and day.get('kwh_neg') is not None:
2852
- pv_history[date] += day['kwh_neg'] / 0.92 if day['variable'] == 'meterPower2' else day['kwh']
2853
- pv_sum = sum([pv_history[d] for d in sorted(pv_history.keys())[-gen_days:]])
2854
- output(f"\nGeneration (kWh):")
2855
- s = ""
2856
- for d in sorted(pv_history.keys())[-gen_days:]:
2857
- s += f" {d}: {pv_history[d]:4.1f},"
2858
- output(' ' + s[:-1])
2859
- generation = pv_sum / gen_days
2860
- output(f" Average of last {gen_days} days: {generation:.1f}kWh")
2861
2879
  # choose expected value and produce generation time line
2862
2880
  quarter = int(today[5:7] if charge_today else tomorrow[5:7]) // 3 % 4
2863
2881
  sun_name = seasonal_sun[quarter]['name']
@@ -2875,11 +2893,32 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2875
2893
  elif solar_value is not None:
2876
2894
  expected = solar_value
2877
2895
  generation_timed = solar_timed
2878
- elif generation is None or generation == 0.0:
2879
- output(f"\nNo generation data available")
2880
- output_close()
2881
- return None
2882
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
2883
2922
  expected = generation
2884
2923
  generation_timed = [expected * x / sun_sum for x in sun_timed]
2885
2924
  if charge_config['forecast_selection'] == 1 and update_settings > 0:
@@ -2887,7 +2926,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2887
2926
  update_settings = 0
2888
2927
  # produce time lines for charge, discharge and work mode
2889
2928
  charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
2890
- 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]
2891
2930
  work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
2892
2931
  for i in range(0, len(work_mode_timed)):
2893
2932
  # get work mode
@@ -2899,19 +2938,17 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2899
2938
  work_mode_timed[i]['charge'] = charge_power * duration
2900
2939
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
2901
2940
  fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
2902
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
2903
- 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
2904
2942
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
2905
- discharge_timed[i] = bms_loss
2906
- if timed_mode > 1:
2907
- work_mode_timed[i]['hold'] = 1
2943
+ discharge_timed[i] = operating_loss
2944
+ work_mode_timed[i]['hold'] = 1
2908
2945
  elif timed_mode > 0 and work_mode == 'Backup':
2909
- 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
2910
2947
  elif timed_mode > 0 and work_mode == 'Feedin':
2911
- (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]),
2912
2949
  0.0 if (charge_timed[i] <= export_limit + discharge_timed[i]) else (charge_timed[i] - export_limit - discharge_timed[i]))
2913
2950
  else: # work_mode == 'SelfUse'
2914
- (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]),
2915
2952
  0.0 if (charge_timed[i] <= discharge_timed[i]) else (charge_timed[i] - discharge_timed[i]))
2916
2953
  work_mode_timed[i]['pv'] = charge_timed[i]
2917
2954
  work_mode_timed[i]['discharge'] = discharge_timed[i]
@@ -2924,7 +2961,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2924
2961
  kwh_contingency = consumption * contingency / 100
2925
2962
  kwh_needed = reserve + kwh_contingency - kwh_min
2926
2963
  start_residual = interpolate(time_to_start, bat_timed) # residual when charge time starts
2927
- start_soc = int(start_residual / capacity * 100 + 0.5)
2928
2964
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends without charging
2929
2965
  target_soc = charge_config.get('target_soc')
2930
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
@@ -2944,7 +2980,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2944
2980
  hours = 0.0
2945
2981
  start_timed = time_to_end
2946
2982
  end_timed = time_to_end
2947
- end_soc = int(end_residual / capacity * 100 + 0.5)
2948
2983
  else:
2949
2984
  # work out time to add kwh_needed to battery
2950
2985
  charge_rate = charge_power * charge_loss
@@ -2955,30 +2990,30 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2955
2990
  charge_message = "with charge added"
2956
2991
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2957
2992
  # check if charge time exceeded or charge needed exceeds capacity
2958
- hours_to_full = (capacity - start_residual) / charge_rate
2959
- if hours > charge_time:
2993
+ hours_to_full = (capacity - end_residual) / charge_rate
2994
+ if hours > charge_time or bat_hold == 2:
2960
2995
  hours = charge_time
2961
2996
  elif hours > hours_to_full:
2962
- 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
2963
2998
  required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
2964
2999
  hours = required if required > hours and required < charge_time else charge_time
2965
- # round charge time and work out what will actually be added
3000
+ # round charge time
2966
3001
  min_hours = charge_config['min_hours']
2967
3002
  hours = int(hours / min_hours + 0.99) * min_hours
2968
- kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
2969
- kwh_added += discharge_rate * hours # discharge saved during charging
2970
- kwh_spare = kwh_min - reserve + kwh_added
2971
3003
  # rework charge and discharge
2972
3004
  charge_period = get_best_charge_period(start_at, hours)
2973
- 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
2974
3006
  price = charge_period.get('price') if charge_period is not None else None
2975
3007
  start_timed = time_to_start + charge_offset * steps_per_hour
2976
3008
  end_timed = start_timed + hours * steps_per_hour
2977
3009
  start_residual = interpolate(start_timed, bat_timed)
2978
- end_soc = min([int((start_residual + kwh_added) / capacity * 100 + 0.5), 100])
2979
- output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(start_timed, time_line))} ({start_residual:.2f}kWh)")
2980
- output(f" Charge to: {end_soc:.0f}% {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}"
2981
- + (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)")
2982
3017
  for i in range(int(time_to_start), int(time_to_end)):
2983
3018
  j = i + 1
2984
3019
  # work out time (fraction of hour) when charging in hour from i to j
@@ -3001,8 +3036,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3001
3036
  # rebuild the battery residual with any charge added and min_soc
3002
3037
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next=start_timed)
3003
3038
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3039
+ end_soc = end_residual / capacity * 100
3004
3040
  # show the results
3005
- 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)")
3006
3042
  output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
3007
3043
  if not charge_today:
3008
3044
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
@@ -3967,17 +4003,9 @@ class Solcast :
3967
4003
  total_actual = None
3968
4004
  self.actual = get_history('day', d=day, v=v)
3969
4005
  plots = {}
4006
+ times = [i/2 for i in range(0, 48)]
3970
4007
  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
4008
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
3981
4009
  if v['variable'] == 'pvPower':
3982
4010
  total_actual = v.get('kwh')
3983
4011
  if total_actual is None:
@@ -4003,16 +4031,16 @@ class Solcast :
4003
4031
  estimate_values = [self.estimate[r][hours_time(t)] for t in times]
4004
4032
  plots[r] = estimate_values
4005
4033
  total_forecast = 0.0
4006
- if self.daily.get(day) is not None:
4034
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4007
4035
  sun_times = get_suntimes(day)
4008
4036
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4009
4037
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
4010
4038
  total_forecast = sum(forecast_values) / 2
4011
4039
  plots['forecast'] = forecast_values
4012
- if total_actual is not None:
4013
- print(f" Total actual: {total_actual:.3f}kWh")
4014
4040
  if total_forecast is not None:
4015
4041
  print(f" Total forecast: {total_forecast:.3f}kWh")
4042
+ if total_actual is not None:
4043
+ print(f" Total actual: {total_actual:.3f}kWh")
4016
4044
  print()
4017
4045
  title = f"Forecast / Actual PV Power on {day}"
4018
4046
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4300,17 +4328,9 @@ class Solar :
4300
4328
  total_actual = None
4301
4329
  self.actual = get_history('day', d=day, v=v)
4302
4330
  plots = {}
4331
+ times = [i/2 for i in range(0, 48)]
4303
4332
  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
4333
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4314
4334
  if v['variable'] == 'pvPower':
4315
4335
  total_actual = v.get('kwh')
4316
4336
  if total_actual is None:
@@ -4334,16 +4354,16 @@ class Solar :
4334
4354
  estimate_values = [c_float(self.estimate[r].get(hours_time(t))) for t in times]
4335
4355
  plots[r] = estimate_values
4336
4356
  total_forecast = 0.0
4337
- if self.daily.get(day) is not None:
4357
+ if hasattr(self, 'daily') and self.daily.get(day) is not None:
4338
4358
  sun_times = get_suntimes(day)
4339
4359
  print(f"\n{day}:\n Sunrise {sun_times[0]}, Sunset {sun_times[1]}")
4340
4360
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4341
4361
  total_forecast = sum(forecast_values) / 2
4342
4362
  plots['forecast'] = forecast_values
4343
- if total_actual is not None:
4344
- print(f" Total actual: {total_actual:.3f}kWh")
4345
4363
  if total_forecast is not None:
4346
4364
  print(f" Total forecast: {total_forecast:.3f}kWh")
4365
+ if total_actual is not None:
4366
+ print(f" Total actual: {total_actual:.3f}kWh")
4347
4367
  print()
4348
4368
  title = f"Forecast / Actual PV Power on {day}"
4349
4369
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: foxesscloud
3
+ Version: 2.7.1
4
+ Summary: library for accessing Fox ESS cloud data using Open API
5
+ Author-email: Tony Matthews <tony@quasair.co.uk>
6
+ Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
7
+ Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+
1
15
  # FoxESS-Cloud
2
16
 
3
17
  <a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
@@ -144,12 +158,12 @@ set_min() applies new SoC settings to the inverter. The parameters update batter
144
158
  + minSoc: min Soc setting e.g. 10 = 10%
145
159
 
146
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:
147
- + ch1: enable charge from grid for period 1 (True or False)
148
- + st1: the start time for period 1
149
- + en1: the end time for period 1
150
- + ch2: enable charge from grid for period 2 (True or False)
151
- + st2: the start time for period 2
152
- + 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)
153
167
  + enable: set to 0 to show settings but stop inverter settings being updated. Default is 1.
154
168
 
155
169
  set_period() returns a period structure that can be used to build a list for set_schedule()
@@ -327,7 +341,7 @@ The previous section provides functions that can be used to access and control y
327
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.
328
342
 
329
343
  ```
330
- 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)
331
345
  ```
332
346
 
333
347
  All the parameters are optional:
@@ -338,6 +352,7 @@ All the parameters are optional:
338
352
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
339
353
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
340
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
341
356
 
342
357
  ### Modelling
343
358
 
@@ -380,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
380
395
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
381
396
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
382
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
383
400
  inverter_power: None # inverter power consumption in W (dynamically set)
384
401
  bms_power: 50 # BMS power consumption in W
385
402
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -790,6 +807,21 @@ This setting can be:
790
807
 
791
808
  # Version Info
792
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
+
820
+ 2.7.0<br>
821
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
822
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
823
+ Correct problem with missing periods of actual data in forecast.compare()
824
+
793
825
  2.6.9<br>
794
826
  Add get and set_named_settings() (for WorkMode and ExportLimit).
795
827
  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