foxesscloud 2.9.6__tar.gz → 2.9.8__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.6
3
+ Version: 2.9.8
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
@@ -140,9 +140,9 @@ get_peakshaving() will return the current peak shaving settings for inverters th
140
140
 
141
141
  get_flag() returns the current scheduler enable / support / maxsoc flags. By default support for Max Soc is set to False.
142
142
  + f.max_periods / schedule['maxGroupCount'] gives the maximum number of time periods that are supported.
143
+ + f.schedule['properties'] details the parameters supported in Mode Scheduler for the current inverter.
143
144
 
144
145
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
145
- + 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().
146
146
 
147
147
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
148
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -157,7 +157,7 @@ You can change inverter settings using:
157
157
  f.set_min(minSocOnGrid, minSoc)
158
158
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
159
159
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
160
- f.set_schedule(periods, enable)
160
+ f.set_schedule(periods, enable, is_default)
161
161
  f.set_named_settings(name, value, force)
162
162
  f.set_heating(enable, start, end, time1, time2, time3)
163
163
  ```
@@ -182,10 +182,10 @@ set_period() returns a period structure that can be used to build a list for set
182
182
  + max_soc: optional, default is 100
183
183
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
184
184
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
185
- + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
186
- + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
185
+ + import_limit: optional, default is None (not set) but set to 0 if the mode is force discharge
186
+ + export_limit: optional, default is None (not set).
187
+ + pv_limit: optional, default is 1.5 x inverter rating.
187
188
  + price: optional, default None. Used to display plunge pricing for time period.
188
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
189
189
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
190
190
 
191
191
  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.
@@ -195,6 +195,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
195
195
  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
196
196
  + periods: a time segment or list of time segments created using f.set_period().
197
197
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
198
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
198
199
 
199
200
  set_named_settings() sets the 'name' setting to 'value'.
200
201
  + 'name' may also be a list of (name, value) pairs.
@@ -426,7 +427,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
426
427
  min_kwh: 0.5 # minimum charge to add in kwh
427
428
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
428
429
  solar_adjust: 100 # % adjustment to make to Solar forecast
429
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
430
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
430
431
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
431
432
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
432
433
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -832,13 +833,29 @@ This setting can be:
832
833
 
833
834
  # Version Info
834
835
 
836
+ 2.9.8 - 2026/03/22<br>
837
+ ** potential breaking changes **
838
+ Update to use Mode Scheduler API v3.
839
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
840
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
841
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
842
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
843
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
844
+ set_period() now supports 'reactive_power' as a parameter.
845
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
846
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
847
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
848
+
849
+ 2.9.7 - 2026/03/18<br>
850
+ Add pv_limit to set_period().
851
+ Default import_linit to 0 for force discharge in set_period()
852
+
835
853
  2.9.6 - 2026/03/10<br>
836
854
  Fix problem setting fdsoc and fdpwr from segment for set_period().
837
855
  Correctly identify H1-G2 model inverter.
838
856
  Add maxChargeCurrent and maxDischargeCurrent to f.battery.
839
857
  Update battery charge derating with temperature so charge current is not 0.
840
858
 
841
-
842
859
  2.9.5 - 2026/02/27<br>
843
860
  Add step parameter to date_list().
844
861
  Get maxGroupCount from v3 scheduler and update f.max_periods with maximum number of time periods supported.
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.9.6
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>
@@ -140,9 +126,9 @@ get_peakshaving() will return the current peak shaving settings for inverters th
140
126
 
141
127
  get_flag() returns the current scheduler enable / support / maxsoc flags. By default support for Max Soc is set to False.
142
128
  + f.max_periods / schedule['maxGroupCount'] gives the maximum number of time periods that are supported.
129
+ + f.schedule['properties'] details the parameters supported in Mode Scheduler for the current inverter.
143
130
 
144
131
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
145
- + 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().
146
132
 
147
133
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
134
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -157,7 +143,7 @@ You can change inverter settings using:
157
143
  f.set_min(minSocOnGrid, minSoc)
158
144
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
159
145
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
160
- f.set_schedule(periods, enable)
146
+ f.set_schedule(periods, enable, is_default)
161
147
  f.set_named_settings(name, value, force)
162
148
  f.set_heating(enable, start, end, time1, time2, time3)
163
149
  ```
