foxesscloud 2.6.4__tar.gz → 2.6.6__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.4
3
+ Version: 2.6.6
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
@@ -111,6 +111,7 @@ f.get_charge()
111
111
  f.get_min()
112
112
  f.get_flag()
113
113
  f.get_schedule()
114
+ f.get_named_settings(name)
114
115
 
115
116
  ```
116
117
  Each of these calls will return a dictionary or list containing the relevant information.
@@ -130,6 +131,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
130
131
 
131
132
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
132
133
 
134
+ get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
135
+ + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
136
+ + the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
137
+
133
138
 
134
139
  ## Inverter Settings
135
140
  You can change inverter settings using:
@@ -140,6 +145,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
140
145
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
141
146
  f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
142
147
  f.set_schedule(periods, enable)
148
+ f.set_named_settings(name, value)
143
149
  ```
144
150
 
145
151
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -180,6 +186,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
180
186
  + periods: a time segment or list of time segments created using f.set_period().
181
187
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
182
188
 
189
+ set_named_settings() sets the 'name' setting to 'value'.
190
+ + 'name' may also be a list of (name, value) pairs.
191
+ + the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
192
+
183
193
 
184
194
  ## Real Time Data
185
195
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -787,6 +797,21 @@ This setting can be:
787
797
 
788
798
  # Version Info
789
799
 
800
+ 2.6.6<br>
801
+ Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
802
+ Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
803
+ Allow unlimited periods in strategy, including overlap with charge periods but warn and limit if the periods sent to inverter would be more than 8.
804
+ Improve behaviour prediction for schedules when clocks change due to day light saving.
805
+ Improve schedule generation and prediction when Min Soc changes.
806
+ Cache Solcast RIDS to reduce API usage (run with reload=1 if arrays are edited and cached RIDs need to be updated).
807
+ Remove spurious error message when (failing) to get inverter work mode.
808
+
809
+ 2.6.5<br>
810
+ Add get_named_settings() and set_named_settings().
811
+ Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
812
+ Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
813
+ Update calibration of 'charge_loss' and 'discharge_loss'.
814
+
790
815
  2.6.4<br>
791
816
  Increase default plungs_slots from 6 to 8.
792
817
  Correct battery capacity in get_batteries().
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.6.4
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>
@@ -111,6 +97,7 @@ f.get_charge()
111
97
  f.get_min()
112
98
  f.get_flag()
113
99
  f.get_schedule()
100
+ f.get_named_settings(name)
114
101
 
115
102
  ```
116
103
  Each of these calls will return a dictionary or list containing the relevant information.
@@ -130,6 +117,10 @@ get_flag() returns the current scheduler enable / support / maxsoc flags
130
117
 
131
118
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
132
119
 
120
+ get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
121
+ + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
122
+ + the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
123
+
133
124
 
134
125
  ## Inverter Settings
135
126
  You can change inverter settings using:
@@ -140,6 +131,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
140
131
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
141
132
  f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
142
133
  f.set_schedule(periods, enable)
134
+ f.set_named_settings(name, value)
143
135
  ```
144
136
 
145
137
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -180,6 +172,10 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
180
172
  + periods: a time segment or list of time segments created using f.set_period().
181
173
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
182
174
 
175
+ set_named_settings() sets the 'name' setting to 'value'.
176
+ + 'name' may also be a list of (name, value) pairs.
177
+ + the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
178
+
183
179
 
184
180
  ## Real Time Data
185
181
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -787,6 +783,21 @@ This setting can be:
787
783
 
788
784
  # Version Info
789
785
 
