foxesscloud 2.9.1__tar.gz → 2.9.3__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.9.1
3
+ Version: 2.9.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
@@ -61,6 +61,7 @@ You don't have to configure all of the settings. Your Fox ESS Cloud api key is t
61
61
  For example, replace _my.fox_api_key_ with the API key. Add you inverter serial number if you have more than 1 inverter linked to your account. Be sure to keep the double quotes around the values you enter or you will get a syntax error.
62
62
 
63
63
  Residual handling configures how battery residual energy reported by Fox is handled:
64
+ + 0: Use rated capacity, SoH and SoC to calculate residual energy (default)
64
65
  + 1: Fox returns the current battery residual energy and battery capacity is calculated using soc
65
66
  + 2: Fox returns the current battery capacity and battery residual is calculated using soc
66
67
  + 3: Fox returns the residual capacity per battery (Mira)
@@ -108,8 +109,8 @@ Once an inverter is selected, you can make other calls to get information:
108
109
 
109
110
  ```
110
111
  f.get_generation()
111
- f.get_battery(info, rated, count)
112
- f.get_batteries(info, rated, count)
112
+ f.get_battery()
113
+ f.get_batteries(info)
113
114
  f.get_settings()
114
115
  f.get_charge()
115
116
  f.get_min()
@@ -117,17 +118,15 @@ f.get_peakshaving()
117
118
  f.get_flag()
118
119
  f.get_schedule()
119
120
  f.get_named_settings(name)
121
+ f.get_battery_heating()
120
122
 
121
123
  ```
122
124
  Each of these calls will return a dictionary or list containing the relevant information.
123
125
 
124
126
  get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
125
127
 
126
- 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.
127
- get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Parameters:
128
- + 'info': get battery serial number info, if available. Default 0 (not available via Open API)
129
- + 'rated': optional rated capacity for the battery in Wh to work out SoH. If not provided, it will try to work this out.
130
- + 'count': optional battery count. If not provided, it will try to work this out.
128
+ get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature', 'residual' and 'throughput'. The result also updates f.battery / f.batteries.
129
+ get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery.
131
130
 
132
131
  Additional battery attributes provided include:
133
132
  + 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
@@ -146,8 +145,9 @@ get_schedule() returns the current work mode / soc schedule settings. The result
146
145
 
147
146
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
147
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
149
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
148
+ + named_settings currently available are stored in f.name_list. The settings supported depends on the inverter model and firmware version. An error will be returned if an unsupported variable is used.
150
149
 
150
+ get_battery_heating returns the current battery heating parameters and store these in f.battery_heating
151
151
 
152
152
  ## Inverter Settings
153
153
  You can change inverter settings using:
@@ -158,6 +158,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
158
158
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
159
159
  f.set_schedule(periods, enable)
160
160
  f.set_named_settings(name, value, force)
161
+ f.set_battery_heating(enable, start, end, time1, time2, time3)
161
162
  ```
162
163
 
163
164
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -180,12 +181,16 @@ set_period() returns a period structure that can be used to build a list for set
180
181
  + max_soc: optional, default is 100
181
182
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
182
183
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
184
+ + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
185
+ + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
183
186
  + price: optional, default None. Used to display plunge pricing for time period.
184
187
  + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
185
188
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
186
189
 
187
190
  Before calling set_period(), do at least one call to get_schedule(). This will inspect the schedule result to check if max_soc is supported and set the flag f.schedule['maxsoc'] to enable or disable this field as appropriate.
188
191
 
192
+ The variable f.max_periods is used to check the number of time periods allowed. The default is 8.
193
+
189
194
  set_schedule() configures a list of scheduled work mode / soc changes with enable=1. If called with enable=0, any existing schedules are disabled. To enable a schedule, you must provide a list of time segments
190
195
  + periods: a time segment or list of time segments created using f.set_period().
191
196
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
@@ -194,8 +199,12 @@ set_named_settings() sets the 'name' setting to 'value'.
194
199
  + 'name' may also be a list of (name, value) pairs.
195
200
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
196
201
  + a return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
197
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
202
+ + named_settings currently available are stored in f.name_list. The settings supported depend on the inverter model and firmware version. An error will be returned if an unsupported varaible is used.
198
203
 
204
+ set_battery_heating() set the heating parameters as follows:
205
+ + enable: optional, 0 or 1, default is 1
206
+ + start, end: optional start and end temperatures. The defaults are start at 9C and end at 12C.
207
+ + time1, time2, time3: optional times when the battery can heat from the grid time. The structure is {'enable': 1, 'start': '00:30', 'end': '05:30'}. The time slot is disabled by default.
199
208
 
200
209
  ## Real Time Data
201
210
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -821,6 +830,19 @@ This setting can be:
821
830
 
822
831
  # Version Info
823
832
 
833
+ 2.9.3 - 2026/01/18<br>
834
+ Update get_device() to use v1 API call.
835
+ Update get_battery() to work out battery count and rated capacity from device battery list.
836
+ Update default residual_handling to option 0 (residual is calculated from rated capacity, SoH and SoC).
837
+ Added get_battery_real() for testing (Fox does not currently populate most of the data).
838
+ Added 'throughput' to battery variables returned.
839
+ Add f.name_list to hold the list of setting variables and update the list.
840
+ Added get_battery_heating() and set_battery_heating().
841
+
842
+ 2.9.2 - 2025/11/30<br>
843
+ Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
844
+ Added f.max_periods to allow for more time periods in schedules (default is 8).
845
+
824
846
  2.9.1 - 2025/10/25<br>
825
847
  Change default contingency for charge_needed() to [25,15,10,20] and special contingency to 35.
826
848
  Update info shown around contingency.
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.9.1
4
- Summary: library for accessing Fox ESS cloud data using Open API
5
- Author-email: Tony Matthews <tony@quasair.co.uk>
6
- Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
7
- Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Requires-Python: >=3.7
12
- Description-Content-Type: text/markdown
13
- License-File: LICENCE
14
-
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>
@@ -61,6 +47,7 @@ You don't have to configure all of the settings. Your Fox ESS Cloud api key is t
61
47
  For example, replace _my.fox_api_key_ with the API key. Add you inverter serial number if you have more than 1 inverter linked to your account. Be sure to keep the double quotes around the values you enter or you will get a syntax error.
62
48
 
63
49
  Residual handling configures how battery residual energy reported by Fox is handled:
50
+ + 0: Use rated capacity, SoH and SoC to calculate residual energy (default)
64
51
  + 1: Fox returns the current battery residual energy and battery capacity is calculated using soc
65
52
  + 2: Fox returns the current battery capacity and battery residual is calculated using soc
66
53
  + 3: Fox returns the residual capacity per battery (Mira)
@@ -108,8 +95,8 @@ Once an inverter is selected, you can make other calls to get information:
108
95
 
109
96
  ```