@@ -182,10 +168,10 @@ set_period() returns a period structure that can be used to build a list for set
182
168
  + max_soc: optional, default is 100
183
169
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
184
170
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
185
- + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
186
- + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
171
+ + import_limit: optional, default is None (not set) but set to 0 if the mode is force discharge
172
+ + export_limit: optional, default is None (not set).
173
+ + pv_limit: optional, default is 1.5 x inverter rating.
187
174
  + price: optional, default None. Used to display plunge pricing for time period.
188
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
189
175
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
190
176
 
191
177
  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.
@@ -195,6 +181,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
195
181
  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
196
182
  + periods: a time segment or list of time segments created using f.set_period().
197
183
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
184
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
198
185
 
199
186
  set_named_settings() sets the 'name' setting to 'value'.
200
187
  + 'name' may also be a list of (name, value) pairs.
@@ -426,7 +413,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
426
413
  min_kwh: 0.5 # minimum charge to add in kwh
427
414
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
428
415
  solar_adjust: 100 # % adjustment to make to Solar forecast
429
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
416
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
430
417
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
431
418
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
432
419
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -832,13 +819,29 @@ This setting can be:
832
819
 
833
820
  # Version Info
834
821
 
822
+ 2.9.8 - 2026/03/22<br>
823
+ ** potential breaking changes **
824
+ Update to use Mode Scheduler API v3.
825
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
826
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
827
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
828
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
829
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
830
+ set_period() now supports 'reactive_power' as a parameter.
831
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
832
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
833
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
834
+
835
+ 2.9.7 - 2026/03/18<br>
836
+ Add pv_limit to set_period().
837
+ Default import_linit to 0 for force discharge in set_period()
838
+
835
839
  2.9.6 - 2026/03/10<br>
836
840
  Fix problem setting fdsoc and fdpwr from segment for set_period().
837
841
  Correctly identify H1-G2 model inverter.
838
842
  Add maxChargeCurrent and maxDischargeCurrent to f.battery.
839
843
  Update battery charge derating with temperature so charge current is not 0.
840
844
 
841
-
842
845
  2.9.5 - 2026/02/27<br>
843
846
  Add step parameter to date_list().
844
847
  Get maxGroupCount from v3 scheduler and update f.max_periods with maximum number of time periods supported.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.9.6"
7
+ version = "2.9.8"
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: 05 March 2026
4
+ Updated: 18 March 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.11.2"
13
+ version = "1.11.3"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -1478,7 +1478,7 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1478
1478
  if quiet == 0:
1479
1479
  s = f" {hours_time(start)}-{hours_time(end)} {mode}, minsoc {min_soc}%"
1480
1480
  s += f", maxsoc {max_soc}%" if max_soc is not None and 'ForceCharge' in mode else ""
1481
- s += f", fdPwr {fdpwr}W, fdSoC {fdsoc}%" if ('ForceCharge' in mod or 'ForceDischarge' in mode) else ""
1481
+ s += f", fdPwr {fdpwr}W, fdSoC {fdsoc}%" if ('ForceCharge' in mode or 'ForceDischarge' in mode) else ""
1482
1482
  s += f", {price:.2f}p/kWh" if price is not None else ""
1483
1483
  output(s, 1)
1484
1484
  start_h, start_m = split_hours(start)
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 10 March 2026
4
+ Updated: 22 March 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 2024
11
11
  ##################################################################################################
12
12
 
13
- version = "2.9.6"
13
+ version = "2.9.8"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -1154,7 +1154,7 @@ max_periods = 8
1154
1154
 
1155
1155
  # get the current switch status
1156
1156
  def get_flag():
1157
- global device_sn, schedule, debug_setting, max_periods
1157
+ global device_sn, schedule, debug_setting, max_periods, work_modes, settable_modes
1158
1158
  if get_device() is None:
1159
1159
  return None
1160
1160
  output(f"getting flag", 2)