786
+ 2.6.6<br>
787
+ Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
788
+ Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
789
+ Allow unlimited periods in strategy, including overlap with charge periods but warn and limit if the periods sent to inverter would be more than 8.
790
+ Improve behaviour prediction for schedules when clocks change due to day light saving.
791
+ Improve schedule generation and prediction when Min Soc changes.
792
+ Cache Solcast RIDS to reduce API usage (run with reload=1 if arrays are edited and cached RIDs need to be updated).
793
+ Remove spurious error message when (failing) to get inverter work mode.
794
+
795
+ 2.6.5<br>
796
+ Add get_named_settings() and set_named_settings().
797
+ Update get_work_mode() and set_work_mode() to use named settings (still doesn't work though as blocked by Fox)
798
+ Updated get_history() and get_report() saved filenames to use _history_ and _report_ for consistency.
799
+ Update calibration of 'charge_loss' and 'discharge_loss'.
800
+
790
801
  2.6.4<br>
791
802
  Increase default plungs_slots from 6 to 8.
792
803
  Correct battery capacity in get_batteries().
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.6.4"
7
+ version = "2.6.6"
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: 13 October 2024
4
+ Updated: 01 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.7.6"
13
+ version = "1.7.8"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -583,7 +583,7 @@ battery = None
583
583
  batteries = None
584
584
  battery_settings = None
585
585
 
586
- # 1 = Residual Energy, 2 = Residual Capacity
586
+ # 1 = Residual Energy, 2 = Residual Capacity (HV), 3 = Residual Capacity per battery (Mira)
587
587
  residual_handling = 1
588
588
 
589
589
  # charge rates based on residual_handling
@@ -593,13 +593,20 @@ battery_params = {
593
593
  1: {'table': [ 0, 2, 10, 15, 25, 50, 50, 50, 50, 50, 30, 20, 0],
594
594
  'step': 5,
595
595
  'offset': 5,
596
- 'charge_loss': 0.975,
597
- 'discharge_loss': 0.975},
596
+ 'charge_loss': 0.974,
597
+ 'discharge_loss': 0.974},
598
+ # HV BMS v2 with firmware 1.014 or later
598
599
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
599
600
  'step': 5,
600
601
  'offset': 5,
601
602
  'charge_loss': 1.08,
602
- 'discharge_loss': 0.85},
603
+ 'discharge_loss': 0.95},
604
+ # Mira BMS with firmware 1.014 or later
605
+ 3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
606
+ 'step': 5,
607
+ 'offset': 5,
608
+ 'charge_loss': 0.974,
609
+ 'discharge_loss': 0.974},
603
610
  }
604
611
 
605
612
  def get_battery(info=1):
@@ -632,10 +639,13 @@ def get_battery(info=1):
632
639
  output(f"** get_battery().info, no result data, {errno_message(errno)}")
633
640
  else:
634
641
  battery['info'] = result['batteries'][0]
635
- if battery['info']['masterVersion'] >= '1.014' and battery['info']['masterSN'][:7] == '60BBHV2':
642
+ if battery['info'].get('slaveBatteries') is not None:
643
+ battery['count'] = len(battery['info']['slaveBatteries'])
644
+ if battery['info']['masterSN'][:7] == '60BBHV2' and battery['info']['masterVersion'] >= '1.014':
636
645
  residual_handling = 2
646
+ elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
647
+ residual_handling = 3
637
648
  battery['residual_handling'] = residual_handling
638
- battery['rated_capacity'] = None
639
649
  battery['soh'] = None
640
650
  battery['soh_supported'] = False
641
651
  if battery.get('residual') is not None:
@@ -644,6 +654,18 @@ def get_battery(info=1):
644
654
  capacity = battery.get('residual')
645
655
  soc = battery.get('soc')
646
656
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
657
+ if battery.get('count') is None:
658
+ battery['count'] = int(battery['volt'] / 49)
659
+ if battery.get('ratedCapacity') is None:
660
+ battery['ratedCapacity'] = 2560 * battery['count']
661
+ elif battery['residual_handling'] == 3:
662
+ if battery.get('count') is None:
663
+ battery['count'] = int(battery['volt'] / 49)
664
+ capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
665
+ soc = battery.get('soc')
666
+ residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
667
+ if battery.get('ratedCapacity') is None:
668
+ battery['ratedCapacity'] = 2450 * battery['count']
647
669
  else:
648
670
  residual = battery.get('residual')
649
671
  soc = battery.get('soc')