110
97
  f.get_generation()
111
- f.get_battery(info, rated, count)
112
- f.get_batteries(info, rated, count)
98
+ f.get_battery()
99
+ f.get_batteries(info)
113
100
  f.get_settings()
114
101
  f.get_charge()
115
102
  f.get_min()
@@ -117,17 +104,15 @@ f.get_peakshaving()
117
104
  f.get_flag()
118
105
  f.get_schedule()
119
106
  f.get_named_settings(name)
107
+ f.get_battery_heating()
120
108
 
121
109
  ```
122
110
  Each of these calls will return a dictionary or list containing the relevant information.
123
111
 
124
112
  get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
125
113
 
126
- 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.
127
- get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Parameters:
128
- + 'info': get battery serial number info, if available. Default 0 (not available via Open API)
129
- + 'rated': optional rated capacity for the battery in Wh to work out SoH. If not provided, it will try to work this out.
130
- + 'count': optional battery count. If not provided, it will try to work this out.
114
+ get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature', 'residual' and 'throughput'. The result also updates f.battery / f.batteries.
115
+ get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery.
131
116
 
132
117
  Additional battery attributes provided include:
133
118
  + 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
@@ -146,8 +131,9 @@ get_schedule() returns the current work mode / soc schedule settings. The result
146
131
 
147
132
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
133
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
149
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
134
+ + named_settings currently available are stored in f.name_list. The settings supported depends on the inverter model and firmware version. An error will be returned if an unsupported variable is used.
150
135
 
136
+ get_battery_heating returns the current battery heating parameters and store these in f.battery_heating
151
137
 
152
138
  ## Inverter Settings
153
139
  You can change inverter settings using:
@@ -158,6 +144,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
158
144
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
159
145
  f.set_schedule(periods, enable)
160
146
  f.set_named_settings(name, value, force)
147
+ f.set_battery_heating(enable, start, end, time1, time2, time3)
161
148
  ```
162
149
 
163
150
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -180,12 +167,16 @@ set_period() returns a period structure that can be used to build a list for set
180
167
  + max_soc: optional, default is 100
181
168
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
182
169
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
170
+ + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
171
+ + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
183
172
  + price: optional, default None. Used to display plunge pricing for time period.
184
173
  + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
185
174
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
186
175
 
187
176
  Before calling set_period(), do at least one call to get_schedule(). This will inspect the schedule result to check if max_soc is supported and set the flag f.schedule['maxsoc'] to enable or disable this field as appropriate.
188
177
 
178
+ The variable f.max_periods is used to check the number of time periods allowed. The default is 8.
179
+
189
180
  set_schedule() configures a list of scheduled work mode / soc changes with enable=1. If called with enable=0, any existing schedules are disabled. To enable a schedule, you must provide a list of time segments
190
181
  + periods: a time segment or list of time segments created using f.set_period().
