foxesscloud 2.8.1__py3-none-any.whl → 2.8.3__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: 12 March 2025
4
+ Updated: 16 April 2025
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.9.3"
13
+ version = "1.9.5"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -657,11 +657,10 @@ def get_battery(info=1, rated=None, count=None):
657
657
  battery['soh'] = None
658
658
  battery['soh_supported'] = False
659
659
  if battery.get('status') is None:
660
- battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 0 else 1
661
- if battery.get('status') is None or battery['status'] != 1:
660
+ battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 10 else 1
661
+ if battery.get('status') is None or battery['status'] == 0:
662
662
  output(f"** get_battery(): battery status not available")
663
663
  return None
664
- battery['status'] = 1
665
664
  if battery.get('residual') is not None:
666
665
  battery['residual'] *= residual_scale / 10
667
666
  if battery['residual_handling'] == 2:
@@ -736,7 +735,7 @@ def get_batteries(info=1, rated=None, count=None):
736
735
  while len(count) < len(batteries):
737
736
  count.append(None)
738
737
  for i,b in enumerate(batteries):
739
- if b.get('status') is None or b['status'] != 1:
738
+ if b.get('status') is None or b['status'] == 0:
740
739
  output(f"** get_batteries(): battery {i+1} status not available")
741
740
  continue
742
741
  b['residual_handling'] = residual_handling
@@ -755,7 +754,7 @@ def get_batteries(info=1, rated=None, count=None):
755
754
  b['soh'] = int(soh) if soh.isnumeric() and int(soh) > 10 else None
756
755
  b['soh_supported'] = b['soh'] is not None
757
756
  for i, b in enumerate(batteries):
758
- if b.get('status') is None or b['status'] != 1:
757
+ if b.get('status') is None or b['status'] == 0:
759
758
  continue
760
759
  if i == 0:
761
760
  residual_handling = b['residual_handling']
@@ -973,6 +972,7 @@ merge_settings = { # keys to add
973
972
  'h116__': 'operation_mode__work_mode',
974
973
  'h117__': 'operation_mode__work_mode',
975
974
  # 'k106__': 'operation_mode__work_mode',
975
+ # 'k110__': 'operation_mode__work_mode',
976
976
  },
977
977
  'values': ['SelfUse', 'Feedin', 'Backup']},