@@ -654,6 +676,8 @@ def get_battery(info=1):
654
676
  params = battery_params[battery['residual_handling']]
655
677
  battery['charge_loss'] = params['charge_loss']
656
678
  battery['discharge_loss'] = params['discharge_loss']
679
+ if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
680
+ battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1)
657
681
  if battery.get('temperature') is not None:
658
682
  battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
659
683
  return battery
@@ -688,8 +712,13 @@ def get_batteries(info=1):
688
712
  batteries[i]['info'] = result['batteries'][i]
689
713
  for b in batteries:
690
714
  b['residual_handling'] = residual_handling
691
- if b.get('info') is not None and b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
692
- b['residual_handling'] = 2
715
+ if b.get('info') is not None:
716
+ if b['info'].get('slaveBatteries') is not None:
717
+ b['count'] = len(b['info']['slaveBatteries'])
718
+ if b['info']['masterVersion'] >= '1.014' and b['info']['masterSN'][:7] == '60BBHV2':
719
+ b['residual_handling'] = 2
720
+ elif battery['info']['masterSN'][:7] == '60MBB01' and battery['info']['masterVersion'] >= '1.014':
721
+ residual_handling = 3
693
722
  rated_capacity = b.get('ratedCapacity')
694
723
  b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else None
695
724
  soh = b.get('soh')
@@ -708,7 +737,7 @@ def get_batteries(info=1):
708
737
  soc = b.get('soc')
709
738
  residual = b['capacity'] * b['soc'] / 100
710
739
  b['residual'] = round(residual, 3)
711
- if b.get('capacity') is not None and b.get('ratedCapacity') is not None:
740
+ if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
712
741
  b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
713
742
  b['charge_rate'] = 50
714
743
  params = battery_params[b['residual_handling']]
@@ -1022,7 +1051,7 @@ def get_remote_settings(key):
1022
1051
  result = response.json().get('result')
1023
1052
  if result is None:
1024
1053
  errno = response.json().get('errno')
1025
- output(f"** get_remote_settings(), no result data, {errno_message(errno)}")
1054
+ output(f"** get_remote_settings(), no result data, {errno_message(response)}")
1026
1055
  return None
1027
1056
  values = result.get('values')
1028
1057
  if values is None:
@@ -1323,10 +1352,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1323
1352
  price = segment.get('price')
1324
1353
  start = time_hours(start)
1325
1354
  # adjust exclusive time to inclusive
1326
- end = round_time(time_hours(end) - 1/60)
1355
+ end = time_hours(end)
1327
1356
  if start is None or end is None or start >= end:
1328
1357
  output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
1329
1358
  return None
1359
+ end = round_time(end - 1/60)
1330
1360
  mode = 'SelfUse' if mode is None else mode
1331
1361
  if mode not in work_modes:
1332
1362
  output(f"** mode must be one of {work_modes}")
@@ -1399,7 +1429,7 @@ def set_schedule(periods=None, template=None, enable=True):
1399
1429
  if len(periods) > 8:
1400
1430
  output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
1401
1431
  return None
1402
- data = {'pollcy': periods, 'deviceSN': device_sn}
1432
+ data = {'pollcy': periods[-8:], 'deviceSN': device_sn}
1403
1433
  schedule['pollcy'] = periods
1404
1434
  schedule['template_id'] = None
1405
1435
  elif template is not None:
@@ -1436,7 +1466,7 @@ def set_schedule(periods=None, template=None, enable=True):
1436
1466
  # d = day 'YYYY-MM-DD'. Can also include 'HH:MM' in 'hour' mode
1437
1467
  # v = list of variables to get
1438
1468
  # summary = 0: raw data, 1: add max, min, sum, 2: summarise and drop raw data, 3: calculate state
1439
- # save = "xxxxx": save the raw results to xxxxx_raw_<time_span>_<d>.json
1469
+ # save = "xxxxx": save the raw results to xxxxx_history_<time_span>_<d>.json
1440
1470
  # load = "<file>": load the raw results from <file>