191
182
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
@@ -194,8 +185,12 @@ set_named_settings() sets the 'name' setting to 'value'.
194
185
  + 'name' may also be a list of (name, value) pairs.
195
186
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
196
187
  + a return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
197
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
188
+ + named_settings currently available are stored in f.name_list. The settings supported depend on the inverter model and firmware version. An error will be returned if an unsupported varaible is used.
198
189
 
190
+ set_battery_heating() set the heating parameters as follows:
191
+ + enable: optional, 0 or 1, default is 1
192
+ + start, end: optional start and end temperatures. The defaults are start at 9C and end at 12C.
193
+ + time1, time2, time3: optional times when the battery can heat from the grid time. The structure is {'enable': 1, 'start': '00:30', 'end': '05:30'}. The time slot is disabled by default.
199
194
 
200
195
  ## Real Time Data
201
196
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -821,6 +816,19 @@ This setting can be:
821
816
 
822
817
  # Version Info
823
818
 
819
+ 2.9.3 - 2026/01/18<br>
820
+ Update get_device() to use v1 API call.
821
+ Update get_battery() to work out battery count and rated capacity from device battery list.
822
+ Update default residual_handling to option 0 (residual is calculated from rated capacity, SoH and SoC).
823
+ Added get_battery_real() for testing (Fox does not currently populate most of the data).
824
+ Added 'throughput' to battery variables returned.
825
+ Add f.name_list to hold the list of setting variables and update the list.
826
+ Added get_battery_heating() and set_battery_heating().
827
+
828
+ 2.9.2 - 2025/11/30<br>
829
+ Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
830
+ Added f.max_periods to allow for more time periods in schedules (default is 8).
831
+
824
832
  2.9.1 - 2025/10/25<br>
825
833
  Change default contingency for charge_needed() to [25,15,10,20] and special contingency to 35.
826
834
  Update info shown around contingency.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.9.1"
7
+ version = "2.9.3"
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: 25 October 2025
4
+ Updated: 18 January 2026
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.10.0"
13
+ version = "1.11.0"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -1862,8 +1862,8 @@ def rescale_history(data, steps):
1862
1862
  # station = 0: use device_id, 1 = use station_id
1863
1863
  ##################################################################################################
1864
1864
 
1865
- report_vars = ['PVEnergyTotal', 'input','generation', 'feedin', 'loads', 'gridConsumption', 'chargeEnergyToTal', 'dischargeEnergyToTal']
1866
- report_names = ['PV Yield', 'Input', 'Generation', 'Grid Export', 'Consumption', 'Grid Import', 'Battery Charge', 'Battery Discharge']
1865
+ report_vars = ['PVEnergyTotal', 'generation', 'feedin', 'loads', 'gridConsumption', 'chargeEnergyToTal', 'dischargeEnergyToTal']
1866
+ report_names = ['PV Yield', 'Generation', 'Grid Export', 'Consumption', 'Grid Import', 'Battery Charge', 'Battery Discharge']
1867
1867
 
1868
1868
  # fix power values after fox corrupts high word of 32-bit energy total
1869
1869
  fix_values = 1
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 25 October 2025
4
+ Updated: 18 January 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.9.1"
13
+ version = "2.9.3"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -486,7 +486,7 @@ def get_device(sn=None, device_type=None):
486
486
  # load information for the device
487
487
  device_sn = device_list[n].get('deviceSN')
488
488
  params = {'sn': device_sn }
489
- response = signed_get(path="/op/v0/device/detail", params=params)
489
+ response = signed_get(path="/op/v1/device/detail", params=params)
490
490
  if response.status_code != 200:
491
491
  output(f"** get_device() got detail response code {response.status_code}: {response.reason}")
492
492
  return None
@@ -574,11 +574,11 @@ def get_generation(update=1):
574
574
  battery = None
575
575
  batteries = None
576
576
  battery_settings = None
577
- battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy','SOH' ]
578
- battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual', 'soh']
577
+ battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy','SOH','energyThroughput' ]
578
+ battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual', 'soh', 'throughput']
579
579
 
580
580
  # 1 = Residual Energy, 2 = Residual Capacity (HV), 3 = Residual Capacity per battery (Mira)
581
- residual_handling = 1
581
+ residual_handling = 0
582
582
 
583
583
  # charge rates based on residual_handling. Index is bms temperature