@@ -1167,22 +1167,29 @@ def get_flag():
1167
1167
  if result is None:
1168
1168
  return None
1169
1169
  if schedule is None:
1170
- schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': None}
1170
+ schedule = {'enable': None, 'support': None, 'periods': [], 'maxsoc': None}
1171
1171
  schedule['enable'] = result.get('enable')
1172
1172
  schedule['support'] = result.get('support')
1173
1173
  if device.get('function') is not None and device['function'].get('scheduler') is not None:
1174
1174
  device['function']['scheduler'] = schedule['support']
1175
1175
  if schedule.get('maxGroupCount') is None:
1176
- output(f"getting maxGroupCount", 2)
1176
+ output(f"getting properties", 2)
1177
1177
  body = {'deviceSN': device_sn}
1178
1178
  response = signed_post(path="/op/v3/device/scheduler/get", body=body)
1179
1179
  if response.status_code != 200:
1180
- output(f"** get_flag() got response code getting maxGroupCount {response.status_code}: {response.reason}")
1180
+ output(f"** get_flag() got response code getting properties {response.status_code}: {response.reason}")
1181
1181
  return None
1182
1182
  result = response.json().get('result')
1183
1183
  if result is not None:
1184
1184
  schedule['maxGroupCount'] = result.get('maxGroupCount')
1185
1185
  max_periods = schedule['maxGroupCount']
1186
+ schedule['properties'] = result.get('properties')
1187
+ if schedule['properties'] is not None:
1188
+ schedule['maxsoc'] = schedule['properties'].get('maxsoc') is not None
1189
+ modes = schedule['properties'].get('workmode')
1190
+ if modes is not None:
1191
+ work_modes = sorted(modes['enumList'])
1192
+ settable_modes = [w for w in work_modes if 'Force' not in w]
1186
1193
  return schedule
1187
1194
 
1188
1195
  ##################################################################################################
@@ -1199,7 +1206,7 @@ def get_schedule():
1199
1206
  return None
1200
1207
  output(f"getting schedule", 2)
1201
1208
  body = {'deviceSN': device_sn}
1202
- response = signed_post(path="/op/v2/device/scheduler/get", body=body)
1209
+ response = signed_post(path="/op/v3/device/scheduler/get", body=body)
1203
1210
  if response.status_code != 200:
1204
1211
  output(f"** get_schedule() got response code {response.status_code}: {response.reason}")
1205
1212
  return None
@@ -1212,13 +1219,10 @@ def get_schedule():
1212
1219
  enable = True if enable == 1 else False
1213
1220
  schedule['enable'] = enable
1214
1221
  schedule['periods'] = []
1215
- schedule['maxsoc'] = False
1216
1222
  # remove invalid work mode from periods
1217
1223
  for g in result['groups']:
1218
- if g['enable'] == 1 and g['workMode'] in work_modes:
1224
+ if g['workMode'] in work_modes:
1219
1225
  schedule['periods'].append(g)
1220
- if g.get('extraParam') is not None and g['extraParam'].get('maxSoc') is not None:
1221
- schedule['maxsoc'] = True
1222
1226
  return schedule
1223
1227
 
1224
1228
  # build strategy using current schedule
@@ -1246,9 +1250,10 @@ def build_strategy_from_schedule():
1246
1250
  ##################################################################################################
1247
1251
 
1248
1252
  # create time segment structure. Note: end time is exclusive.
1249
- 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):
1253
+ 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, pv_limit=None, reactive_power=None
1254
+ , price=None, segment=None, enable=1, quiet=1):
1250
1255
  global schedule, device
1251
- if schedule is None or schedule.get('maxsoc') is None:
1256
+ if schedule is None:
1252
1257
  get_schedule()
1253
1258
  if segment is not None and type(segment) is dict:
1254
1259
  start = segment.get('start')
@@ -1260,7 +1265,11 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1260
1265
  fdpwr = segment.get('fdpwr')
1261
1266
  import_limit = segment.get('import_limit')
1262
1267
  export_limit = segment.get('export_limit')
1268
+ pv_limit = segment.get('pv_limit')
1269
+ reactive_power = segment.get('reactive_power')
1263
1270
  price = segment.get('price')
