foxesscloud 2.9.2__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.2
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:
@@ -198,8 +199,12 @@ set_named_settings() sets the 'name' setting to 'value'.
198
199
  + 'name' may also be a list of (name, value) pairs.
199
200
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
200
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.
201
- + 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.
202
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.
203
208
 
204
209
  ## Real Time Data
205
210
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -825,6 +830,15 @@ This setting can be:
825
830
 
826
831
  # Version Info
827
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
+
828
842
  2.9.2 - 2025/11/30<br>
829
843
  Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
830
844
  Added f.max_periods to allow for more time periods in schedules (default is 8).
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.9.2
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:
@@ -198,8 +185,12 @@ set_named_settings() sets the 'name' setting to 'value'.
198
185
  + 'name' may also be a list of (name, value) pairs.
199
186
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
200
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.
201
- + 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.
202
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.
203
194
 
204
195
  ## Real Time Data
205
196
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -825,6 +816,15 @@ This setting can be:
825
816
 
826
817
  # Version Info
827
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
828
  2.9.2 - 2025/11/30<br>
829
829
  Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
830
830
  Added f.max_periods to allow for more time periods in schedules (default is 8).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.9.2"
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: 30 November 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.2"
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')
@@ -1070,7 +1152,7 @@ def get_flag():
1070
1152
  if result is None:
1071
1153
  return None
1072
1154
  if schedule is None:
1073
- schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': False}
1155
+ schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': None}
1074
1156
  schedule['enable'] = result.get('enable')
1075
1157
  schedule['support'] = result.get('support')
1076
1158
  if device.get('function') is not None and device['function'].get('scheduler') is not None:
@@ -1104,6 +1186,7 @@ def get_schedule():
1104
1186
  enable = True if enable == 1 else False
1105
1187
  schedule['enable'] = enable
1106
1188
  schedule['periods'] = []
1189
+ schedule['maxsoc'] = False
1107
1190
  # remove invalid work mode from periods
1108
1191
  for g in result['groups']:
1109
1192
  if g['enable'] == 1 and g['workMode'] in work_modes:
@@ -1139,7 +1222,7 @@ def build_strategy_from_schedule():
1139
1222
  # create time segment structure. Note: end time is exclusive.
1140
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):
1141
1224
  global schedule, device
1142
- if schedule is None:
1225
+ if schedule is None or schedule.get('maxsoc') is None:
1143
1226
  get_schedule()
1144
1227
  if segment is not None and type(segment) is dict:
1145
1228
  start = segment.get('start')
@@ -3442,7 +3525,8 @@ def battery_info(log=0, plot=1, rated=None, count=None, info=1, bat=None):
3442
3525
  output(f"Cell Volts: {avg(cell_volts):.3f}V average, {max(cell_volts):.3f}V maximum, {min(cell_volts):.3f}V minimum")
3443
3526
  output(f"Cell Imbalance: {imbalance(cell_volts):.2f}%:")
3444
3527
  output(f"BMS Temperature: {bms_temperature:.1f}°C")
3445
- 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)")
3446
3530
  output(f"Battery Temperature: {avg(cell_temps):.1f}°C average, {max(cell_temps):.1f}°C maximum, {min(cell_temps):.1f}°C minimum")
3447
3531
  output(f"\nInfo by battery:")
3448
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:
@@ -184,8 +199,12 @@ set_named_settings() sets the 'name' setting to 'value'.
184
199
  + 'name' may also be a list of (name, value) pairs.
185
200
  + force: setting to 1 will disable Mode Scheduler, if enabled. Default is 0.
186
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.
187
- + 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.
188
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.
189
208
 
190
209
  ## Real Time Data
191
210
  Real time data reports the latest values for inverter variables, collected every 5 minutes:
@@ -811,6 +830,15 @@ This setting can be:
811
830
 
812
831
  # Version Info
813
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
+
814
842
  2.9.2 - 2025/11/30<br>
815
843
  Update get_schedule(), set_period() and set_schedule() to use v2 interface and add setting import_limit and export_limit.
816
844
  Added f.max_periods to allow for more time periods in schedules (default is 8).
File without changes
File without changes