1441
1471
  # plot = 0: no plot, 1: plot variables separately, 2: combine variables
1442
1472
  # station = 0: use device_id, 1: use station_id
@@ -1504,7 +1534,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
1504
1534
  result = json.load(file)
1505
1535
  file.close()
1506
1536
  if save is not None:
1507
- file_name = save + "_raw_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
1537
+ file_name = save + "_history_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
1508
1538
  file = open(storage + file_name, 'w', encoding='utf-8')
1509
1539
  json.dump(result, file, indent=4, ensure_ascii= False)
1510
1540
  file.close()
@@ -1709,7 +1739,7 @@ get_history = get_raw
1709
1739
  # d = day 'YYYY-MM-DD'
1710
1740
  # v = list of report variables to get
1711
1741
  # summary = 0, 1, 2: do a quick total energy report for a day
1712
- # save = "xxxxx": save the report results to xxxxx_raw_<time_span>_<d>.json
1742
+ # save = "xxxxx": save the report results to xxxxx_report_<time_span>_<d>.json
1713
1743
  # load = "<file>": load the report results from <file>
1714
1744
  # plot = 0: no plot, 1 = plot variables separately, 2 = combine variables
1715
1745
  # station = 0: use device_id, 1 = use station_id
@@ -1839,7 +1869,7 @@ def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=Non
1839
1869
  result = json.load(file)
1840
1870
  file.close()
1841
1871
  elif save is not None:
1842
- file_name = save + "_rep_" + report_type + "_" + d.replace('-','') + ".txt"
1872
+ file_name = save + "_report_" + report_type + "_" + d.replace('-','') + ".txt"
1843
1873
  file = open(storage + file_name, 'w', encoding='utf-8')
1844
1874
  json.dump(result, file, indent=4, ensure_ascii= False)
1845
1875
  file.close()
@@ -2588,7 +2618,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
2588
2618
  elif type(strategy) is not list:
2589
2619
  strategy = [strategy]
2590
2620
  output(f"\nStrategy")
2591
- use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
2621
+ use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0) #, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
2592
2622
  output_close(plot=tariff_config['show_plot'])
2593
2623
  if update == 1:
2594
2624
  tariff = use
@@ -2665,15 +2695,15 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
2665
2695
  return profile[:run_time]
2666
2696
 
2667
2697
  # build the timed work mode profile from the tariff strategy:
2668
- def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, current_mode=None):
2698
+ def strategy_timed(timed_mode, time_line, run_time, min_soc=10, max_soc=100, current_mode=None):
2669
2699
  global tariff, steps_per_hour
2670
2700
  work_mode_timed = []
2671
2701
  min_soc_now = min_soc
2672
2702
  max_soc_now = max_soc
2673
2703
  current_mode = 'SelfUse' if current_mode is None else current_mode
2674
2704
  strategy = get_strategy(timed_mode=timed_mode)
2675
- h = base_hour
2676
2705
  for i in range(0, run_time):
2706
+ h = time_line[i]
2677
2707
  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,
2678
2708
  'pv': 0.0, 'discharge': 0.0, 'hold': 0, 'kwh': None}
2679
2709
  if strategy is not None:
@@ -2692,8 +2722,8 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
2692
2722
  if d.get('fdpwr') is not None:
2693
2723
  period['fdpwr'] = d['fdpwr']
2694
2724
  period['duration'] = duration_in(h, d, steps_per_hour) * steps_per_hour
2725
+ break
2695
2726
  work_mode_timed.append(period)
2696
- h = round_time(h + 1 / steps_per_hour)
2697
2727
  return work_mode_timed
2698
2728
 
2699
2729
  # build the timed battery residual from the charge / discharge, work mode and min_soc
@@ -2706,7 +2736,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2706
2736
  discharge_loss = charge_config['discharge_loss']
2707
2737
  charge_limit = charge_config['charge_limit']
2708
2738
  float_charge = charge_config['float_charge']
2709
- for i in range(0, len(work_mode_timed)):
2739
+ run_time = len(work_mode_timed)
2740
+ for i in range(0, run_time):
2710
2741
  w = work_mode_timed[i]