1271
+ if enable == 0:
1272
+ return None
1264
1273
  start = time_hours(start)
1265
1274
  # adjust exclusive time to inclusive
1266
1275
  end = time_hours(end)
@@ -1272,41 +1281,48 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1272
1281
  if mode not in work_modes:
1273
1282
  output(f"** mode must be one of {work_modes}")
1274
1283
  return None
1284
+ properties = schedule.get('properties')
1275
1285
  min_soc = 10 if min_soc is None else min_soc
1276
- max_soc = None if schedule is None or schedule.get('maxsoc') is None or schedule['maxsoc'] == False else 100 if max_soc is None else max_soc
1286
+ max_soc = None if properties.get('maxsoc') is None or 'ForceCharge' not in mode else 100 if max_soc is None else max_soc
1277
1287
  if 'ForceCharge' in mode and fdsoc is None:
1278
1288
  fdsoc = max_soc if max_soc is not None else 100
1279
- fdsoc = min_soc if fdsoc is None else fdsoc
1289
+ fdsoc = None if properties.get('fdsoc') is None or 'Force' not in mode else min_soc if fdsoc is None else fdsoc
1280
1290
  power = (device['power'] * 1000) if device.get('power') is not None else None
1281
- fdpwr = power if fdpwr is None and device.get('power') is not None and ('ForceCharge' in mode or 'ForceDischarge' in mode) else fdpwr
1282
- fdpwr = 0 if fdpwr is None else fdpwr
1283
- if min_soc < 0 or min_soc > 100:
1284
- output(f"set_period(): ** min_soc must be between 0 and 100")
1285
- return None
1286
- if max_soc is not None and (max_soc < 10 or max_soc > 100):
1287
- output(f"set_period(): ** max_soc must be between 10 and 100")
1288
- return None
1289
- if fdpwr < 0 or fdpwr > 30000:
1290
- output(f"set_period(): ** fdpwr must be between 0 and 30000")
1291
- return None
1292
- if fdsoc < min_soc or fdsoc > 100:
1293
- output(f"set_period(): ** fdsoc must between {min_soc} and 100")
1294
- return None
1291
+ fdpwr = None if properties.get('fdpwr') is None else power if fdpwr is None and device.get('power') is not None and ('Force' in mode) else fdpwr
1292
+ pv_limit = None if properties.get('pvlimit') is None else int(1.5 * power) if pv_limit is None and device.get('power') is not None and ('Force' in mode) else pv_limit
1293
+ import_limit = None if properties.get('importlimit') is None else 0 if import_limit is None and 'ForceDischarge' in mode else import_limit
1294
+ export_limit = None if properties.get('exportlimit') is None else export_limit
1295
+ reactive_power = None if properties.get('reactivepower') is None else reactive_power
1295
1296
  if quiet == 0:
1296
1297
  s = f" {hours_time(start)}-{hours_time(end)} {mode}, minsoc {min_soc}%"
1297
1298
  s += f", maxsoc {max_soc}%" if max_soc is not None and 'ForceCharge' in mode else ""
1298
- s += f", fdPwr {fdpwr}W, fdSoC {fdsoc}%" if ('ForceCharge' in mode or 'ForceDischarge' in mode) else ""
1299
+ s += f", fdPwr {int(fdpwr)}W, fdSoC {fdsoc}%" if ('ForceCharge' in mode or 'ForceDischarge' in mode) else ""
1300
+ s += f", exportLimit {int(export_limit)}W" if export_limit is not None else ""
1301
+ s += f", importLimit {int(import_limit)}W" if import_limit is not None else ""
1302
+ s += f", pvLimit {int(pv_limit)}W" if pv_limit is not None else ""
1299
1303
  s += f", {price:.2f}p/kWh" if price is not None else ""
1300
1304
  output(s, 1)
1301
1305
  start_hour, start_minute = split_hours(start)
1302
1306
  end_hour, end_minute = split_hours(end)