978
978
  'ExportLimit': {'keys': {
@@ -980,6 +980,7 @@ merge_settings = { # keys to add
980
980
  'h116__': 'basic2__05',
981
981
  'h117__': 'basic2__05',
982
982
  # 'k106__': 'basic2__05',
983
+ # 'k1110__': 'basic2__05',
983
984
  },
984
985
  'valueType': 'int'},
985
986
  'BatteryVolt': {'keys': {
@@ -987,6 +988,7 @@ merge_settings = { # keys to add
987
988
  'h116__': ['h116__15', 'h116__16', 'h116__17'],
988
989
  'h117__': ['h117__15', 'h117__16', 'h117__17'],
989
990
  # 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
991
+ # 'k110__': ['k110__xx', 'k110__xx', 'k110__xx'],
990
992
  },
991
993
  'type': 'list',
992
994
  'valueType': 'float',
@@ -996,12 +998,20 @@ merge_settings = { # keys to add
996
998
  'h116__': 'h116__18',
997
999
  'h117__': 'h117__18',
998
1000
  # 'k106__': 'k106__xx',
1001
+ # 'k110__': 'k110__xx',
999
1002
  },
1000
1003
  'type': 'list',
1001
1004
  'valueType': 'int',
1002
1005
  'unit': '℃'},
1003
1006
  }
1004
1007
 
1008
+ # change named settings, if required, so names match between Fox API and Open API
1009
+ translate_names = {
1010
+ 'MaxSoc': 'MaximumSoC',
1011
+ 'MinSoc': 'MinimumSoC',
1012
+ 'MinSocOnGrid': 'MinimumSoC-OnGrid'
1013
+ }
1014
+
1005
1015
  def get_ui():
1006
1016
  global device_id, debug_setting, messages, remote_settings, named_settings, merge_settings
1007
1017
  if get_device() is None:
@@ -1066,7 +1076,7 @@ def get_ui():
1066
1076
  return remote_settings
1067
1077
 
1068
1078
  def get_remote_settings(key):
1069
- global device_id, debug_setting, messages
1079
+ global device_id, debug_setting, messages, translate_settings
1070
1080
  if get_device() is None:
1071
1081
  return None
1072
1082
  output(f"getting remote settings", 2)
@@ -1098,7 +1108,7 @@ def get_remote_settings(key):
1098
1108
  return values
1099
1109
 
1100
1110
  def get_named_settings(name):
1101
- global named_settings
1111
+ global named_settings, translate_names
1102
1112
  if get_device() is None:
1103
1113
  return None
1104
1114
  if type(name) is list:
@@ -1106,10 +1116,11 @@ def get_named_settings(name):
1106
1116
  for n in name:
1107
1117
  result.append(get_named_settings(n))
1108
1118
  return result
1109
- if named_settings is None or named_settings.get(name) is None:
1119
+ key_name = translate_names[name] if translate_names.get(name) is not None else name
1120
+ if named_settings is None or named_settings.get(key_name) is None:
1110
1121
  output(f"** get_named_settings(): {name} was not recognised")
1111
1122
  return None
1112
- keys = named_settings[name].get('keys')
1123
+ keys = named_settings[key_name].get('keys')
1113
1124
  if keys is None:
1114
1125
  output(f"** get_named_settings(): no keys for name: {name}")
1115
1126
  return None
@@ -1118,8 +1129,8 @@ def get_named_settings(name):
1118
1129
  if result is None:
1119
1130
  output(f"** get_named_settings(): no result for {name} using key: {keys}")
1120
1131
  return None
1121
- result_type = named_settings[name].get('type')
1122
- value_type = named_settings[name].get('valueType')
1132
+ result_type = named_settings[key_name].get('type')
1133
+ value_type = named_settings[key_name].get('valueType')
1123
1134
  if result_type is None:
1124
1135
  v = result.get([k for k in result.keys()][0])
1125
1136
  return v if value_type is None else c_float(v) if value_type == 'float' else c_int(v)
@@ -1131,7 +1142,7 @@ def get_named_settings(name):
1131
1142
  return result
1132
1143
 
1133
1144
  def set_named_settings(name, value, force=0):
1134
- global named_settings
1145
+ global named_settings, translate_names
1135
1146
  if get_device() is None:
1136
1147
  return None
1137
1148
  if force == 1 and get_schedule().get('enable'):
@@ -1141,18 +1152,19 @@ def set_named_settings(name, value, force=0):
1141
1152
  for (n, v) in name:
1142
1153
  result.append(set_named_settings(name=n, value=v))
1143
1154
  return result
1144
- if named_settings is None or named_settings.get(name) is None:
1155
+ key_name = translate_names[name] if translate_names.get(name) is not None else name
1156
+ if named_settings is None or named_settings.get(key_name) is None:
1145
1157
  output(f"** set_named_settings(): {name} was not recognised")
1146
1158
  return None
1147
- keys = named_settings[name].get('keys')
1159
+ keys = named_settings[key_name].get('keys')
1148
1160
  if keys is None:
1149
1161
  output(f"** set_named_settings(): no keys for name: {name}")
1150
1162
  return None
1151
- item_type = named_settings[name].get('type')
1163
+ item_type = named_settings[key_name].get('type')
1152
1164
  if item_type is None:
1153
1165
  values = {keys: str(value)}
1154
1166
  elif item_type == 'block':
1155
- items = named_setting[name]['items']
1167
+ items = named_setting[key_name]['items']
1156
1168
  n = len(items)
1157
1169
  if type(value) is not list or n != len(value):
1158
1170
  output(f"** set_named_settings(): {name} requires list of {n} values")
@@ -3089,7 +3101,7 @@ def charge_needed(forecast=None, consumption=None, update_settings=0, timed_mode
3089
3101
  else:
3090
3102
  # get device and battery info from inverter
3091
3103
  get_battery()
3092
- if battery is None or battery['status'] != 1:
3104
+ if battery is None or battery['status'] == 0:
3093
3105
  return None
3094
3106
  current_soc = battery['soc']
3095
3107
  bat_volt = battery['volt']
@@ -4485,7 +4497,7 @@ class Solar :
4485
4497
  if debug_setting > 0 and not quiet:
4486
4498
  print(f"Getting data for {name} array")
4487
4499
  path = f"{a['lat']}/{a['lon']}/{a['dec']}/{a['az']}/{a['kwp']}"
4488
- params = {'start': '00:00', 'no_sun': 1, 'damping': a['dam'], 'inverter': a['inv'], 'horizon': a['hor']}
4500
+ params = {'no_sun': 1, 'damping': a['dam'], 'inverter': a['inv'], 'horizon': a['hor']}
4489
4501
  response = requests.get(solar_url + self.api_key + 'estimate/' + path, params = params)
4490
4502
  if response.status_code != 200:
4491
4503
  if response.status_code == 429:
foxesscloud/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 12 March 2025
4
+ Updated: 16 April 2025
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.8.1"
13
+ version = "2.8.3"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -597,11 +597,10 @@ def get_battery(info=0, v=None, rated=None, count=None):
597
597
  battery['soh'] = None
598
598
  battery['soh_supported'] = False
599
599
  if battery.get('status') is None:
600
- battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 0 else 1
601
- if battery['status'] != 1:
600
+ battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 10 else 1
601
+ if battery['status'] == 0:
602
602
  output(f"** get_battery(): battery status not available")
603
603
  return None
604
- battery['status'] = 1
605
604
  if battery['residual_handling'] == 2:
606
605
  capacity = battery.get('residual')
607
606
  soc = battery.get('soc')
@@ -628,7 +627,6 @@ def get_battery(info=0, v=None, rated=None, count=None):
628
627
  battery['ratedCapacity'] = rated
629
628
  battery['capacity'] = round(capacity, 3)
630
629
  battery['residual'] = round(residual, 3)
631
- battery['status'] = 1
632
630
  battery['charge_rate'] = None
633
631
  params = battery_params[battery['residual_handling']]
634
632
  battery['charge_loss'] = params['charge_loss']
@@ -863,6 +861,7 @@ def get_remote_settings(name):
863
861
  values[x] = v[x]
864
862
  return values
865
863
  body = {'sn': device_sn, 'key': name}
864
+ setting_delay()
866
865
  response = signed_post(path="/op/v0/device/setting/get", body=body)
867
866
  if response.status_code != 200:
868
867
  output(f"** get_remote_settings() got response code {response.status_code}: {response.reason}")
@@ -1018,10 +1017,9 @@ def get_flag():
1018
1017
  if result is None:
1019
1018
  return None
1020
1019
  if schedule is None:
1021
- schedule = {'enable': None, 'support': None, 'periods': None}
1020
+ schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': False}
1022
1021
  schedule['enable'] = result.get('enable')
1023
1022
  schedule['support'] = result.get('support')
1024
- schedule['maxsoc'] = False
1025
1023
  if device.get('function') is not None and device['function'].get('scheduler') is not None:
1026
1024
  device['function']['scheduler'] = schedule['support']
1027
1025
  return schedule
@@ -1057,6 +1055,8 @@ def get_schedule():
1057
1055
  for g in result['groups']:
1058
1056
  if g['enable'] == 1 and g['workMode'] in work_modes:
1059
1057
  schedule['periods'].append(g)
1058
+ if g.get('maxSoc') is not None:
1059
+ schedule['maxsoc'] = True
1060
1060
  return schedule
1061
1061
 
1062
1062
  # build strategy using current schedule
@@ -2756,7 +2756,7 @@ def charge_needed(forecast=None, consumption=None, update_settings=0, timed_mode
2756
2756
  else:
2757
2757
  # get device and battery info from inverter
2758
2758
  get_battery()
2759
- if battery is None or battery['status'] != 1:
2759
+ if battery is None or battery['status'] == 0:
2760
2760
  return None
2761
2761
  current_soc = battery['soc']
2762
2762
  bat_volt = battery['volt']
@@ -4153,7 +4153,7 @@ class Solar :
4153
4153
  if debug_setting > 0 and not quiet:
4154
4154
  print(f"Getting data for {name} array")
4155
4155
  path = f"{a['lat']}/{a['lon']}/{a['dec']}/{a['az']}/{a['kwp']}"
4156
- params = {'start': '00:00', 'no_sun': 1, 'damping': a['dam'], 'inverter': a['inv'], 'horizon': a['hor']}
4156
+ params = {'no_sun': 1, 'damping': a['dam'], 'inverter': a['inv'], 'horizon': a['hor']}
4157
4157
  response = requests.get(solar_url + self.api_key + 'estimate/' + path, params = params)
4158
4158
  if response.status_code != 200:
4159
4159
  if response.status_code == 429:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.8.1
3
+ Version: 2.8.3
4
4
  Summary: library for accessing Fox ESS cloud data using Open API
5
5
  Author-email: Tony Matthews <tony@quasair.co.uk>
6
6
  Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
@@ -132,13 +132,14 @@ Additional battery attributes provided include:
132
132
 
133
133
  get_settings() will return the battery settings and is equivalent to get_charge() and get_min(). The results are stored in f.battery_settings. The settings include minSoc, minSocOnGrid, enable charge from grid and the charge times.
134
134
 
135
- get_flag() returns the current scheduler enable / support / maxsoc flags
135
+ get_flag() returns the current scheduler enable / support / maxsoc flags. By default support for Max Soc is set to False.
136
136
 
137
137
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
138
+ + if the schedule returned contains any values for 'maxSoc', the f.schedule['maxsoc'] is set to True to indicate that the current inverter supports setting Max Soc in schedules and Max Soc values are set by set_schedule().
138
139
 
139
140
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
140
- + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name.
141
- + the only name currently supported by Fox is 'ExportLimit' and this is only available for H3 inverters.
141
+ + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
142
+ + named_settings current supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode
142
143
 
143
144
 
144
145
  ## Inverter Settings
@@ -194,8 +195,8 @@ set_schedule() configures a list of scheduled work mode / soc changes with enabl
194
195
  set_named_settings() sets the 'name' setting to 'value'.
195
196
  + 'name' may also be a list of (name, value) pairs.
196
197
  + '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'
198
+ + a return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
199
+ + named_settings current supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode
199
200
 
200
201
 
201
202
  ## Real Time Data
@@ -620,6 +621,17 @@ Plunge pricing allows for the automatic configuration of charging periods when A
620
621
  # PV Output
621
622
  These functions produce CSV data for upload to [pvoutput.org](https://pvoutput.org) including PV generation, Export, Load and Grid consumption by day in Wh. The functions use the energy estimates created from the raw power data (see above). The estimates include PV energy generation that are not otherwise available from the Fox Cloud. Typically, the energy results are within 3% of the values reported by the meters built into the inverter.
622
623
 
624
+ ## Calibration
625
+ PV generation data is created using the Riemann sum of the PV power and CT2 power as the history of the stats provided by the Fox cloud can be unreliable. You can change the calibration factors used:
626
+
627
+ ```
628
+ f.pv_calibration = 0.98
629
+ f.ct2_calibration = 0.92
630
+ ```
631
+ * pv_calibration is a DC calibration factor and is multiplied by the Riemann sum. The default calibration factor of 0.98 was derrived by comparing the total solar production calculated against the inverter generation data provided via Modbus over 12 months
632
+
633
+ * ct2_calibration is an AC calibration factor and is the divisor for the Riemann sum. CT2 measures the AC power output by the inverter and this factor converts this back to the DC power coming from the solar panels into the inverter, assuming an MPPT efficiency of 95% and DC-AC conversion efficiency of 97%. This aligns the solar panel generation with PV power, allowing the total solar generation from both primary (connected to the hybrid inverter) and secondary (connected via a separate solar inverter) panels to be combined correctly.
634
+
623
635
 
624
636
  ## Get PV Output Data
625
637
 
@@ -809,6 +821,13 @@ This setting can be:
809
821
 
810
822
  # Version Info
811
823
 
824
+ 2.8.3<br>
825
+ Update to support setting Max Soc in schedules now this is supported by Fox using Open API.
826
+
827
+ 2.8.2<br>
828
+ Fix forecast.solar (after change to start parameter processing).
829
+ Change logic around battery status so 0 is offline and others values are online.
830
+
812
831
  2.8.1<br>
813
832
  Update from v0 to v1 for scheduler API.
814
833
  PVEnergyTotal added to report variables.
@@ -0,0 +1,7 @@
1
+ foxesscloud/foxesscloud.py,sha256=E48TEwk75XrLcJNXNvuMwG93VjjIYknuPERqSYGhRDE,224825
2
+ foxesscloud/openapi.py,sha256=d-qKK_XjpnRQpnAQ7HcLZSP7hbViHQ9tckM5o4Xf0eE,208484
3
+ foxesscloud-2.8.3.dist-info/LICENCE,sha256=8JF-24QkE8UfdII-g6RaIEvM-PZ9zwaEcxlwYUDMt-4,1079
4
+ foxesscloud-2.8.3.dist-info/METADATA,sha256=ECLbKmesBVJ5HRnvGXTw842d9q8XPhAc2y0MKuP9AyE,65179
5
+ foxesscloud-2.8.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
+ foxesscloud-2.8.3.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
+ foxesscloud-2.8.3.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- foxesscloud/foxesscloud.py,sha256=keJUa9j7OhAJF1lnbNAbTaFTnJm3T-XyiyuWOwF8KdM,224235
2
- foxesscloud/openapi.py,sha256=HTYc-ueI_gZRelOduaABzWBzaGsJTZlpo_1fWqkEhSc,208461
3
- foxesscloud-2.8.1.dist-info/LICENCE,sha256=8JF-24QkE8UfdII-g6RaIEvM-PZ9zwaEcxlwYUDMt-4,1079
4
- foxesscloud-2.8.1.dist-info/METADATA,sha256=ptLAC2vtyV5pYu-HE8p8Ot_JoMPgnTCYNChXA0dCmv0,63522
5
- foxesscloud-2.8.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- foxesscloud-2.8.1.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
- foxesscloud-2.8.1.dist-info/RECORD,,