2711
2742
  w['kwh'] = kwh_current
2712
2743
  max_now = w['max_soc'] * capacity / 100
@@ -2717,6 +2748,7 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2717
2748
  if kwh_current > capacity:
2718
2749
  # battery is full
2719
2750
  kwh_current = capacity
2751
+ w = work_mode_timed[i+1] if (i + 1) < run_time else w
2720
2752
  min_soc_now = w['fdsoc'] if w['mode'] =='ForceDischarge' else w['min_soc']
2721
2753
  reserve_now = capacity * min_soc_now / 100
2722
2754
  if kwh_current < reserve_now and (i < time_to_next or kwh_min is None):
@@ -2746,7 +2778,7 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2746
2778
  period = times[0] if len(times) > 0 else work_mode_timed[0]
2747
2779
  next_period = work_mode_timed[t]
2748
2780
  h = base_hour + t / steps_per_hour
2749
- if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
2781
+ if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold'] or period['min_soc'] != next_period['min_soc']:
2750
2782
  s = {'start': start % 24, 'end': h % 24, 'mode': period['mode'], 'min_soc': period['min_soc']}
2751
2783
  if period['mode'] == 'ForceDischarge':
2752
2784
  s['fdsoc'] = period.get('fdsoc')
@@ -2838,7 +2870,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
2838
2870
  def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
2839
2871
  forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
2840
2872
  global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
2841
- global timed_strategy, steps_per_hour, base_time, storage, battery, charge_rates
2873
+ global timed_strategy, steps_per_hour, base_time, storage, battery, battery_params
2842
2874
  print(f"\n---------------- charge_needed ----------------")
2843
2875
  # validate parameters
2844
2876
  args = locals()
@@ -2945,8 +2977,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2945
2977
  bat_power = 0.0
2946
2978
  temperature = 30
2947
2979
  bms_charge_current = 15
2948
- charge_loss = charge_rates[2]['charge_loss']
2949
- discharge_loss = charge_rates[2]['discharge_loss']
2980
+ charge_loss = battery_params[2]['charge_loss']
2981
+ discharge_loss = battery_params[2]['discharge_loss']
2950
2982
  bat_current = 0.0
2951
2983
  device_power = 6.0
2952
2984
  device_current = 35
@@ -2968,8 +3000,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2968
3000
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
2969
3001
  return None
2970
3002
  bms_charge_current = battery.get('charge_rate')
2971
- charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 1.0
2972
- discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 1.0
3003
+ charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
3004
+ discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
2973
3005
  device_power = device.get('power')
2974
3006
  device_current = device.get('max_charge_current')
2975
3007
  model = device.get('deviceType')
@@ -3141,7 +3173,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3141
3173
  # produce time lines for charge, discharge and work mode
3142
3174
  charge_timed = [min([charge_limit, c_float(x) * pv_loss]) for x in generation_timed]
3143
3175
  discharge_timed = [min([discharge_limit, c_float(x) / dc_ac_loss]) + bms_loss for x in consumption_timed]