1303
- period = {'enable': enable, 'startHour': start_hour, 'startMinute': start_minute, 'endHour': end_hour, 'endMinute': end_minute, 'workMode': mode,
1304
- 'extraParam': {'minSocOnGrid': int(min_soc), 'fdSoc': int(fdsoc), 'fdPwr': int(fdpwr), 'maxSoc': max_soc, 'importLimit': import_limit,
1305
- 'exportLimit': export_limit }}
1307
+ period = {'startHour': start_hour, 'startMinute': start_minute, 'endHour': end_hour, 'endMinute': end_minute, 'workMode': mode, 'extraParam': {'minSocOnGrid': min_soc}}
1308
+ if max_soc is not None:
1309
+ period['extraParam']['maxSoc'] = max_soc
1310
+ if fdsoc is not None:
1311
+ period['extraParam']['fdSoc'] = int(fdsoc)
1312
+ if fdpwr is not None:
1313
+ period['extraParam']['fdPwr'] = int(fdpwr)
1314
+ if import_limit is not None:
1315
+ period['extraParam']['importLimit'] = import_limit
1316
+ if export_limit is not None:
1317
+ period['extraParam']['exportLimit'] = export_limit
1318
+ if pv_limit is not None:
1319
+ period['extraParam']['pvLimit'] = pv_limit
1320
+ if reactive_power is not None:
1321
+ period['extraParam']['reactivePower'] = reactive_power
1306
1322
  return period
1307
1323
 
1308
1324
  # set a schedule from a period or list of time segment periods
1309
- def set_schedule(periods=None, enable=True):
1325
+ def set_schedule(periods=None, enable=True, is_default=False):
1310
1326
  global device_sn, debug_setting, schedule, max_periods
1311
1327
  if get_flag() is None:
1312
1328
  return None
@@ -1319,18 +1335,18 @@ def set_schedule(periods=None, enable=True):
1319
1335
  return None
1320
1336
  if type(enable) is int:
1321
1337
  enable = True if enable == 1 else False
1322
- if enable == False:
1323
- output(f"\nDisabling schedule", 1)
1324
- else:
1338
+ if enable:
1325
1339
  output(f"\nEnabling schedule", 1)
1340
+ else:
1341
+ output(f"\nDisabling schedule", 1)
1326
1342
  if periods is not None:
1327
1343
  if type(periods) is not list:
1328
1344
  periods = [periods]
1329
1345
  if len(periods) > max_periods:
1330
1346
  output(f"** set_schedule(): maximum of {max_periods} periods allowed, {len(periods)} provided")
1331
- body = {'deviceSN': device_sn, 'groups': periods[-max_periods:]}
1347
+ body = {'deviceSN': device_sn, 'isDefault': is_default, 'groups': periods[-max_periods:]}
1332
1348
  setting_delay()
1333
- response = signed_post(path="/op/v2/device/scheduler/enable", body=body)
1349
+ response = signed_post(path="/op/v3/device/scheduler/enable", body=body)
1334
1350
  if response.status_code != 200:
1335
1351
  output(f"** set_schedule() periods response code {response.status_code}: {response.reason}")
1336
1352
  return None
@@ -2728,10 +2744,10 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2728
2744
  elif 'ForceCharge' in period['mode']:
2729
2745
  s['max_soc'] = period.get('max_soc')
2730
2746
  elif period['mode'] == 'SelfUse' and period['hold'] == 1:
2731
- s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2732
- s['end'] = (start + 1 / steps_per_hour) % 24
2733
- for p in times:
2734
- p['min_soc'] = s['min_soc']
2747
+ s['mode'] = 'ForceDischarge'
2748
+ s['fdpwr'] = 0
2749
+ s['fdsoc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2750
+ s['min_soc'] = 10
2735
2751
  if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
2736
2752
  strategy.append(s)
2737
2753
  start = h
@@ -2784,7 +2800,7 @@ charge_config = {
2784
2800
  'use_today': 21.0, # hour when todays consumption and generation can be used
2785
2801
  'min_hours': 0.5, # minimum charge time in decimal hours
2786
2802
  'min_kwh': 0.5, # minimum to add in kwh
2787
- 'forecast_selection': 1, # 0 = use available forecast / generation, 1 only update settings with forecast
2803
+ 'forecast_selection': 0, # 0 = use available forecast / generation, 1 only update settings with forecast
2788
2804
  'annual_consumption': None, # optional annual consumption in kWh
2789
2805
  'timed_mode': 0, # 0 = None, 1 = timed mode, 2 = strategy mode
2790
2806
  'special_contingency': 35, # contingency for special days when consumption might be higher
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: foxesscloud
3
+ Version: 2.9.8
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>
@@ -126,9 +140,9 @@ get_peakshaving() will return the current peak shaving settings for inverters th
126
140
 
127
141
  get_flag() returns the current scheduler enable / support / maxsoc flags. By default support for Max Soc is set to False.
128
142
  + f.max_periods / schedule['maxGroupCount'] gives the maximum number of time periods that are supported.
143
+ + f.schedule['properties'] details the parameters supported in Mode Scheduler for the current inverter.
129
144
 
130
145
  get_schedule() returns the current work mode / soc schedule settings. The result is stored in f.schedule.
131
- + 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().
132
146
 
133
147
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
134
148
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -143,7 +157,7 @@ You can change inverter settings using:
143
157
  f.set_min(minSocOnGrid, minSoc)
144
158
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
145
159
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
146
- f.set_schedule(periods, enable)
160
+ f.set_schedule(periods, enable, is_default)
147
161
  f.set_named_settings(name, value, force)
148
162
  f.set_heating(enable, start, end, time1, time2, time3)
149
163
  ```
@@ -168,10 +182,10 @@ set_period() returns a period structure that can be used to build a list for set
168
182
  + max_soc: optional, default is 100
169
183
  + fdsoc: optional, default is 10. Used when setting a period with ForceDischarge mode
170
184
  + fdpwr: optional, default is 0. Used when setting a period with ForceDischarge mode
171
- + import_limit: optional, default is None (not set). Used when setting a period with ForceCharge mode
172
- + export_limit: optional, default is None (not set). Used when setting a period with ForceDischarge mode
185
+ + import_limit: optional, default is None (not set) but set to 0 if the mode is force discharge
186
+ + export_limit: optional, default is None (not set).
187
+ + pv_limit: optional, default is 1.5 x inverter rating.
173
188
  + price: optional, default None. Used to display plunge pricing for time period.
174
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
175
189
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
176
190
 
177
191
  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.
@@ -181,6 +195,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
181
195
  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
182
196
  + periods: a time segment or list of time segments created using f.set_period().
183
197
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
198
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
184
199
 
185
200
  set_named_settings() sets the 'name' setting to 'value'.
186
201
  + 'name' may also be a list of (name, value) pairs.
@@ -412,7 +427,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
412
427
  min_kwh: 0.5 # minimum charge to add in kwh
413
428
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
414
429
  solar_adjust: 100 # % adjustment to make to Solar forecast
415
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
430
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
416
431
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
417
432
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
418
433
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -818,13 +833,29 @@ This setting can be:
818
833
 
819
834
  # Version Info
820
835
 
836
+ 2.9.8 - 2026/03/22<br>
837
+ ** potential breaking changes **
838
+ Update to use Mode Scheduler API v3.
839
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
840
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
841
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
842
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
843
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
844
+ set_period() now supports 'reactive_power' as a parameter.
845
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
846
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
847
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
848
+
849
+ 2.9.7 - 2026/03/18<br>
850
+ Add pv_limit to set_period().
851
+ Default import_linit to 0 for force discharge in set_period()
852
+
821
853
  2.9.6 - 2026/03/10<br>
822
854
  Fix problem setting fdsoc and fdpwr from segment for set_period().
823
855
  Correctly identify H1-G2 model inverter.
824
856
  Add maxChargeCurrent and maxDischargeCurrent to f.battery.
825
857
  Update battery charge derating with temperature so charge current is not 0.
826
858
 
827
-
828
859
  2.9.5 - 2026/02/27<br>
829
860
  Add step parameter to date_list().
830
861
  Get maxGroupCount from v3 scheduler and update f.max_periods with maximum number of time periods supported.
File without changes
File without changes