foxesscloud 2.6.8__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 03 November 2024
4
+ Updated: 06 November 2024
5
5
  By: Tony Matthews
6
6
  """
7
7
  ##################################################################################################
@@ -10,7 +10,7 @@ By: Tony Matthews
10
10
  # ALL RIGHTS ARE RESERVED © Tony Matthews 2023
11
11
  ##################################################################################################
12
12
 
13
- version = "1.7.9"
13
+ version = "1.8.1"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -275,7 +275,7 @@ def get_token():
275
275
  info = None
276
276
 
277
277
  def get_info():
278
- global token, debug_setting, info, messages
278
+ global debug_setting, info, messages
279
279
  if get_token() is None:
280
280
  return None
281
281
  output(f"getting access", 2)
@@ -307,7 +307,7 @@ def get_info():
307
307
  status = None
308
308
 
309
309
  def get_status(station=0):
310
- global token, debug_setting, info, messages, status
310
+ global debug_setting, info, messages, status
311
311
  if get_token() is None:
312
312
  return None
313
313
  output(f"getting status", 2)
@@ -335,7 +335,7 @@ site = None
335
335
  station_id = None
336
336
 
337
337
  def get_site(name=None):
338
- global token, site_list, site, debug_setting, messages, station_id
338
+ global site_list, site, debug_setting, messages, station_id
339
339
  if get_token() is None:
340
340
  return None
341
341
  if site is not None and name is None:
@@ -376,6 +376,7 @@ def get_site(name=None):
376
376
  station_id = site['stationID']
377
377
  return site
378
378
 
379
+
379
380
  ##################################################################################################
380
381
  # get list of data loggers
381
382
  ##################################################################################################
@@ -384,7 +385,7 @@ logger_list = None
384
385
  logger = None
385
386
 
386
387
  def get_logger(sn=None):
387
- global token, logger_list, logger, debug_setting, messages
388
+ global logger_list, logger, debug_setting, messages
388
389
  if get_token() is None:
389
390
  return None
390
391
  if logger is not None and sn is None:
@@ -435,7 +436,7 @@ var_list = None
435
436
  raw_vars = var_list
436
437
 
437
438
  def get_device(sn=None):
438
- global token, device_list, device, device_id, device_sn, firmware, battery, var_list, debug_setting, messages, flag, schedule, templates, remote_settings
439
+ global device_list, device, device_id, device_sn, firmware, battery, var_list, debug_setting, messages, flag, schedule, templates, remote_settings
439
440
  if get_token() is None:
440
441
  return None
441
442
  if device is not None:
@@ -527,7 +528,7 @@ def get_device(sn=None):
527
528
  ##################################################################################################
528
529
 
529
530
  def get_vars():
530
- global token, device_id, debug_setting, messages
531
+ global device_id, debug_setting, messages
531
532
  if get_device() is None:
532
533
  return None
533
534
  output(f"getting variables", 2)
@@ -555,7 +556,7 @@ def get_vars():
555
556
  firmware = None
556
557
 
557
558
  def get_firmware():
558
- global token, device_id, firmware, debug_setting, messages
559
+ global device_id, firmware, debug_setting, messages
559
560
  if get_device() is None:
560
561
  return None
561
562
  output(f"getting firmware", 2)
@@ -599,7 +600,7 @@ battery_params = {
599
600
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
600
601
  'step': 5,
601
602
  'offset': 5,
602
- 'charge_loss': 1.08,
603
+ 'charge_loss': 1.07,
603
604
  'discharge_loss': 0.95},
604
605
  # Mira BMS with firmware 1.014 or later
605
606
  3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
@@ -609,8 +610,8 @@ battery_params = {
609
610
  'discharge_loss': 0.974},
610
611
  }
611
612
 
612
- def get_battery(info=1):
613
- global token, device_id, battery, debug_setting, messages, residual_handling, battery_params
613
+ def get_battery(info=1, rated=None, count=None):
614
+ global device_id, battery, debug_setting, messages, residual_handling, battery_params
614
615
  if get_device() is None:
615
616
  return None
616
617
  output(f"getting battery", 2)
@@ -655,24 +656,26 @@ def get_battery(info=1):
655
656
  soc = battery.get('soc')
656
657
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
657
658
  if battery.get('count') is None:
658
- battery['count'] = int(battery['volt'] / 49)
659
+ battery['count'] = int(battery['volt'] / 49) if count is None else count
659
660
  if battery.get('ratedCapacity') is None:
660
- battery['ratedCapacity'] = 2560 * battery['count']
661
+ battery['ratedCapacity'] = 2560 * battery['count'] if rated is None else rated
661
662
  elif battery['residual_handling'] == 3:
662
663
  if battery.get('count') is None:
663
- battery['count'] = int(battery['volt'] / 49)
664
+ battery['count'] = int(battery['volt'] / 49) if count is None else count
664
665
  capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
665
666
  soc = battery.get('soc')
666
667
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
667
668
  if battery.get('ratedCapacity') is None:
668
- battery['ratedCapacity'] = 2450 * battery['count']
669
+ battery['ratedCapacity'] = 2450 * battery['count'] if rated is None else rated
669
670
  else:
670
671
  residual = battery.get('residual')
671
672
  soc = battery.get('soc')
672
- capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
673
+ capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
674
+ if battery.get('ratedCapacity') is None and rated is not None:
675
+ battery['ratedCapacity'] = rated
673
676
  battery['capacity'] = round(capacity, 3)
674
677
  battery['residual'] = round(residual, 3)
675
- battery['charge_rate'] = 50
678
+ battery['charge_rate'] = None
676
679
  params = battery_params[battery['residual_handling']]
677
680
  battery['charge_loss'] = params['charge_loss']
678
681
  battery['discharge_loss'] = params['discharge_loss']
@@ -682,8 +685,8 @@ def get_battery(info=1):
682
685
  battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
683
686
  return battery
684
687
 
685
- def get_batteries(info=1):
686
- global token, device_id, battery, debug_setting, messages, batteries, battery_params, residual_handling
688
+ def get_batteries(info=1, rated=None, count=None):
689
+ global device_id, battery, debug_setting, messages, batteries, battery_params, residual_handling
687
690
  if get_device() is None:
688
691
  return None
689
692
  output(f"getting batteries", 2)
@@ -710,7 +713,15 @@ def get_batteries(info=1):
710
713
  else:
711
714
  for i in range(0, len(batteries)):
712
715
  batteries[i]['info'] = result['batteries'][i]
713
- for b in batteries:
716
+ if type(rated) is not list:
717
+ rated = [rated]
718
+ while len(rated) < len(batteries):
719
+ rated.append(None)
720
+ if type(count) is not list:
721
+ count = [count]
722
+ while len(count) < len(batteries):
723
+ count.append(None)
724
+ for i,b in enumerate(batteries):
714
725
  b['residual_handling'] = residual_handling
715
726
  if b.get('info') is not None:
716
727
  if b['info'].get('slaveBatteries') is not None:
@@ -718,9 +729,11 @@ def get_batteries(info=1):
718
729
  if b['info']['masterSN'][:7] == '60BBHV2' and b['info']['masterVersion'] >= '1.014':
719
730
  b['residual_handling'] = 2
720
731
  elif b['info']['masterSN'][:7] == '60MBB01' and b['info']['masterVersion'] >= '1.014':
721
- residual_handling = 3
732
+ b['residual_handling'] = 3
733
+ if b.get('count') is None:
734
+ b['count'] = count[i]
722
735
  rated_capacity = b.get('ratedCapacity')
723
- b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else None
736
+ b['ratedCapacity'] = rated_capacity if rated_capacity is not None and rated_capacity > 100 else rated[i]
724
737
  soh = b.get('soh')
725
738
  b['soh'] = int(soh) if soh.isnumeric() and int(soh) > 10 else None
726
739
  b['soh_supported'] = b['soh'] is not None
@@ -739,7 +752,7 @@ def get_batteries(info=1):
739
752
  b['residual'] = round(residual, 3)
740
753
  if b.get('ratedCapacity') is not None and b.get('capacity') is not None:
741
754
  b['soh'] = round(b['capacity'] * 1000 / b['ratedCapacity'] * 100, 1)
742
- b['charge_rate'] = 50
755
+ b['charge_rate'] = None
743
756
  params = battery_params[b['residual_handling']]
744
757
  b['charge_loss'] = params['charge_loss']
745
758
  b['discharge_loss'] = params['discharge_loss']
@@ -752,7 +765,7 @@ def get_batteries(info=1):
752
765
  ##################################################################################################
753
766
 
754
767
  def get_charge():
755
- global token, device_sn, battery_settings, debug_setting, messages
768
+ global device_sn, battery_settings, debug_setting, messages
756
769
  if get_device() is None:
757
770
  return None
758
771
  if battery_settings is None:
@@ -785,11 +798,11 @@ def get_charge():
785
798
  def time_period(t):
786
799
  result = f"{t['startTime']['hour']:02d}:{t['startTime']['minute']:02d}-{t['endTime']['hour']:02d}:{t['endTime']['minute']:02d}"
787
800
  if t['startTime']['hour'] != t['endTime']['hour'] or t['startTime']['minute'] != t['endTime']['minute']:
788
- 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"
789
802
  return result
790
803
 
791
804
  def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force=0, enable=1):
792
- global token, device_sn, battery_settings, debug_setting, messages, schedule
805
+ global device_sn, battery_settings, debug_setting, messages, schedule
793
806
  if get_device() is None:
794
807
  return None
795
808
  if battery_settings is None:
@@ -863,7 +876,7 @@ def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force
863
876
  ##################################################################################################
864
877
 
865
878
  def get_min():
866
- global token, device_sn, battery_settings, debug_setting, messages
879
+ global device_sn, battery_settings, debug_setting, messages
867
880
  if get_device() is None:
868
881
  return None
869
882
  if battery_settings is None:
@@ -888,7 +901,7 @@ def get_min():
888
901
  ##################################################################################################
889
902
 
890
903
  def set_min(minGridSoc = None, minSoc = None, force = 0):
891
- global token, device_sn, battery_settings, debug_setting, messages
904
+ global device_sn, battery_settings, debug_setting, messages
892
905
  if get_device() is None:
893
906
  return None
894
907
  if get_schedule().get('enable'):
@@ -944,6 +957,13 @@ merge_settings = { # keys to add
944
957
  # 'k106__': 'operation_mode__work_mode',
945
958
  },
946
959
  'values': ['SelfUse', 'Feedin', 'Backup']},
960
+ 'ExportLimit': {'keys': {
961
+ 'h115__': 'basic2__05',
962
+ 'h116__': 'basic2__05',
963
+ 'h117__': 'basic2__05',
964
+ # 'k106__': 'basic2__05',
965
+ },
966
+ 'valueType': 'int'},
947
967
  'BatteryVolt': {'keys': {
948
968
  'h115__': ['h115__14', 'h115__15', 'h115__16'],
949
969
  'h116__': ['h116__15', 'h116__16', 'h116__17'],
@@ -1003,7 +1023,7 @@ def get_ui():
1003
1023
  block = p['block'] and len(p['properties']) > 1
1004
1024
  for e in p['properties']:
1005
1025
  valueType = e['elemType']['valueType']
1006
- item = {'name': e['key'].replace(protocol,'')} if block else {'key': e['key']} #, 'group': p['name']}
1026
+ item = {'name': e['key'].replace(protocol,'')} if block else {'keys': e['key']} #, 'group': p['name']}
1007
1027
  if e['elemType'].get('uiItems') is not None:
1008
1028
  item['values'] = e['elemType']['uiItems']
1009
1029
  elif e.get('range') is not None:
@@ -1018,7 +1038,7 @@ def get_ui():
1018
1038
  else:
1019
1039
  named_settings[e['name']] = item
1020
1040
  if block:
1021
- named_settings[p['name']] = {'key': p['key'], 'type': 'block', 'items': items}
1041
+ named_settings[p['name']] = {'keys': p['key'], 'type': 'block', 'items': items}
1022
1042
  for name in merge_settings.keys():
1023
1043
  if named_settings.get(name) is None and merge_settings[name]['keys'].get(protocol) is not None:
1024
1044
  named_settings[name] = {'keys': merge_settings[name]['keys'][protocol]}
@@ -1028,7 +1048,7 @@ def get_ui():
1028
1048
  return remote_settings
1029
1049
 
1030
1050
  def get_remote_settings(key):
1031
- global token, device_id, debug_setting, messages
1051
+ global device_id, debug_setting, messages
1032
1052
  if get_device() is None:
1033
1053
  return None
1034
1054
  output(f"getting remote settings", 2)
@@ -1090,10 +1110,66 @@ def get_named_settings(name):
1090
1110
  return values
1091
1111
  return result
1092
1112
 
1113
+ def set_named_settings(name, value, force=0):
1114
+ global named_settings
1115
+ if get_device() is None:
1116
+ return None
1117
+ if force == 1 and get_schedule().get('enable'):
1118
+ set_schedule(enable=0)
1119
+ if type(name) is list:
1120
+ result = []
1121
+ for (n, v) in name:
1122
+ result.append(set_named_settings(name=n, value=v))
1123
+ return result
1124
+ if named_settings is None or named_settings.get(name) is None:
1125
+ output(f"** set_named_settings(): {name} was not recognised")
1126
+ return None
1127
+ keys = named_settings[name].get('keys')
1128
+ if keys is None:
1129
+ output(f"** set_named_settings(): no keys for name: {name}")
1130
+ return None
1131
+ item_type = named_settings[name].get('type')
1132
+ if item_type is None:
1133
+ values = {keys: str(value)}
1134
+ elif item_type == 'block':
1135
+ items = named_setting[name]['items']
1136
+ n = len(items)
1137
+ if type(value) is not list or n != len(value):
1138
+ output(f"** set_named_settings(): {name} requires list of {n} values")
1139
+ return None
1140
+ values = {}
1141
+ for i in range(0, n):
1142
+ values[items[i]['name']] = str(value[i])
1143
+ elif item_type == 'list':
1144
+ if type(value) is not list:
1145
+ output(f"** set_named_settings(): {name} requires a list of values")
1146
+ return None
1147
+ values = {keys: value}
1148
+ else:
1149
+ values = {keys: str(value)}
1150
+ output(f"\nSetting {name} to {value}", 1)
1151
+ values['raw'] = ''
1152
+ data = {'id': device_id, 'key': keys, 'values': values}
1153
+ setting_delay()
1154
+ response = signed_post(path="/c/v0/device/setting/set", data=data)
1155
+ if response.status_code != 200:
1156
+ output(f"** set_named_settings() got response code {response.status_code}: {response.reason}")
1157
+ return None
1158
+ errno = response.json().get('errno')
1159
+ if errno != 0:
1160
+ if errno == 44096:
1161
+ output(f"** cannot update {name} when schedule is active")
1162
+ else:
1163
+ output(f"** set_named_settings(): ({name}, {value}) {errno_message(errno)}")
1164
+ return 0
1165
+ return 1
1166
+
1093
1167
  ##################################################################################################
1094
1168
  # wrappers for named settings
1095
1169
  ##################################################################################################
1096
1170
 
1171
+ work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
1172
+ settable_modes = work_modes[:3]
1097
1173
  work_mode = None
1098
1174
 
1099
1175
  def get_work_mode():
@@ -1103,6 +1179,18 @@ def get_work_mode():
1103
1179
  work_mode = get_named_settings('WorkMode')
1104
1180
  return work_mode
1105
1181
 
1182
+ def set_work_mode(mode, force=0):
1183
+ global settable_modes, work_mode, debug_setting
1184
+ if get_device() is None:
1185
+ return None
1186
+ if mode not in settable_modes:
1187
+ output(f"** work mode: must be one of {settable_modes}")
1188
+ return None
1189
+ result = set_named_settings(name='WorkMode', value=mode, force=force)
1190
+ if result is not None and result == 1:
1191
+ work_mode = mode
1192
+ return result
1193
+
1106
1194
  def get_cell_volts():
1107
1195
  values = get_named_settings('BatteryVolt')
1108
1196
  if values is None:
@@ -1130,46 +1218,6 @@ def get_cell_temps(nbat=8):
1130
1218
  break
1131
1219
  return bat_temps
1132
1220
 
1133
-
1134
- ##################################################################################################
1135
- # set work mode
1136
- ##################################################################################################
1137
-
1138
- work_modes = ['SelfUse', 'Feedin', 'Backup', 'ForceCharge', 'ForceDischarge']
1139
- settable_modes = work_modes[:3]
1140
-
1141
- def set_work_mode(mode, force = 0):
1142
- global token, device_id, work_modes, work_mode, debug_setting, messages, schedule
1143
- if get_device() is None:
1144
- return None
1145
- if mode not in settable_modes:
1146
- output(f"** work mode: must be one of {settable_modes}")
1147
- return None
1148
- if get_flag() is None:
1149
- return None
1150
- if schedule.get('enable') == True:
1151
- if force == 0:
1152
- output(f"** set_work_mode(): cannot set work mode when a schedule is enabled")
1153
- return None
1154
- set_schedule(enable=0)
1155
- output(f"\nSetting work mode: {mode}", 1)
1156
- data = {'id': device_id, 'key': 'operation_mode__work_mode', 'values': {'operation_mode__work_mode': mode}, 'raw': ''}
1157
- setting_delay()
1158
- response = signed_post(path="/c/v0/device/setting/set", data=data)
1159
- if response.status_code != 200:
1160
- output(f"** set_work_mode() got response code {response.status_code}: {response.reason}")
1161
- return None
1162
- errno = response.json().get('errno')
1163
- if errno != 0:
1164
- if errno == 44096:
1165
- output(f"** cannot update settings when schedule is active")
1166
- else:
1167
- output(f"** set_work_mode(), {errno_message(errno)}")
1168
- return None
1169
- work_mode = mode
1170
- return work_mode
1171
-
1172
-
1173
1221
  ##################################################################################################
1174
1222
  # get schedule
1175
1223
  ##################################################################################################
@@ -1179,7 +1227,7 @@ templates = None
1179
1227
 
1180
1228
  # get the current enable flag
1181
1229
  def get_flag():
1182
- global token, device_id, device_sn, schedule, debug_setting, messages
1230
+ global device_id, device_sn, schedule, debug_setting, messages
1183
1231
  if get_device() is None:
1184
1232
  return None
1185
1233
  output(f"getting flag", 2)
@@ -1216,7 +1264,7 @@ def get_flag():
1216
1264
 
1217
1265
  # get the current schedule
1218
1266
  def get_schedule():
1219
- global token, device_id, schedule, debug_setting, messages
1267
+ global device_id, schedule, debug_setting, messages
1220
1268
  if get_flag() is None:
1221
1269
  return None
1222
1270
  if schedule.get('support') == False:
@@ -1258,7 +1306,7 @@ def build_strategy_from_schedule():
1258
1306
 
1259
1307
  # get the details for a specific template
1260
1308
  def get_template_detail(template):
1261
- global token, device_id, schedule, debug_setting, messages, templates
1309
+ global device_id, schedule, debug_setting, messages, templates
1262
1310
  if get_flag() is None:
1263
1311
  return None
1264
1312
  if schedule.get('support') == False:
@@ -1280,7 +1328,7 @@ def get_template_detail(template):
1280
1328
 
1281
1329
  # get the preset templates that contains periods
1282
1330
  def get_templates(template_type=[1,2]):
1283
- global token, device_id, flag, schedule, debug_setting, messages, templates
1331
+ global device_id, flag, schedule, debug_setting, messages, templates
1284
1332
  if get_flag() is None:
1285
1333
  return None
1286
1334
  if schedule.get('support') == False:
@@ -1393,7 +1441,7 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1393
1441
 
1394
1442
  # set a schedule from a period or list of periods
1395
1443
  def set_schedule(periods=None, template=None, enable=True):
1396
- global token, device_sn, debug_setting, messages, schedule, templates
1444
+ global device_sn, debug_setting, messages, schedule, templates
1397
1445
  if get_flag() is None:
1398
1446
  return None
1399
1447
  if schedule.get('support') == False:
@@ -1482,7 +1530,7 @@ sample_time = 5.0 # 5 minutes default
1482
1530
  sample_rounding = 2 # round to 30 seconds
1483
1531
 
1484
1532
  def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
1485
- global token, device_id, debug_setting, var_list, invert_ct2, tariff, max_power_kw, messages, sample_rounding, sample_time, storage
1533
+ global device_id, debug_setting, var_list, invert_ct2, tariff, max_power_kw, messages, sample_rounding, sample_time, storage
1486
1534
  if station == 0 and get_device() is None:
1487
1535
  return None
1488
1536
  elif station == 1 and get_site() is None:
@@ -1700,9 +1748,7 @@ def plot_raw(result, plot=1, station=0):
1700
1748
  def report_value_profile(result):
1701
1749
  if type(result) is not list or result[0]['type'] != 'day':
1702
1750
  return (None, None)
1703
- data = []
1704
- for h in range(0,24):
1705
- data.append((0.0, 0)) # value sum, count of values
1751
+ data = [(0.0, 0) for h in range(0,24)]
1706
1752
  totals = 0
1707
1753
  n = 0
1708
1754
  for day in result:
@@ -1732,6 +1778,30 @@ def report_value_profile(result):
1732
1778
  # forwards compatibility
1733
1779
  get_history = get_raw
1734
1780
 
1781
+ # rescale history data based on time and steps
1782
+ def rescale_history(data, steps):
1783
+ if data is None or len(data) < 1:
1784
+ return None
1785
+ result = [None for i in range(0, 24 * steps)]
1786
+ bst = 1 if 'BST' in data[0]['time'] else 0
1787
+ average = 0.0
1788
+ n = 0
1789
+ i = 0
1790
+ for d in data:
1791
+ h = round_time(time_hours(d['time'][11:]) + bst)
1792
+ new_i = int(h * steps)
1793
+ if new_i != i and i < len(result):
1794
+ result[i] = average / n if n > 0 else None
1795
+ average = 0.0
1796
+ n = 0
1797
+ i = new_i
1798
+ if d['value'] is not None:
1799
+ average += d['value']
1800
+ n += 1
1801
+ if n > 0 and i < len(result):
1802
+ result[i] = average / n
1803
+ return result
1804
+
1735
1805
  ##################################################################################################
1736
1806
  # get energy report data in kWh
1737
1807
  ##################################################################################################
@@ -1754,7 +1824,7 @@ fix_value_threshold = 200000000.0
1754
1824
  fix_value_mask = 0x0000FFFF
1755
1825
 
1756
1826
  def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
1757
- global token, device_id, station_id, var_list, debug_setting, report_vars, messages, station_id
1827
+ global device_id, station_id, var_list, debug_setting, report_vars, messages, station_id
1758
1828
  if station == 0 and get_device() is None:
1759
1829
  return None
1760
1830
  elif station == 1 and get_site() is None:
@@ -1990,7 +2060,7 @@ def plot_report(result, plot=1, station=0):
1990
2060
  ##################################################################################################
1991
2061
 
1992
2062
  def get_earnings():
1993
- global token, device_id, station_id, var_list, debug_setting, messages
2063
+ global device_id, station_id, var_list, debug_setting, messages
1994
2064
  if get_device() is None:
1995
2065
  return None
1996
2066
  id_name = 'deviceID'
@@ -2259,7 +2329,7 @@ octopus_cosy = {
2259
2329
  # time periods for Octopus Go
2260
2330
  octopus_go = {
2261
2331
  'name': 'Octopus Go',
2262
- 'off_peak1': {'start': 0.5, 'end': 4.5, 'hold': 1},
2332
+ 'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
2263
2333
  'forecast_times': [21, 22]
2264
2334
  }
2265
2335
 
@@ -2732,8 +2802,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2732
2802
  global charge_config, steps_per_hour
2733
2803
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2734
2804
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2735
- charge_loss = charge_config['charge_loss']
2736
- discharge_loss = charge_config['discharge_loss']
2805
+ charge_loss = charge_config['_charge_loss']
2806
+ discharge_loss = charge_config['_discharge_loss']
2737
2807
  charge_limit = charge_config['charge_limit']
2738
2808
  float_charge = charge_config['float_charge']
2739
2809
  run_time = len(work_mode_timed)
@@ -2826,6 +2896,8 @@ charge_config = {
2826
2896
  'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
2827
2897
  'pv_loss': 0.950, # loss converting PV power to DC battery charge power
2828
2898
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2899
+ 'charge_loss': None, # loss converting charge energy to stored energy
2900
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2829
2901
  'inverter_power': 101, # Inverter power consumption in W
2830
2902
  'bms_power': 50, # BMS power consumption in W
2831
2903
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2928,7 +3000,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2928
3000
  if len(times) == 0:
2929
3001
  times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
2930
3002
  output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
2931
- time_to_end1 = None
3003
+ time_to_run = None
2932
3004
  for t in times:
2933
3005
  if hour_in(hour_now, t) and update_settings > 0:
2934
3006
  update_settings = 0
@@ -2940,7 +3012,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2940
3012
  t['time_to_start'] = time_to_start
2941
3013
  t['time_to_end'] = time_to_end
2942
3014
  t['charge_time'] = charge_time
2943
- time_to_end1 = time_to_end if time_to_end1 is None else time_to_end1
3015
+ if time_to_run is None:
3016
+ time_to_run = time_to_start
2944
3017
  # get next charge slot
2945
3018
  times = sorted(times, key=lambda t: t['time_to_start'])
2946
3019
  charge_key = times[0]['key']
@@ -2952,7 +3025,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2952
3025
  # work out time window and times with clock changes
2953
3026
  charge_today = (base_hour + time_to_start / steps_per_hour) < 24
2954
3027
  forecast_day = today if charge_today else tomorrow
2955
- run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
3028
+ run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
2956
3029
  run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
2957
3030
  time_line = [round_time(base_hour + x / steps_per_hour - (hour_adjustment if x >= time_change else 0)) for x in range(0, run_time)]
2958
3031
  bat_hold = times[0]['hold']
@@ -2977,8 +3050,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2977
3050
  bat_power = 0.0
2978
3051
  temperature = 30
2979
3052
  bms_charge_current = 15
2980
- charge_loss = battery_params[2]['charge_loss']
2981
- discharge_loss = battery_params[2]['discharge_loss']
3053
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
3054
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
2982
3055
  bat_current = 0.0
2983
3056
  device_power = 6.0
2984
3057
  device_current = 35
@@ -3000,19 +3073,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3000
3073
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
3001
3074
  return None
3002
3075
  bms_charge_current = battery.get('charge_rate')
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
3076
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
3077
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
3005
3078
  device_power = device.get('power')
3006
3079
  device_current = device.get('max_charge_current')
3007
3080
  model = device.get('deviceType')
3008
3081
  min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
3009
3082
  max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
3083
+ reserve = capacity * min_soc / 100
3084
+ # charge current may be derated based on temperature
3085
+ charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
3086
+ if bms_charge_current is not None and charge_current > bms_charge_current:
3087
+ charge_current = bms_charge_current
3010
3088
  volt_curve = charge_config['volt_curve']
3011
3089
  nominal_soc = charge_config['nominal_soc']
3012
3090
  volt_nominal = interpolate(nominal_soc / 10, volt_curve)
3013
3091
  bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
3014
3092
  bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
3015
- reserve = capacity * min_soc / 100
3016
3093
  output(f"\nBattery Info:")
3017
3094
  output(f" Capacity: {capacity:.2f}kWh")
3018
3095
  output(f" Residual: {residual:.2f}kWh")
@@ -3022,15 +3099,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3022
3099
  output(f" Min SoC: {min_soc}% ({reserve:.2f}kWh)")
3023
3100
  output(f" Current SoC: {current_soc}%")
3024
3101
  output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
3025
- output(f" Max Charge: {bms_charge_current:.1f}A")
3102
+ output(f" Max Charge: {charge_current:.1f}A")
3026
3103
  output(f" Temperature: {temperature:.1f}°C")
3027
3104
  output(f" Resistance: {bat_resistance:.2f} ohms")
3028
3105
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
3029
- output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge")
3030
- # charge current may be derated based on temperature
3031
- charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
3032
- if charge_current > bms_charge_current:
3033
- charge_current = bms_charge_current
3106
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
3034
3107
  # inverter losses
3035
3108
  inverter_power = charge_config['inverter_power'] if charge_config['inverter_power'] is not None else round(device_power, 0) * 25
3036
3109
  operating_loss = inverter_power / 1000
@@ -3059,8 +3132,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3059
3132
  charge_config['charge_limit'] = charge_limit
3060
3133
  charge_config['charge_power'] = charge_power
3061
3134
  charge_config['float_charge'] = float_charge
3062
- charge_config['charge_loss'] = charge_loss
3063
- charge_config['discharge_loss'] = discharge_loss
3135
+ charge_config['_charge_loss'] = charge_loss
3136
+ charge_config['_discharge_loss'] = discharge_loss
3064
3137
  # display what we have
3065
3138
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
3066
3139
  output(f"\nDevice Info:")
@@ -3225,6 +3298,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3225
3298
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3226
3299
  charge_message = "no charge needed"
3227
3300
  kwh_needed = 0.0
3301
+ kwh_spare = kwh_min - reserve
3228
3302
  hours = 0.0
3229
3303
  start_timed = time_to_end
3230
3304
  end_timed = time_to_end
@@ -3232,6 +3306,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3232
3306
  else:
3233
3307
  # work out time to add kwh_needed to battery
3234
3308
  charge_rate = charge_power * charge_loss
3309
+ discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
3235
3310
  hours = kwh_needed / charge_rate
3236
3311
  if test_charge is None:
3237
3312
  output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
@@ -3242,13 +3317,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3242
3317
  if hours > charge_time:
3243
3318
  hours = charge_time
3244
3319
  elif hours > hours_to_full:
3245
- kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
3246
- required = hours_to_full + charge_time * kwh_shortfall / abs(start_residual - end_residual) # time to recover energy not added
3320
+ kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
3321
+ required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
3247
3322
  hours = required if required > hours and required < charge_time else charge_time
3248
3323
  # round charge time and work out what will actually be added
3249
3324
  min_hours = charge_config['min_hours']
3250
3325
  hours = int(hours / min_hours + 0.99) * min_hours
3251
3326
  kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
3327
+ kwh_added += discharge_rate * hours # discharge saved by charging
3328
+ kwh_spare = kwh_min - reserve + kwh_added
3252
3329
  # rework charge and discharge
3253
3330
  charge_period = get_best_charge_period(start_at, hours)
3254
3331
  charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
@@ -3284,7 +3361,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3284
3361
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3285
3362
  # show the results
3286
3363
  output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3287
- output(f" Contingency: {kwh_contingency / capacity * 100:.0f}% SoC ({kwh_contingency:.2f}kWh)")
3364
+ output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
3288
3365
  if not charge_today:
3289
3366
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
3290
3367
  # setup charging
@@ -3516,19 +3593,19 @@ def bat_count(cell_count):
3516
3593
  battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
3517
3594
 
3518
3595
  # show information about the current state of the batteries
3519
- def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3596
+ def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
3520
3597
  global debug_setting, battery_info_app_key
3521
3598
  if bat is None:
3522
- bats = get_batteries(info=info)
3599
+ bats = get_batteries(info=info, rated=rated, count=count)
3523
3600
  if bats is None:
3524
3601
  return None
3525
3602
  for i in range(0, len(bats)):
3526
3603
  output(f"\n----------------------- BMS {i+1} -----------------------")
3527
- battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
3604
+ battery_info(log=log, plot=plot, info=info, bat=bats[i])
3528
3605
  return None
3529
3606
  output_spool(battery_info_app_key)
3530
3607
  nbat = None
3531
- if info == 1 and bat.get('info') is not None:
3608
+ if bat.get('info') is not None:
3532
3609
  b = bat['info']
3533
3610
  output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
3534
3611
  nbat = 0
@@ -3551,7 +3628,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3551
3628
  return None
3552
3629
  nv = len(cell_volts)
3553
3630
  if nbat is None:
3554
- nbat = bat_count(nv) if count is None else count
3631
+ nbat = bat_count(nv) if bat.get('count') is None else bat['count']
3555
3632
  if nbat is None:
3556
3633
  output(f"** battery_info(): unable to match cells_per_battery for {nv}")
3557
3634
  output_close()
@@ -4251,17 +4328,9 @@ class Solcast :
4251
4328
  total_actual = None
4252
4329
  self.actual = get_history('day', d=day, v=v)
4253
4330
  plots = {}
4331
+ times = [i/2 for i in range(0, 48)]
4254
4332
  for v in self.actual:
4255
- times = []
4256
- actual_values = []
4257
- average = 0.0
4258
- for i in range(0, len(v.get('data'))):
4259
- average += v['data'][i]['value'] / 6
4260
- if i % 6 == 5:
4261
- times.append(round_time((i - 5) / 12))
4262
- actual_values.append(average)
4263
- average = 0
4264
- plots[v['variable']] = actual_values
4333
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4265
4334
  if v['variable'] == 'pvPower':
4266
4335
  total_actual = v.get('kwh')
4267
4336
  if total_actual is None:
@@ -4293,10 +4362,10 @@ class Solcast :
4293
4362
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
4294
4363
  total_forecast = sum(forecast_values) / 2
4295
4364
  plots['forecast'] = forecast_values
4296
- if total_actual is not None:
4297
- print(f" Total actual: {total_actual:.3f}kWh")
4298
4365
  if total_forecast is not None:
4299
4366
  print(f" Total forecast: {total_forecast:.3f}kWh")
4367
+ if total_actual is not None:
4368
+ print(f" Total actual: {total_actual:.3f}kWh")
4300
4369
  print()
4301
4370
  title = f"Forecast / Actual PV Power on {day}"
4302
4371
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4584,17 +4653,9 @@ class Solar :
4584
4653
  total_actual = None
4585
4654
  self.actual = get_history('day', d=day, v=v)
4586
4655
  plots = {}
4656
+ times = [i/2 for i in range(0, 48)]
4587
4657
  for v in self.actual:
4588
- times = []
4589
- actual_values = []
4590
- average = 0.0
4591
- for i in range(0, len(v.get('data'))):
4592
- average += v['data'][i]['value'] / 6
4593
- if i % 6 == 5:
4594
- times.append(round_time((i - 5) / 12))
4595
- actual_values.append(average)
4596
- average = 0
4597
- plots[v['variable']] = actual_values
4658
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4598
4659
  if v['variable'] == 'pvPower':
4599
4660
  total_actual = v.get('kwh')
4600
4661
  if total_actual is None:
@@ -4624,10 +4685,10 @@ class Solar :
4624
4685
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4625
4686
  total_forecast = sum(forecast_values) / 2
4626
4687
  plots['forecast'] = forecast_values
4627
- if total_actual is not None:
4628
- print(f" Total actual: {total_actual:.3f}kWh")
4629
4688
  if total_forecast is not None:
4630
4689
  print(f" Total forecast: {total_forecast:.3f}kWh")
4690
+ if total_actual is not None:
4691
+ print(f" Total actual: {total_actual:.3f}kWh")
4631
4692
  print()
4632
4693
  title = f"Forecast / Actual PV Power on {day}"
4633
4694
  plt.figure(figsize=(figure_width, figure_width/3))
foxesscloud/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 03 November 2024
4
+ Updated: 06 November 2024
5
5
  By: Tony Matthews
6
6
  """