3144
- work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
3176
+ work_mode_timed = strategy_timed(timed_mode, time_line, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
3145
3177
  for i in range(0, len(work_mode_timed)):
3146
3178
  # get work mode
3147
3179
  work_mode = work_mode_timed[i]['mode']
@@ -3269,14 +3301,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3269
3301
  set_charge(ch1=False, st1=start1, en1=end1, ch2=True, st2=start2, en2=end2, force=1, enable=update_settings)
3270
3302
  if update_settings == 0:
3271
3303
  output(f"\nNo changes made to charge settings")
3304
+ start_t = 0 # int(hour_now % 1 + 0.5) * steps_per_hour
3272
3305
  if show_data > 0:
3273
3306
  data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
3274
3307
  s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3275
3308
  h = base_hour
3276
- t = 0
3309
+ t = start_t
3277
3310
  while t < len(time_line) and bat_timed[t] is not None:
3278
3311
  col = h % data_wrap
3279
- s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
3312
+ s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
3280
3313
  s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
3281
3314
  h += 1
3282
3315
  t += steps_per_hour
@@ -3284,8 +3317,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3284
3317
  if show_plot > 0:
3285
3318
  print()
3286
3319
  plt.figure(figsize=(figure_width, figure_width/2))
3287
- x_timed = [i for i in range(0, run_time)]
3288
- x_ticks = [i for i in range(0, run_time, steps_per_hour)]
3320
+ x_timed = [i for i in range(start_t, run_time)]
3321
+ x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
3289
3322
  plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
3290
3323
  if show_plot == 1:
3291
3324
  title = f"Predicted Battery SoC % at {base_time}({charge_message})"
@@ -3395,14 +3428,15 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
3395
3428
  for v in plots.keys():
3396
3429
  for i in range(0, run_time):
3397
3430
  plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
3431
+ start_t = 0 #int(hour_now % 1 + 0.5) * steps_per_hour
3398
3432
  if show_data > 0 and plots.get('SoC') is not None:
3399
3433
  data_wrap = 1 #charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 1
3400
3434
  s = f"\nBattery Energy kWh (predicted / actual):" if show_data == 2 else f"\nBattery SoC (predicted / actual):"
3401
3435
  h = base_hour
3402
- t = 0
3436
+ t = start_t
3403
3437
  while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
3404
3438
  col = h % data_wrap
3405
- s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
3439
+ s += f"\n {hours_time(time_line[t])}" if t == start_t or col == 0 else ""
3406
3440
  s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%"
3407
3441
  s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
3408
3442
  h += 1
@@ -3411,8 +3445,8 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3, d=None):
3411
3445
  if show_plot > 0:
3412
3446
  print()
3413
3447
  plt.figure(figsize=(figure_width, figure_width/2))
3414
- x_timed = [i for i in range(0, run_time)]
3415
- x_ticks = [i for i in range(0, run_time, steps_per_hour)]
3448
+ x_timed = [i for i in range(start_t, run_time)]
3449
+ x_ticks = [i for i in range(start_t, run_time, steps_per_hour)]
3416
3450
  plt.xticks(ticks=x_ticks, labels=[hours_time(time_line[x]) for x in x_ticks], rotation=90, fontsize=8, ha='center')
3417
3451
  if show_plot == 1:
3418
3452
  title = f"Predicted Battery SoC % at {base_time}({charge_message})"
@@ -3555,11 +3589,11 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3555
3589
  s +=f",{v:.0f}"
3556
3590
  return s
3557
3591
  output(f"Current SoC: {current_soc}%")