584
584
  battery_params = {
@@ -611,57 +611,53 @@ def get_battery(info=0, v=None, rated=None, count=None):
611
611
  global device_sn, battery, debug_setting, residual_handling, battery_params
612
612
  if get_device() is None:
613
613
  return None
614
+ battery = {}
615
+ rated = 0
616
+ count = 0
617
+ for b in device['batteryList']:
618
+ if b.get('type') == 'bmu' and b.get('capacity') is not None:
619
+ rated += b['capacity']
620
+ count += 1
621
+ if count > 0:
622
+ battery['count'] = count
623
+ battery['ratedCapacity'] = rated
624
+ else:
625
+ output(f"** get_battery(): battery capacity not available")
626
+ return None
614
627
  output(f"getting battery", 2)
615
628
  if v is None:
616
629
  v = battery_vars
617
630
  result = get_real(v)
618
- battery = {}
619
631
  for i in range(0, len(battery_vars)):
620
632
  battery[battery_data[i]] = result[i].get('value')
621
633
  if debug_setting > 1:
622
634
  print(f"raw battery = {battery}")
623
- battery['residual_handling'] = residual_handling
624
- battery['soh'] = None
625
- battery['soh_supported'] = False
626
635
  if battery.get('status') is None:
627
636
  battery['status'] = 0 if battery.get('volt') is None or battery['volt'] <= 10 else 1
628
637
  if battery['status'] == 0:
629
638
  output(f"** get_battery(): battery status not available")
630
639
  return None
631
- if battery['residual_handling'] == 2:
640
+ capacity = battery['ratedCapacity'] / 1000 * (battery['soh'] if battery.get('soh') is not None else 100) / 100
641
+ soc = battery.get('soc')
642
+ battery['residual_handling'] = residual_handling
643
+ if battery['residual_handling'] == 1:
644
+ capacity = battery['residual'] / soc * 100
645
+ battery['soh'] = round(capacity * 1000 / battery['ratedCapacity'] * 100, 1)
646
+ elif battery['residual_handling'] == 2:
632
647
  capacity = battery.get('residual')
633
- soc = battery.get('soc')
634
- residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
635
- if battery.get('count') is None:
636
- battery['count'] = int(battery['volt'] / 49) if count is None else count
637
- if battery.get('ratedCapacity') is None:
638
- battery['ratedCapacity'] = 2560 * battery['count'] if rated is None else rated
648
+ battery['soh'] = round(capacity * 1000 / battery['ratedCapacity'] * 100, 1)
639
649
  elif battery['residual_handling'] == 3:
640
- if battery.get('count') is None:
641
- battery['count'] = int(battery['volt'] / 49) if count is None else count
642
650
  capacity = (battery['residual'] * battery['count']) if battery.get('residual') is not None else None
643
- soc = battery.get('soc')
644
- residual = capacity * soc / 100 if capacity is not None and soc is not None else capacity
645
- if battery.get('ratedCapacity') is None:
646
- battery['ratedCapacity'] = 2450 * battery['count'] if rated is None else rated
647
- else:
648
- residual = battery.get('residual')
649
- soc = battery.get('soc')
650
- capacity = residual / soc * 100 if residual is not None and soc is not None and soc > 0 else None
651
- if battery.get('count') is None or battery['count'] < 1:
652
- battery['count'] = count
653
- if battery.get('ratedCapacity') is None or battery['ratedCapacity'] < 100:
654
- battery['ratedCapacity'] = rated
651
+ battery['soh'] = round(capacity / battery['ratedCapacity'] * 100, 1)
652
+ residual = capacity * soc / 100
655
653
  battery['capacity'] = round(capacity, 3)
656
654
  battery['residual'] = round(residual, 3)
657
- battery['charge_rate'] = None
658
- params = battery_params[battery['residual_handling']]
659
- battery['charge_loss'] = params['charge_loss']
660
- battery['discharge_loss'] = params['discharge_loss']
661
- if battery.get('ratedCapacity') is not None and battery.get('capacity') is not None:
662
- battery['soh'] = round(battery['capacity'] * 1000 / battery['ratedCapacity'] * 100, 1) if battery['ratedCapacity'] > 0.0 else None
663
- if battery.get('temperature') is not None:
664
- battery['charge_rate'] = interpolate((battery['temperature'] - params['offset']) / params['step'], params['table'])
655
+ if battery['residual_handling'] > 0:
656
+ params = battery_params[battery['residual_handling']]
657
+ battery['charge_loss'] = params['charge_loss']
658
+ battery['discharge_loss'] = params['discharge_loss']
659
+ if battery.get('temperature') is not None:
660
+ battery['charge_rate'] = interpolate((battery['temperature'] - params['offset']) / params['step'], params['table'])
665
661
  return battery
666
662
 
667
663
  def get_batteries(info=0, rated=None, count=None):
@@ -676,6 +672,91 @@ def get_batteries(info=0, rated=None, count=None):
676
672
  batteries = [battery]
677
673
  return batteries
678
674
 
675
+ def get_battery_real():
676
+ global device_sn, device
677
+ if get_device() is None:
678
+ return None
679
+ output(f"getting battery real", 2)
680
+ params = {'sn': device_sn}
681
+ response = signed_get(path="/op/v0/device/battery/real/query", params=params)
682
+ if response.status_code != 200:
683
+ output(f"** get_battery_real() got response code {response.status_code}: {response.reason}")
684
+ return None
685
+ result = response.json().get('result')
686
+ if result is None:
687
+ output(f"** get_battery_real(), no result data, {errno_message(response)}")
688
+ return None
689
+ return result
690
+
691
+ ##################################################################################################
692
+ # battery heating settings
693
+ ##################################################################################################
694
+
695
+ battery_heating = None
696
+
697
+ def get_battery_heating():
698
+ global device_sn, device, battery_heating
699
+ if get_device() is None:
700
+ return None
701
+ output(f"getting battery heating", 2)
702
+ body = {'sn': device_sn}
703
+ response = signed_post(path="/op/v0/device/batteryHeating/get", body=body)
704
+ if response.status_code != 200:
705
+ output(f"** get_battery_heating() got response code {response.status_code}: {response.reason}")
706
+ return None
707
+ errno = response.json().get('errno')
708
+ if errno != 0:
709
+ if errno == 41200:
710
+ output(f"** get_battery_heating(): not supported")
711
+ else:
712
+ output(f"** get_battery_heating(): {errno_message(response)}")
713
+ return None
714
+ result = response.json().get('result')
715
+ battery_heating = result
716
+ return result
717
+
718
+ def set_time(body, s, time):
719
+ if time is None:
720
+ body[s + 'Enable'] = 0
721
+ body[s + 'StartHour'] = 0
722
+ body[s + 'StartMinute'] = 0
723
+ body[s + 'EndHour'] = 0
724
+ body[s + 'EndMinute'] = 0
725
+ else:
726
+ body[s + 'Enable'] = time['enable']
727
+ t = time_hours(time['start'])
728
+ body[s + 'StartHour'] = int(t)
729
+ body[s + 'StartMinute'] = int(60 * (t - int(t)) + 0.5)
730
+ t = time_hours(time['end'])
731
+ body[s + 'EndHour'] = int(t)
732
+ body[s + 'EndMinute'] = int(60 * (t - int(t)) + 0.5)
733
+ return
734
+
735
+ def set_battery_heating(enable=None, start=None, end=None, time1=None, time2=None, time3=None):
736
+ global device_sn, device
737
+ if get_device() is None:
738
+ return None
739
+ output(f"setting battery heating", 2)
740
+ body = {'sn': device_sn}
741
+ body['batteryWarmUpEnable'] = enable if enable is not None else 1
742
+ body['startTemperature'] = start if start is not None else 9
743
+ body['endTemperature'] = end if end is not None else 12
744
+ set_time(body, 'time1', time1)
745
+ set_time(body, 'time2', time2)
746
+ set_time(body, 'time3', time3)
747
+ response = signed_post(path="/op/v0/device/batteryHeating/set", body=body)
748
+ if response.status_code != 200:
749
+ output(f"** set_battery_heating() got response code {response.status_code}: {response.reason}")
750
+ return None
751
+ errno = response.json().get('errno')
752
+ if errno != 0:
753
+ if errno == 41200:
754
+ output(f"** set_battery_heating(): not supported")
755
+ else:
756
+ output(f"** set_battery_heating(): {errno_message(response)}")
757
+ return None
758
+ return 1
759
+
679
760
  ##################################################################################################
680
761
  # get charge times and save to battery_settings
681
762
  ##################################################################################################
@@ -896,6 +977,8 @@ def get_peakshaving():
896
977
  ##################################################################################################
897
978
 
898
979
  # store for named settings info
980
+ name_list = ['ExportLimit','MinSoc','MinSocOnGrid','MaxSoc','GridCode','WorkMode','ExportLimitPower',
981
+ 'EpsOutPut','MaxSetChargeCurrent','MaxSetDischargeCurrent','ECOMode','Meter1Enable','Meter2Enable','SysSwitch','GroundProtection']
899
982
  named_settings = {}
900
983
 
901
984
  def get_remote_settings(name):
@@ -911,8 +994,7 @@ def get_remote_settings(name):
911
994
  v = get_remote_settings(n)
912
995
  if v is None:
913
996
  continue
914
- for x in v.keys():
915
- values[x] = v[x]
997
+ values[n] = v
916
998
  return values
917
999
  body = {'sn': device_sn, 'key': name}
918
1000
  setting_delay()
@@ -923,7 +1005,7 @@ def get_remote_settings(name):
923
1005
  result = response.json().get('result')
924
1006
  if result is None:
925
1007
  errno = response.json().get('errno')
926
- output(f"** get_remote_settings(), no result data, {errno_message(response)}")
1008
+ output(f"** get_remote_settings(), no result data for {name}, {errno_message(response)}")
927
1009
  return None
928
1010
  named_settings[name] = result
929
1011
  value = result.get('value')
@@ -1053,6 +1135,7 @@ def set_work_mode(mode, force = 0):
1053
1135
  ##################################################################################################
1054
1136
 
1055
1137
  schedule = None
1138
+ max_periods = 8
1056
1139
 
1057
1140
  # get the current switch status
1058
1141
  def get_flag():
@@ -1069,7 +1152,7 @@ def get_flag():
1069
1152
  if result is None:
1070
1153
  return None
1071
1154
  if schedule is None:
1072
- schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': False}
1155
+ schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': None}
1073
1156
  schedule['enable'] = result.get('enable')
1074
1157
  schedule['support'] = result.get('support')
1075
1158
  if device.get('function') is not None and device['function'].get('scheduler') is not None:
@@ -1090,7 +1173,7 @@ def get_schedule():
1090
1173
  return None
1091
1174
  output(f"getting schedule", 2)
1092
1175
  body = {'deviceSN': device_sn}
1093
- response = signed_post(path="/op/v1/device/scheduler/get", body=body)
1176
+ response = signed_post(path="/op/v2/device/scheduler/get", body=body)
1094
1177
  if response.status_code != 200:
1095
1178
  output(f"** get_schedule() got response code {response.status_code}: {response.reason}")
1096
1179
  return None
@@ -1103,11 +1186,12 @@ def get_schedule():
1103
1186
  enable = True if enable == 1 else False
1104
1187
  schedule['enable'] = enable
1105
1188
  schedule['periods'] = []
1189
+ schedule['maxsoc'] = False
1106
1190
  # remove invalid work mode from periods
1107
1191
  for g in result['groups']:
1108
1192
  if g['enable'] == 1 and g['workMode'] in work_modes:
1109
1193
  schedule['periods'].append(g)
1110
- if g.get('maxSoc') is not None:
1194
+ if g.get('extraParam') is not None and g['extraParam'].get('maxSoc') is not None:
1111
1195
  schedule['maxsoc'] = True
1112
1196
  return schedule
1113
1197
 
@@ -1126,6 +1210,8 @@ def build_strategy_from_schedule():
1126
1210
  period['max_soc'] = p.get('maxSoc')
1127
1211
  period['fdsoc'] = p.get('fdsoc')
1128
1212
  period['fdpwr'] = p.get('fdpwr')
1213
+ period['import_limit'] = p.get('importLimit')
1214
+ period['export_limit'] = p.get('exportLimit')
1129
1215
  strategy.append(period)
1130
1216
  return strategy
1131
1217
 
@@ -1134,9 +1220,9 @@ def build_strategy_from_schedule():
1134
1220
  ##################################################################################################
1135
1221
 
1136
1222
  # create time segment structure. Note: end time is exclusive.
1137
- def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdsoc=None, fdpwr=None, price=None, segment=None, enable=1, quiet=1):
1223
+ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdsoc=None, fdpwr=None, import_limit=None, export_limit=None, price=None, segment=None, enable=1, quiet=1):
1138
1224
  global schedule, device
1139
- if schedule is None:
1225
+ if schedule is None or schedule.get('maxsoc') is None:
1140
1226
  get_schedule()
1141
1227
  if segment is not None and type(segment) is dict:
1142
1228
  start = segment.get('start')
@@ -1146,6 +1232,8 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1146
1232
  max_soc = segment.get('max_soc')
1147
1233
  fdsoc = segment.get('fdSoc')
1148
1234
  fdpwr = segment.get('fdPwr')
1235
+ import_limit = segment.get('import_limit')
1236
+ export_limit = segment.get('export_limit')
1149
1237
  price = segment.get('price')
1150
1238
  start = time_hours(start)
1151
1239
  # adjust exclusive time to inclusive
@@ -1166,8 +1254,8 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1166
1254
  power = (device['power'] * 1000) if device.get('power') is not None else None
1167
1255
  fdpwr = power if fdpwr is None and device.get('power') is not None and ('ForceCharge' in mode or 'ForceDischarge' in mode) else fdpwr
1168
1256
  fdpwr = 0 if fdpwr is None else fdpwr
1169
- if min_soc < 10 or min_soc > 100:
1170
- output(f"set_period(): ** min_soc must be between 10 and 100")
1257
+ if min_soc < 0 or min_soc > 100:
1258
+ output(f"set_period(): ** min_soc must be between 0 and 100")
1171
1259
  return None
1172
1260
  if max_soc is not None and (max_soc < 10 or max_soc > 100):
1173
1261
  output(f"set_period(): ** max_soc must be between 10 and 100")
@@ -1187,14 +1275,13 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1187
1275
  start_hour, start_minute = split_hours(start)
1188
1276
  end_hour, end_minute = split_hours(end)
1189
1277
  period = {'enable': enable, 'startHour': start_hour, 'startMinute': start_minute, 'endHour': end_hour, 'endMinute': end_minute, 'workMode': mode,
1190
- 'minSocOnGrid': int(min_soc), 'fdSoc': int(fdsoc), 'fdPwr': int(fdpwr)}
1191
- if max_soc is not None:
1192
- period['maxSoc'] = int(max_soc)
1278
+ 'extraParam': {'minSocOnGrid': int(min_soc), 'fdSoc': int(fdsoc), 'fdPwr': int(fdpwr), 'maxSoc': max_soc, 'importLimit': import_limit,
1279
+ 'exportLimit': export_limit }}
1193
1280
  return period
1194
1281
 
1195
1282
  # set a schedule from a period or list of time segment periods
1196
1283
  def set_schedule(periods=None, enable=True):
1197
- global device_sn, debug_setting, schedule
1284
+ global device_sn, debug_setting, schedule, max_periods
1198
1285
  if get_flag() is None:
1199
1286
  return None
1200
1287
  if schedule.get('support') == False:
@@ -1202,6 +1289,7 @@ def set_schedule(periods=None, enable=True):
1202
1289
  return None
1203
1290
  output(f"set_schedule(): enable = {enable}, periods = {periods}", 2)
1204
1291
  if debug_setting > 2:
1292
+ print(f"** schedule not set (debug_setting={debug_setting})")
1205
1293
  return None
1206
1294
  if type(enable) is int:
1207
1295
  enable = True if enable == 1 else False
@@ -1212,11 +1300,11 @@ def set_schedule(periods=None, enable=True):
1212
1300
  if periods is not None:
1213
1301
  if type(periods) is not list:
1214
1302
  periods = [periods]
1215
- if len(periods) > 8:
1216
- output(f"** set_schedule(): maximum of 8 periods allowed, {len(periods)} provided")
1217
- body = {'deviceSN': device_sn, 'groups': periods[-8:]}
1303
+ if len(periods) > max_periods:
1304
+ output(f"** set_schedule(): maximum of {max_periods} periods allowed, {len(periods)} provided")
1305
+ body = {'deviceSN': device_sn, 'groups': periods[-max_periods:]}
1218
1306
  setting_delay()
1219
- response = signed_post(path="/op/v1/device/scheduler/enable", body=body)
1307
+ response = signed_post(path="/op/v2/device/scheduler/enable", body=body)
1220
1308
  if response.status_code != 200:
1221
1309
  output(f"** set_schedule() periods response code {response.status_code}: {response.reason}")
1222
1310
  return None
@@ -3437,7 +3525,8 @@ def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
3437
3525
  output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
3438
3526
  output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
3439
3527
  output(f"BMS Temperature: {bms_temperature:.1f}°C")
3440
- output(f"BMS Charge Rate: {bat.get('charge_rate'):.1f}A (estimated)")
3528
+ if bat.get('charge_rate') is not None:
3529
+ output(f"BMS Charge Rate: {bat['charge_rate']:.1f}A (estimated)")
3441
3530
  output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
3442
3531
  output(f"\nInfo by battery:")
3443
3532
  for i in range(0, nbat):
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: foxesscloud
3
+ Version: 2.9.3
4
+ Summary: library for accessing Fox ESS cloud data using Open API
5
+ Author-email: Tony Matthews <tony@quasair.co.uk>
6
+ Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
7
+ Project-URL: Bug Tracker, https://github.com/TonyM1958/FoxESS-Cloud/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+
1
15
  # FoxESS-Cloud
2
16
 
3
17
  <a href="https://www.buymeacoffee.com/tonym1958" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174" align="right"></a>
@@ -47,6 +61,7 @@ You don't have to configure all of the settings. Your Fox ESS Cloud api key is t
47
61
  For example, replace _my.fox_api_key_ with the API key. Add you inverter serial number if you have more than 1 inverter linked to your account. Be sure to keep the double quotes around the values you enter or you will get a syntax error.
48
62
 
49
63
  Residual handling configures how battery residual energy reported by Fox is handled:
64
+ + 0: Use rated capacity, SoH and SoC to calculate residual energy (default)
50
65
  + 1: Fox returns the current battery residual energy and battery capacity is calculated using soc
51
66
  + 2: Fox returns the current battery capacity and battery residual is calculated using soc
52
67
  + 3: Fox returns the residual capacity per battery (Mira)
@@ -94,8 +109,8 @@ Once an inverter is selected, you can make other calls to get information:
94
109
 
95
110
  ```
96
111
  f.get_generation()
97
- f.get_battery(info, rated, count)
98
- f.get_batteries(info, rated, count)
112
+ f.get_battery()
113
+ f.get_batteries(info)
99
114
  f.get_settings()
100
115
  f.get_charge()
101
116
  f.get_min()
@@ -103,17 +118,15 @@ f.get_peakshaving()
103
118
  f.get_flag()
104
119
  f.get_schedule()
105
120
  f.get_named_settings(name)
121
+ f.get_battery_heating()
106
122
 
107
123
  ```
108
124
  Each of these calls will return a dictionary or list containing the relevant information.
109
125
 
110
126
  get_generation() will return the latest generation information for the device. The results are also stored in f.device as 'generationToday', 'generationMonth' and 'generationTotal'.
111
127
 
112
- 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.
113
- get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery. Parameters:
114
- + 'info': get battery serial number info, if available. Default 0 (not available via Open API)
115
- + 'rated': optional rated capacity for the battery in Wh to work out SoH. If not provided, it will try to work this out.
116
- + 'count': optional battery count. If not provided, it will try to work this out.
128
+ get_battery() / get_batteries() returns the current battery status, including 'soc', 'volt', 'current', 'power', 'temperature', 'residual' and 'throughput'. The result also updates f.battery / f.batteries.
129
+ get_batteries() returns multiple batteries (if available) as a list. get_battery() returns the first battery.
117
130
 
118
131
  Additional battery attributes provided include:
119
132
  + 'capacity': the estimated battery capacity, derrived from 'residual' and 'soc'
@@ -132,8 +145,9 @@ get_schedule() returns the current work mode / soc schedule settings. The result
132
145
 
133
146
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
134
147
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
135
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
148
+ + named_settings currently available are stored in f.name_list. The settings supported depends on the inverter model and firmware version. An error will be returned if an unsupported variable is used.
136
149
 
150
+ get_battery_heating returns the current battery heating parameters and store these in f.battery_heating
137
151
 
138
152
  ## Inverter Settings
139
153
  You can change inverter settings using:
@@ -144,6 +158,7 @@ f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
144
158
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
145
159
  f.set_schedule(periods, enable)
146
160
  f.set_named_settings(name, value, force)
161
+ f.set_battery_heating(enable, start, end, time1, time2, time3)
147
162
  ```
148
163
 
149
164
  set_min() applies new SoC settings to the inverter. The parameters update battery_settings:
@@ -166,12 +181,16 @@ set_period() returns a period structure that can be used to build a list for set
166
181
  + max_soc: optional, default is 100
167
182
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
168
183
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
184
+ + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
185
+ + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
169
186
  + price: optional, default None. Used to display plunge pricing for time period.
170
187
  + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
171
188
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
172
189
 
173
190
  Before calling set_period(), do at least one call to get_schedule(). This will inspect the schedule result to check if max_soc is supported and set the flag f.schedule['maxsoc'] to enable or disable this field as appropriate.
174
191
 
192
+ The variable f.max_periods is used to check the number of time periods allowed. The default is 8.
193
+
175
194
  set_schedule() configures a list of scheduled work mode / soc changes with enable=1. If called with enable=0, any existing schedules are disabled. To enable a schedule, you must provide a list of time segments
176
195
  + periods: a time segment or list of time segments created using f.set_period().
177
196
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
@@ -180,8 +199,12 @@ set_named_settings() sets the 'name' setting to 'value'.
180
199
  + 'name' may also be a list of (name, value) pairs.
181
200
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
182
201
  + a return value of 1 is success. 0 means setting failed. None is another error e.g. device not found, invalid name or value.
183
- + named_settings currently supported include: ExportLimit, MinSoc, MinSocOnGrid, MaxSoc, GridCode, WorkMode
202
+ + named_settings currently available are stored in f.name_list. The settings supported depend on the inverter model and firmware version. An error will be returned if an unsupported varaible is used.
184
203
 
204
+ set_battery_heating() set the heating parameters as follows:
205
+ + enable: optional, 0 or 1, default is 1
206
+ + start, end: optional start and end temperatures. The defaults are start at 9C and end at 12C.
207
+ + time1, time2, time3: optional times when the battery can heat from the grid time. The structure is {'enable': 1, 'start': '00:30', 'end': '05:30'}. The time slot is disabled by default.
185
208
 
186
209
  ## Real Time Data
187
210
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -807,6 +830,19 @@ This setting can be:
807
830
 
808
831
  # Version Info
809
832
 
833
+ 2.9.3 - 2026/01/18<br>
834
+ Update get_device() to use v1 API call.
835
+ Update get_battery() to work out battery count and rated capacity from device battery list.
836
+ Update default residual_handling to option 0 (residual is calculated from rated capacity, SoH and SoC).
837
+ Added get_battery_real() for testing (Fox does not currently populate most of the data).
838
+ Added 'throughput' to battery variables returned.
839
+ Add f.name_list to hold the list of setting variables and update the list.
840
+ Added get_battery_heating() and set_battery_heating().
841
+
842
+ 2.9.2 - 2025/11/30<br>
843
+ Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
844
+ Added f.max_periods to allow for more time periods in schedules (default is 8).
845
+
810
846
  2.9.1 - 2025/10/25<br>
811
847
  Change default contingency for charge_needed() to [25,15,10,20] and special contingency to 35.
812
848
  Update info shown around contingency.
File without changes
File without changes