7
7
  ##################################################################################################
@@ -10,7 +10,7 @@ By: Tony Matthews
10
10
  # ALL RIGHTS ARE RESERVED © Tony Matthews 2024
11
11
  ##################################################################################################
12
12
 
13
- version = "2.6.8"
13
+ version = "2.7.0"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -564,7 +564,7 @@ battery_params = {
564
564
  2: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
565
565
  'step': 5,
566
566
  'offset': 5,
567
- 'charge_loss': 1.08,
567
+ 'charge_loss': 1.07,
568
568
  'discharge_loss': 0.95},
569
569
  # Mira BMS with firmware 1.014 or later
570
570
  3: {'table': [ 0, 2, 10, 10, 15, 15, 25, 50, 50, 50, 30, 20, 0],
@@ -574,7 +574,7 @@ battery_params = {
574
574
  'discharge_loss': 0.974},
575
575
  }
576
576
 
577
- def get_battery(info=0, v=None):
577
+ def get_battery(info=0, v=None, rated=None, count=None):
578
578
  global device_sn, battery, debug_setting, residual_handling, battery_params
579
579
  if get_device() is None:
580
580
  return None
@@ -594,25 +594,29 @@ def get_battery(info=0, v=None):
594
594
  soc = battery.get('soc')
595
595
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
596
596
  if battery.get('count') is None:
597
- battery['count'] = int(battery['volt'] / 49)
597
+ battery['count'] = int(battery['volt'] / 49) if count is None else count
598
598
  if battery.get('ratedCapacity') is None:
599
- battery['ratedCapacity'] = 2560 * battery['count']
599
+ battery['ratedCapacity'] = 2560 * battery['count'] if rated is None else rated
600
600
  elif battery['residual_handling'] == 3:
601
601
  if battery.get('count') is None:
602
- battery['count'] = int(battery['volt'] / 49)
602
+ battery['count'] = int(battery['volt'] / 49) if count is None else count
603
603
  capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
604
604
  soc = battery.get('soc')
605
605
  residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
606
606
  if battery.get('ratedCapacity') is None:
607
- battery['ratedCapacity'] = 2450 * battery['count']
607
+ battery['ratedCapacity'] = 2450 * battery['count'] if rated is None else rated
608
608
  else:
609
609
  residual = battery.get('residual')
610
610
  soc = battery.get('soc')
611
611
  capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
612
+ if battery.get('count') is None or battery['count'] < 1:
613
+ battery['count'] = count
614
+ if battery.get('ratedCapacity') is None or battery['ratedCapacity'] < 100:
615
+ battery['ratedCapacity'] = rated
612
616
  battery['capacity'] = round(capacity, 3)
613
617
  battery['residual'] = round(residual, 3)
614
618
  battery['status'] = 1
615
- battery['charge_rate'] = 50
619
+ battery['charge_rate'] = None
616
620
  params = battery_params[battery['residual_handling']]
617
621
  battery['charge_loss'] = params['charge_loss']
618
622
  battery['discharge_loss'] = params['discharge_loss']
@@ -622,9 +626,13 @@ def get_battery(info=0, v=None):
622
626
  battery['charge_rate'] = params['table'][int((battery['temperature'] - params['offset']) / params['step'])]
623
627
  return battery
624
628
 
625
- def get_batteries(info=0):
629
+ def get_batteries(info=0, rated=None, count=None):
626
630
  global battery, batteries
627
- get_battery(info=info)
631
+ if type(rated) is not list:
632
+ rated = [rated]
633
+ if type(count) is not list:
634
+ count = [count]
635
+ get_battery(info=info, rated=rated[0], count=count[0])
628
636
  if battery is None:
629
637
  return None
630
638
  batteries = [battery]
@@ -663,7 +671,7 @@ def time_period(t, n):
663
671
  (enable, start, end) = (t['enable1'], t['startTime1'], t['endTime1']) if n == 1 else (t['enable2'], t['startTime2'], t['endTime2'])
664
672
  result = f"{start['hour']:02d}:{start['minute']:02d}-{end['hour']:02d}:{end['minute']:02d}"
665
673
  if start['hour'] != end['hour'] or start['minute'] != end['minute']:
666
- result += f" Charge from grid" if enable else f" Force Charge"
674
+ result += f" Charge from grid" if enable else f" Battery Hold"
667
675
  return result
668
676
 
669
677
  def set_charge(ch1=None, st1=None, en1=None, ch2=None, st2=None, en2=None, force = 0, enable=1):
@@ -860,17 +868,17 @@ def get_remote_settings(name):
860
868
  def get_named_settings(name):
861
869
  return get_remote_settings(name)
862
870
 
863
- def set_named_setting(name, value):
871
+ def set_named_settings(name, value, force=0):
864
872
  global device_sn, debug_setting
865
873
  if get_device() is None:
866
874
  return None
875
+ if force == 1 and get_schedule().get('enable'):
876
+ set_schedule(enable=0)
867
877
  if type(name) is list:
868
- count = 0
878
+ result = []
869
879
  for (n, v) in name:
870
- result = set_named_settings(name=n, value=v)
871
- if result is not None:
872
- count += 1
873
- return count
880
+ result.append(set_named_settings(name=n, value=v))
881
+ return result
874
882
  output(f"\nSetting {name} to {value}", 1)
875
883
  body = {'sn': device_sn, 'key': name, 'value': f"{value}"}
876
884
  setting_delay()
@@ -881,10 +889,10 @@ def set_named_setting(name, value):
881
889
  errno = response.json().get('errno')
882
890
  if errno != 0:
883
891
  if errno == 44096:
884
- output(f"** cannot update settings when schedule is active")
892
+ output(f"** cannot update {name} when schedule is active")
885
893
  else:
886
894
  output(f"** set_named_settings(): ({name}, {value}) {errno_message(response)}")
887
- return None
895
+ return 0
888
896
  return 1
889
897
 
890
898
  ##################################################################################################
@@ -1440,9 +1448,7 @@ get_raw = get_history
1440
1448
  def report_value_profile(result):
1441
1449
  if type(result) is not list or result[0]['type'] != 'day':
1442
1450
  return (None, None)
1443
- data = []
1444
- for h in range(0,24):
1445
- data.append((0.0, 0)) # value sum, count of values
1451
+ data = [(0.0, 0) for h in range(0,24)]
1446
1452
  totals = 0
1447
1453
  n = 0
1448
1454
  for day in result:
@@ -1469,6 +1475,30 @@ def report_value_profile(result):
1469
1475
  result.append(by_hour[t] * daily_average / current_total)
1470
1476
  return (daily_average, result)
1471
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
+
1472
1502
 
1473
1503
  ##################################################################################################
1474
1504
  # get production report in kWh
@@ -1964,7 +1994,7 @@ octopus_cosy = {
1964
1994
  # time periods for Octopus Go
1965
1995
  octopus_go = {
1966
1996
  'name': 'Octopus Go',
1967
- 'off_peak1': {'start': 0.5, 'end': 4.5, 'hold': 1},
1997
+ 'off_peak1': {'start': 0.5, 'end': 5.5, 'hold': 1},
1968
1998
  'forecast_times': [21, 22]
1969
1999
  }
1970
2000
 
@@ -2436,8 +2466,8 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2436
2466
  global charge_config, steps_per_hour
2437
2467
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2438
2468
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2439
- charge_loss = charge_config['charge_loss']
2440
- discharge_loss = charge_config['discharge_loss']
2469
+ charge_loss = charge_config['_charge_loss']
2470
+ discharge_loss = charge_config['_discharge_loss']
2441
2471
  charge_limit = charge_config['charge_limit']
2442
2472
  float_charge = charge_config['float_charge']
2443
2473
  run_time = len(work_mode_timed)
@@ -2530,6 +2560,8 @@ charge_config = {
2530
2560
  'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
2531
2561
  'pv_loss': 0.95, # loss converting PV power to DC battery charge power
2532
2562
  'ac_dc_loss': 0.963, # loss converting AC grid power to DC battery charge power
2563
+ 'charge_loss': None, # loss converting charge energy to stored energy
2564
+ 'discharge_loss': None, # loss converting stored energy to discharge energy
2533
2565
  'inverter_power': 101, # Inverter power consumption in W
2534
2566
  'bms_power': 50, # BMS power consumption in W
2535
2567
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2632,7 +2664,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2632
2664
  if len(times) == 0:
2633
2665
  times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'hold': force_charge})
2634
2666
  output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
2635
- time_to_end1 = None
2667
+ time_to_run = None
2636
2668
  for t in times:
2637
2669
  if hour_in(hour_now, t) and update_settings > 0:
2638
2670
  update_settings = 0
@@ -2644,7 +2676,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2644
2676
  t['time_to_start'] = time_to_start
2645
2677
  t['time_to_end'] = time_to_end
2646
2678
  t['charge_time'] = charge_time
2647
- time_to_end1 = time_to_end if time_to_end1 is None else time_to_end1
2679
+ if time_to_run is None:
2680
+ time_to_run = time_to_start
2648
2681
  # get next charge slot
2649
2682
  times = sorted(times, key=lambda t: t['time_to_start'])
2650
2683
  charge_key = times[0]['key']
@@ -2656,7 +2689,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2656
2689
  # work out time window and times with clock changes
2657
2690
  charge_today = (base_hour + time_to_start / steps_per_hour) < 24
2658
2691
  forecast_day = today if charge_today else tomorrow
2659
- run_to = time_to_end1 if time_to_end < time_to_end1 else time_to_end1 + 24 * steps_per_hour
2692
+ run_to = time_to_run if time_to_end < time_to_run else time_to_run + 24 * steps_per_hour
2660
2693
  run_time = int(run_to + 0.99) + 1 + hour_adjustment * steps_per_hour
2661
2694
  time_line = [round_time(base_hour + x / steps_per_hour - (hour_adjustment if x >= time_change else 0)) for x in range(0, run_time)]
2662
2695
  bat_hold = times[0]['hold']
@@ -2681,8 +2714,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2681
2714
  bat_power = 0.0
2682
2715
  temperature = 30
2683
2716
  bms_charge_current = 15
2684
- charge_loss = battery_params[2]['charge_loss']
2685
- discharge_loss = battery_params[2]['discharge_loss']
2717
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery_params[2]['charge_loss']
2718
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery_params[2]['discharge_loss']
2686
2719
  bat_current = 0.0
2687
2720
  device_power = 6.0
2688
2721
  device_current = 35
@@ -2704,19 +2737,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2704
2737
  output(f"Battery capacity could not be estimated. Please add the parameter 'capacity=xx' in kWh")
2705
2738
  return None
2706
2739
  bms_charge_current = battery.get('charge_rate')
2707
- charge_loss = battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
2708
- discharge_loss = battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
2740
+ charge_loss = charge_config['charge_loss'] if charge_config.get('charge_loss') is not None else battery['charge_loss'] if battery.get('charge_loss') is not None else 0.974
2741
+ discharge_loss = charge_config['discharge_loss'] if charge_config.get('discharge_loss') is not None else battery['discharge_loss'] if battery.get('discharge_loss') is not None else 0.974
2709
2742
  device_power = device.get('power')
2710
2743
  device_current = device.get('max_charge_current')
2711
2744
  model = device.get('deviceType')
2712
2745
  min_soc = charge_config['min_soc'] if charge_config['min_soc'] is not None else 10
2713
2746
  max_soc = charge_config['max_soc'] if charge_config['max_soc'] is not None else 100
2747
+ reserve = capacity * min_soc / 100
2748
+ # charge current may be derated based on temperature
2749
+ charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
2750
+ if bms_charge_current is not None and charge_current > bms_charge_current:
2751
+ charge_current = bms_charge_current
2714
2752
  volt_curve = charge_config['volt_curve']
2715
2753
  nominal_soc = charge_config['nominal_soc']
2716
2754
  volt_nominal = interpolate(nominal_soc / 10, volt_curve)
2717
2755
  bat_resistance = charge_config['bat_resistance'] * bat_volt / volt_nominal
2718
2756
  bat_ocv = (bat_volt + bat_current * bat_resistance) * volt_nominal / interpolate(current_soc / 10, volt_curve)
2719
- reserve = capacity * min_soc / 100
2720
2757
  output(f"\nBattery Info:")
2721
2758
  output(f" Capacity: {capacity:.2f}kWh")
2722
2759
  output(f" Residual: {residual:.2f}kWh")
@@ -2727,9 +2764,10 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2727
2764
  output(f" Current SoC: {current_soc}%")
2728
2765
  output(f" Max SoC: {max_soc}% ({capacity * max_soc / 100:.2f}kWh)")
2729
2766
  output(f" Temperature: {temperature:.1f}°C")
2730
- output(f" Max Charge: {bms_charge_current:.1f}A")
2767
+ output(f" Max Charge: {charge_current:.1f}A")
2731
2768
  output(f" Resistance: {bat_resistance:.2f} ohms")
2732
2769
  output(f" Nominal OCV: {bat_ocv:.1f}V at {nominal_soc}% SoC")
2770
+ output(f" Losses: {charge_loss * 100:.1f}% charge / {discharge_loss * 100:.1f}% discharge", 2)
2733
2771
  # charge current may be derated based on temperature
2734
2772
  charge_current = device_current if charge_config['charge_current'] is None else charge_config['charge_current']
2735
2773
  if charge_current > bms_charge_current:
@@ -2762,8 +2800,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2762
2800
  charge_config['charge_limit'] = charge_limit
2763
2801
  charge_config['charge_power'] = charge_power
2764
2802
  charge_config['float_charge'] = float_charge
2765
- charge_config['charge_loss'] = charge_loss
2766
- charge_config['discharge_loss'] = discharge_loss
2803
+ charge_config['_charge_loss'] = charge_loss
2804
+ charge_config['_discharge_loss'] = discharge_loss
2767
2805
  # display what we have
2768
2806
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
2769
2807
  output(f"\nDevice Info:")
@@ -2927,6 +2965,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2927
2965
  output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2928
2966
  charge_message = "no charge needed"
2929
2967
  kwh_needed = 0.0
2968
+ kwh_spare = kwh_min - reserve
2930
2969
  hours = 0.0
2931
2970
  start_timed = time_to_end
2932
2971
  end_timed = time_to_end
@@ -2934,6 +2973,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2934
2973
  else:
2935
2974
  # work out time to add kwh_needed to battery
2936
2975
  charge_rate = charge_power * charge_loss
2976
+ discharge_rate = max([(start_residual - end_residual) / charge_time - bms_loss, 0.0])
2937
2977
  hours = kwh_needed / charge_rate
2938
2978
  if test_charge is None:
2939
2979
  output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
@@ -2944,13 +2984,15 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2944
2984
  if hours > charge_time:
2945
2985
  hours = charge_time
2946
2986
  elif hours > hours_to_full:
2947
- kwh_shortfall = (hours - hours_to_full) * charge_rate # amount of energy that won't be added
2948
- required = hours_to_full + charge_time * kwh_shortfall / abs(start_residual - end_residual) # hold time to recover energy not added
2987
+ kwh_shortfall = kwh_needed - (capacity - start_residual) # amount of energy that won't be added
2988
+ required = (hours_to_full + kwh_shortfall / discharge_rate) if discharge_rate > 0.0 else charge_time
2949
2989
  hours = required if required > hours and required < charge_time else charge_time
2950
2990
  # round charge time and work out what will actually be added
2951
2991
  min_hours = charge_config['min_hours']
2952
2992
  hours = int(hours / min_hours + 0.99) * min_hours
2953
2993
  kwh_added = (hours * charge_rate) if hours < hours_to_full else (capacity - start_residual)
2994
+ kwh_added += discharge_rate * hours # discharge saved during charging
2995
+ kwh_spare = kwh_min - reserve + kwh_added
2954
2996
  # rework charge and discharge
2955
2997
  charge_period = get_best_charge_period(start_at, hours)
2956
2998
  charge_offset = round_time(charge_period['start'] - start_at) if charge_period is not None else 0
@@ -2986,7 +3028,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2986
3028
  end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
2987
3029
  # show the results
2988
3030
  output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
2989
- output(f" Contingency: {kwh_contingency / capacity * 100:.0f}% SoC ({kwh_contingency:.2f}kWh)")
3031
+ output(f" Contingency: {kwh_spare / capacity * 100:.0f}% SoC ({kwh_spare:.2f}kWh)")
2990
3032
  if not charge_today:
2991
3033
  output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
2992
3034
  # setup charging
@@ -3215,19 +3257,19 @@ def bat_count(cell_count):
3215
3257
  battery_info_app_key = "aug938dqt5cbqhvq69ixc4v39q6wtw"
3216
3258
 
3217
3259
  # show information about the current state of the batteries
3218
- def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3260
+ def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
3219
3261
  global debug_setting, battery_info_app_key
3220
3262
  if bat is None:
3221
- bats = get_batteries(info=info)
3263
+ bats = get_batteries(info=info, rated=rated, count=count)
3222
3264
  if bats is None:
3223
3265
  return None
3224
3266
  for i in range(0, len(bats)):
3225
3267
  output(f"\n----------------------- BMS {i+1} -----------------------")
3226
- battery_info(log=log, plot=plot, count=count, info=info, bat=bats[i])
3268
+ battery_info(log=log, plot=plot, info=info, bat=bats[i])
3227
3269
  return None
3228
3270
  output_spool(battery_info_app_key)
3229
3271
  nbat = None
3230
- if info == 1 and bat.get('info') is not None:
3272
+ if bat.get('info') is not None:
3231
3273
  b = bat['info']
3232
3274
  output(f"SN {b['masterSN']}, {b['masterBatType']}, Version {b['masterVersion']} (BMS)")
3233
3275
  nbat = 0
@@ -3250,7 +3292,7 @@ def battery_info(log=0, plot=1, count=None, info=1, bat=None):
3250
3292
  return None
3251
3293
  nv = len(cell_volts)
3252
3294
  if nbat is None:
3253
- nbat = bat_count(nv) if count is None else count
3295
+ nbat = bat_count(nv) if bat.get('count') is None else bat['count']
3254
3296
  if nbat is None:
3255
3297
  output(f"** battery_info(): unable to match cells_per_battery for {nv}")
3256
3298
  output_close()
@@ -3950,17 +3992,9 @@ class Solcast :
3950
3992
  total_actual = None
3951
3993
  self.actual = get_history('day', d=day, v=v)
3952
3994
  plots = {}
3995
+ times = [i/2 for i in range(0, 48)]
3953
3996
  for v in self.actual:
3954
- times = []
3955
- actual_values = []
3956
- average = 0.0
3957
- for i in range(0, len(v.get('data'))):
3958
- average += v['data'][i]['value'] / 6
3959
- if i % 6 == 5:
3960
- times.append(round_time((i - 5) / 12))
3961
- actual_values.append(average)
3962
- average = 0
3963
- plots[v['variable']] = actual_values
3997
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
3964
3998
  if v['variable'] == 'pvPower':
3965
3999
  total_actual = v.get('kwh')
3966
4000
  if total_actual is None:
@@ -3992,10 +4026,10 @@ class Solcast :
3992
4026
  forecast_values = [self.daily[day]['pt30'][hours_time(t - time_offset)] for t in times]
3993
4027
  total_forecast = sum(forecast_values) / 2
3994
4028
  plots['forecast'] = forecast_values
3995
- if total_actual is not None:
3996
- print(f" Total actual: {total_actual:.3f}kWh")
3997
4029
  if total_forecast is not None:
3998
4030
  print(f" Total forecast: {total_forecast:.3f}kWh")
4031
+ if total_actual is not None:
4032
+ print(f" Total actual: {total_actual:.3f}kWh")
3999
4033
  print()
4000
4034
  title = f"Forecast / Actual PV Power on {day}"
4001
4035
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -4283,17 +4317,9 @@ class Solar :
4283
4317
  total_actual = None
4284
4318
  self.actual = get_history('day', d=day, v=v)
4285
4319
  plots = {}
4320
+ times = [i/2 for i in range(0, 48)]
4286
4321
  for v in self.actual:
4287
- times = []
4288
- actual_values = []
4289
- average = 0.0
4290
- for i in range(0, len(v.get('data'))):
4291
- average += v['data'][i]['value'] / 6
4292
- if i % 6 == 5:
4293
- times.append(round_time((i - 5) / 12))
4294
- actual_values.append(average)
4295
- average = 0
4296
- plots[v['variable']] = actual_values
4322
+ plots[v['variable']] = rescale_history(v.get('data'), 2)
4297
4323
  if v['variable'] == 'pvPower':
4298
4324
  total_actual = v.get('kwh')
4299
4325
  if total_actual is None:
@@ -4323,10 +4349,10 @@ class Solar :
4323
4349
  forecast_values = [self.daily[day]['pt30'][hours_time(t)] for t in times]
4324
4350
  total_forecast = sum(forecast_values) / 2
4325
4351
  plots['forecast'] = forecast_values
4326
- if total_actual is not None:
4327
- print(f" Total actual: {total_actual:.3f}kWh")
4328
4352
  if total_forecast is not None:
4329
4353
  print(f" Total forecast: {total_forecast:.3f}kWh")
4354
+ if total_actual is not None:
4355
+ print(f" Total actual: {total_actual:.3f}kWh")
4330
4356
  print()
4331
4357
  title = f"Forecast / Actual PV Power on {day}"
4332
4358
  plt.figure(figsize=(figure_width, figure_width/3))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.6.8
3
+ Version: 2.7.0
4
4
  Summary: library for accessing Fox ESS cloud data using Open API
5
5
  Author-email: Tony Matthews <tony@quasair.co.uk>
6
6
  Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
@@ -104,8 +104,8 @@ Once an inverter is selected, you can make other calls to get information:
104
104
 
105
105
  ```
106
106
  f.get_generation()
107
- f.get_battery()
108
- f.get_batteries()
107
+ f.get_battery(info, rated, count)
108
+ f.get_batteries(info, rated, count)
109
109
  f.get_settings()
110
110
  f.get_charge()
111
111
  f.get_min()
@@ -119,7 +119,12 @@ Each of these calls will return a dictionary or list containing the relevant inf
119
119
  get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
120
120
 
121
121
  get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature' and 'residual'. The result also updates f.battery / f.batteries.
122
- get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Additional battery attributes include:
122
+ get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Parameters:
123
+ + 'info': get battery serial number info, if available. Default 0 (not available via Open API)
124
+ + 'rated': optional rated capacity for the battery in Wh to work out SoH. If not provided, it will try to work this out.
125
+ + 'count': optional battery count. If not provided, it will try to work this out.
126
+
127
+ Additional battery attributes provided include:
123
128
  + 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
124
129
  + 'charge_rate': the estimated BMS charge rate available, based on the current 'temperature' of the BMS
125
130
  + 'charge_loss': the ratio of the kWh added to the battery for each kWh applied during charging
@@ -145,7 +150,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
145
150
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
146
151
  f.charge_periods(st0, en0, st1, en1, st2, en2, min_soc, target_soc, start_soc)
147
152
  f.set_schedule(periods, enable)
148
- f.set_named_settings(name, value)
153
+ f.set_named_settings(name, value, force)
149
154
  ```
150
155
 
151
156
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -188,7 +193,9 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
188
193
 
189
194
  set_named_settings() sets the 'name' setting to 'value'.
190
195
  + 'name' may also be a list of (name, value) pairs.
191
- + the only 'name' currently supported by Fox is 'ExportLimit' on H3 inverters
196
+ + 'force': setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
197
+ + A return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
198
+ + the only 'name' currently supported is 'ExportLimit'
192
199
 
193
200
 
194
201
  ## Real Time Data
@@ -334,7 +341,7 @@ The previous section provides functions that can be used to access and control y
334
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.
335
342
 
336
343
  ```
337
- 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)
338
345
  ```
339
346
 
340
347
  All the parameters are optional:
@@ -345,6 +352,7 @@ All the parameters are optional:
345
352
  + update_settings: 0 no changes, 1 update charge settings. The default is 0
346
353
  + show_data: 1 show battery SoC data, 2 show battery Residual data, 3 show timed data. The default is 1.
347
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
348
356
 
349
357
  ### Modelling
350
358
 
@@ -387,6 +395,8 @@ export_limit: None # maximum export power in kW. None uses the inver
387
395
  dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
388
396
  pv_loss: 0.950 # loss converting PV power to DC battery charge power
389
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
390
400
  inverter_power: None # inverter power consumption in W (dynamically set)
391
401
  bms_power: 50 # BMS power consumption in W
392
402
  force_charge_power: 5.00 # power used when Force Charge is scheduled
@@ -797,6 +807,19 @@ This setting can be:
797
807
 
798
808
  # Version Info
799
809
 
810
+ 2.7.0<br>
811
+ Allow charge_loss / discharge_loss to be configured for charge_needed().
812
+ Change 'Force Charge' to 'Battery Hold' in charge times to avoid confusion with Force Charge work mode.
813
+ Correct problem with missing periods of actual data in forecast.compare()
814
+
815
+ 2.6.9<br>
816
+ Add get and set_named_settings() (for WorkMode and ExportLimit).
817
+ If a list of named settings is provided, the return value is a list indicating which settings succeeded (1) or failed (0).
818
+ Updates to get_battery() / get_batteries() to add optional rated and count parameters.
819
+ Updates to charge_needed() to end prediction at start of next charge period.
820
+ Correct charge time for Octopus Go tariff.
821
+ Update charge_needed() to show contingency achieved rather than requested.
822
+
800
823
  2.6.8<br>
801
824
  Add residual_handling=3 for Mira BMS with firmware 1.014 or later that returns residual capacity per battery.
802
825
  Calculate 'ratedCapacity' in get_battery() and 'soh' for HV2600 and Mira.
@@ -0,0 +1,7 @@
1
+ foxesscloud/foxesscloud.py,sha256=rr9famUpnu4L8sSyBgkcR1GfQyANvbfK05hw4p4d9LI,222420
2
+ foxesscloud/openapi.py,sha256=EvgmhA1zq70xx1YqpcifBa9AepRsqM7gt0x3M9M3Vrg,206406
3
+ foxesscloud-2.7.0.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
+ foxesscloud-2.7.0.dist-info/METADATA,sha256=NqT_e_whf-RIU3rvVqAUzNN7N09WPctPJOp7xhyRCuE,61042
5
+ foxesscloud-2.7.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
+ foxesscloud-2.7.0.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
+ foxesscloud-2.7.0.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- foxesscloud/foxesscloud.py,sha256=o4SAnr0y0dNtIXxY4BOxyoJE7g-tg3Tyt8EJupzGi6c,219881
2
- foxesscloud/openapi.py,sha256=XFtZGaWr11_CV5kawpz9aTQxATMmEdTXzMVDGUiTc3Y,204609
3
- foxesscloud-2.6.8.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
- foxesscloud-2.6.8.dist-info/METADATA,sha256=Inu6jh79s-Af8lcyBg06QWizy9rUY_5UWRNOBNkwO4A,59461
5
- foxesscloud-2.6.8.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- foxesscloud-2.6.8.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
- foxesscloud-2.6.8.dist-info/RECORD,,