3558
- output(f"Capacity: {capacity:.2f}kWh" + (" (Residual / SoC x 100)" if bat['residual_handling'] == 1 else ""))
3559
- output(f"Residual: {residual:.2f}kWh" + (" (SoC x Capacity / 100)" if bat['residual_handling'] == 2 else ""))
3592
+ output(f"Capacity: {capacity:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [1,3] else ""))
3593
+ output(f"Residual: {residual:.2f}kWh" + (" (calculated)" if bat['residual_handling'] in [2,3] else ""))
3560
3594
  if rated_capacity is not None and bat_soh is not None:
3561
3595
  output(f"Rated Capacity: {rated_capacity / 1000:.2f}kWh")
3562
- output(f"SoH: {bat_soh:.1f}%" + (" (Capacity / Rated Capacity x 100)" if not bat['soh_supported'] else ""))
3596
+ output(f"SoH: {bat_soh:.1f}%" + (" (calculated)" if not bat['soh_supported'] else ""))
3563
3597
  output(f"InvBatVolt: {bat_volt:.1f}V")
3564
3598
  output(f"InvBatCurrent: {bat_current:.1f}A")
3565
3599
  output(f"State: {'Charging' if bat_power < 0 else 'Discharging'} ({abs(bat_power):.3f}kW)")
@@ -3968,7 +4002,6 @@ class Solcast :
3968
4002
  # The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
3969
4003
  # The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
3970
4004
  global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
3971
- self.data = {}
3972
4005
  now = convert_date(d)
3973
4006
  self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
3974
4007
  self.today = datetime.strftime(datetime.date(now), '%Y-%m-%d')
@@ -3976,6 +4009,8 @@ class Solcast :
3976
4009
  self.tomorrow = datetime.strftime(datetime.date(now + timedelta(days=1)), '%Y-%m-%d')
3977
4010
  self.yesterday = datetime.strftime(datetime.date(now - timedelta(days=1)), '%Y-%m-%d')
3978
4011
  self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
4012
+ self.data = {}
4013
+ self.rids = []
3979
4014
  if reload == 1 and os.path.exists(storage + self.save):
3980
4015
  os.remove(storage + self.save)
3981
4016
  if self.save is not None and os.path.exists(storage + self.save):
@@ -3984,33 +4019,37 @@ class Solcast :
3984
4019
  file.close()
3985
4020
  if len(self.data) == 0:
3986
4021
  print(f"No data in {self.save}")
3987
- elif reload == 2 and 'date' in self.data and self.data['date'] != self.today:
3988
- self.data = {}
3989
- elif debug_setting > 0 and not quiet:
3990
- print(f"Using data for {self.data['date']} from {self.save}")
3991
- if len(self.data) == 0 :
4022
+ else:
4023
+ self.rids = self.data['forecasts'].keys() if self.data.get('forecasts') is not None else []
4024
+ if reload == 2 and self.data.get('date') is not None and self.data['date'] != self.today:
4025
+ self.data = {}
4026
+ elif debug_setting > 0 and not quiet:
4027
+ print(f"Using data for {self.data['date']} from {self.save}")
4028
+ if len(self.data) == 0:
3992
4029
  if solcast_api_key is None or solcast_api_key == 'my.solcast_api_key>':
3993
4030
  print(f"\nSolcast: solcast_api_key not set, exiting")
3994
4031
  return
3995
4032
  self.credentials = HTTPBasicAuth(solcast_api_key, '')
3996
- if debug_setting > 1 and not quiet:
3997
- print(f"Getting rids from solcast.com")
3998
- params = {'format' : 'json'}
3999
- response = requests.get(solcast_url + 'rooftop_sites', auth = self.credentials, params = params)
4000
- if response.status_code != 200:
4001
- if response.status_code == 429:
4002
- print(f"\nSolcast API call limit reached for today")
4003
- else:
4004
- print(f"Solcast: response code getting rooftop_sites was {response.status_code}: {response.reason}")
4005
- return
4006
- sites = response.json().get('sites')
4033
+ if len(self.rids) == 0:
4034
+ if debug_setting > 1 and not quiet:
4035
+ print(f"Getting rids from solcast.com")
4036
+ params = {'format' : 'json'}
4037
+ response = requests.get(solcast_url + 'rooftop_sites', auth = self.credentials, params = params)
4038
+ if response.status_code != 200:
4039
+ if response.status_code == 429:
4040
+ print(f"\nSolcast API call limit reached for today")
4041
+ else:
4042
+ print(f"Solcast: response code getting rooftop_sites was {response.status_code}: {response.reason}")
4043
+ return
4044
+ sites = response.json().get('sites')
4045
+ self.rids = [s['resource_id'] for s in sites]
4007
4046
  if debug_setting > 0 and not quiet:
4008
4047
  print(f"Getting forecast for {self.today} from solcast.com")
4009
4048
  self.data['date'] = self.today
4010
4049
  params = {'format' : 'json', 'hours' : 168, 'period' : 'PT30M'} # always get 168 x 30 min values
4011
4050
  for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
4012
4051
  self.data[t] = {}
4013
- for rid in [s['resource_id'] for s in sites] :
4052
+ for rid in self.rids:
4014
4053
  response = requests.get(solcast_url + 'rooftop_sites/' + rid + '/' + t, auth = self.credentials, params = params)
4015
4054
  if response.status_code != 200 :
4016
4055
  if response.status_code == 429: