foxesscloud 2.9.7__tar.gz → 2.9.9__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.7
3
+ Version: 2.9.9
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
@@ -116,7 +116,7 @@ f.get_charge()
116
116
  f.get_min()
117
117
  f.get_peakshaving()
118
118
  f.get_flag()
119
- f.get_schedule()
119
+ f.get_schedule(filter)
120
120
  f.get_named_settings(name)
121
121
  f.get_heating()
122
122
 
@@ -140,9 +140,10 @@ 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
+ + filter: 1 = remove Remain Time period (00:00-23:59) from results. 0 = don't remove. Default 1.
146
147
 
147
148
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
149
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -157,7 +158,7 @@ You can change inverter settings using:
157
158
  f.set_min(minSocOnGrid, minSoc)
158
159
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
159
160
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
160
- f.set_schedule(periods, enable)
161
+ f.set_schedule(periods, enable, is_default)
161
162
  f.set_named_settings(name, value, force)
162
163
  f.set_heating(enable, start, end, time1, time2, time3)
163
164
  ```
@@ -186,7 +187,6 @@ set_period() returns a period structure that can be used to build a list for set
186
187
  + export_limit: optional, default is None (not set).
187
188
  + pv_limit: optional, default is 1.5 x inverter rating.
188
189
  + price: optional, default None. Used to display plunge pricing for time period.
189
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
190
190
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
191
191
 
192
192
  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.
@@ -196,6 +196,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
196
196
  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
197
197
  + periods: a time segment or list of time segments created using f.set_period().
198
198
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
199
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
199
200
 
200
201
  set_named_settings() sets the 'name' setting to 'value'.
201
202
  + 'name' may also be a list of (name, value) pairs.
@@ -427,7 +428,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
427
428
  min_kwh: 0.5 # minimum charge to add in kwh
428
429
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
429
430
  solar_adjust: 100 # % adjustment to make to Solar forecast
430
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
431
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
431
432
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
432
433
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
433
434
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -833,6 +834,23 @@ This setting can be:
833
834
 
834
835
  # Version Info
835
836
 
837
+ 2.9.9 - 2026/04/03<br>
838
+ Add 'isRemainMode' flag to time periods (not currently processed by Open API).
839
+ Add 'filter' to f.get_schedule() to drop Remain Time period (00:00-23:59) to avoid overlap with other time periods.
840
+
841
+ 2.9.8 - 2026/03/22<br>
842
+ ** potential breaking changes **
843
+ Update to use Mode Scheduler API v3.
844
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
845
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
846
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
847
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
848
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
849
+ set_period() now supports 'reactive_power' as a parameter.
850
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
851
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
852
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
853
+
836
854
  2.9.7 - 2026/03/18<br>
837
855
  Add pv_limit to set_period().
838
856
  Default import_linit to 0 for force discharge in set_period()
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: foxesscloud
3
- Version: 2.9.7
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>
@@ -116,7 +102,7 @@ f.get_charge()
116
102
  f.get_min()
117
103
  f.get_peakshaving()
118
104
  f.get_flag()
119
- f.get_schedule()
105
+ f.get_schedule(filter)
120
106
  f.get_named_settings(name)
121
107
  f.get_heating()
122
108
 
@@ -140,9 +126,10 @@ 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().
132
+ + filter: 1 = remove Remain Time period (00:00-23:59) from results. 0 = don't remove. Default 1.
146
133
 
147
134
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
148
135
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -157,7 +144,7 @@ You can change inverter settings using:
157
144
  f.set_min(minSocOnGrid, minSoc)
158
145
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
159
146
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
160
- f.set_schedule(periods, enable)
147
+ f.set_schedule(periods, enable, is_default)
161
148
  f.set_named_settings(name, value, force)
162
149
  f.set_heating(enable, start, end, time1, time2, time3)
163
150
  ```
@@ -186,7 +173,6 @@ set_period() returns a period structure that can be used to build a list for set
186
173
  + export_limit: optional, default is None (not set).
187
174
  + pv_limit: optional, default is 1.5 x inverter rating.
188
175
  + price: optional, default None. Used to display plunge pricing for time period.
189
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
190
176
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
191
177
 
192
178
  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.
@@ -196,6 +182,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
196
182
  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
197
183
  + periods: a time segment or list of time segments created using f.set_period().
198
184
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
185
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
199
186
 
200
187
  set_named_settings() sets the 'name' setting to 'value'.
201
188
  + 'name' may also be a list of (name, value) pairs.
@@ -427,7 +414,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
427
414
  min_kwh: 0.5 # minimum charge to add in kwh
428
415
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
429
416
  solar_adjust: 100 # % adjustment to make to Solar forecast
430
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
417
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
431
418
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
432
419
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
433
420
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -833,6 +820,23 @@ This setting can be:
833
820
 
834
821
  # Version Info
835
822
 
823
+ 2.9.9 - 2026/04/03<br>
824
+ Add 'isRemainMode' flag to time periods (not currently processed by Open API).
825
+ Add 'filter' to f.get_schedule() to drop Remain Time period (00:00-23:59) to avoid overlap with other time periods.
826
+
827
+ 2.9.8 - 2026/03/22<br>
828
+ ** potential breaking changes **
829
+ Update to use Mode Scheduler API v3.
830
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
831
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
832
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
833
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
834
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
835
+ set_period() now supports 'reactive_power' as a parameter.
836
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
837
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
838
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
839
+
836
840
  2.9.7 - 2026/03/18<br>
837
841
  Add pv_limit to set_period().
838
842
  Default import_linit to 0 for force discharge in set_period()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.9.7"
7
+ version = "2.9.9"
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 using Open API
4
- Updated: 18 March 2026
4
+ Updated: 03 April 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.7"
13
+ version = "2.9.9"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -1144,6 +1144,51 @@ def set_work_mode(mode, force = 0):
1144
1144
  work_mode = mode
1145
1145
  return work_mode
1146
1146
 
1147
+ ##################################################################################################
1148
+ # Modbus Commands
1149
+ ##################################################################################################
1150
+
1151
+ slave_address = 247
1152
+ modbus_timeout = 10
1153
+
1154
+ def modbus_data(function, register, value=None):
1155
+ return None
1156
+
1157
+ def get_modbus(register, slave=None, function=None, timeout=None):
1158
+ global device_sn, modbus_timeout
1159
+ function = 4 if function is None else function
1160
+ output(f"\nGetting Modbus: {mode}", 1)
1161
+ packet = modbus_data(function, register)
1162
+ body = {'sn': device_sn, 'timeout': modbus_timeout, 'data': packet}
1163
+ response = signed_post(path="/op/v0/module/modbus/commands", body=body)
1164
+ if response.status_code != 200:
1165
+ output(f"** get_modbus() got response code {response.status_code}: {response.reason}")
1166
+ return None
1167
+ errno = response.json().get('errno')
1168
+ if errno != 0:
1169
+ output(f"** get_modbus(), {errno_message(response)}")
1170
+ return None
1171
+ result = response.json().get('result')
1172
+ return result
1173
+
1174
+ def set_modbus(register, value, slave=None, function=None, timeout=None):
1175
+ global device_sn, modbus_timeout
1176
+ function = 16 if type(value) is list else 6
1177
+ output(f"\nSetting Modbus: {mode}", 1)
1178
+ packet = modbus_data(function, register, value)
1179
+ body = {'sn': device_sn, 'timeout': modbus_timeout, 'data': packet}
1180
+ setting_delay()
1181
+ response = signed_post(path="/op/v0/module/modbus/commands", body=body)
1182
+ if response.status_code != 200:
1183
+ output(f"** get_modbus() got response code {response.status_code}: {response.reason}")
1184
+ return None
1185
+ errno = response.json().get('errno')
1186
+ if errno != 0:
1187
+ output(f"** get_modbus(), {errno_message(response)}")
1188
+ return None
1189
+ result = response.json().get('result')
1190
+ return result
1191
+
1147
1192
 
1148
1193
  ##################################################################################################
1149
1194
  # get flag
@@ -1154,7 +1199,7 @@ max_periods = 8
1154
1199
 
1155
1200
  # get the current switch status
1156
1201
  def get_flag():
1157
- global device_sn, schedule, debug_setting, max_periods
1202
+ global device_sn, schedule, debug_setting, max_periods, work_modes, settable_modes
1158
1203
  if get_device() is None:
1159
1204
  return None
1160
1205
  output(f"getting flag", 2)
@@ -1167,22 +1212,29 @@ def get_flag():
1167
1212
  if result is None:
1168
1213
  return None
1169
1214
  if schedule is None:
1170
- schedule = {'enable': None, 'support': None, 'periods': None, 'maxsoc': None}
1215
+ schedule = {'enable': None, 'support': None, 'periods': [], 'maxsoc': None}
1171
1216
  schedule['enable'] = result.get('enable')
1172
1217
  schedule['support'] = result.get('support')
1173
1218
  if device.get('function') is not None and device['function'].get('scheduler') is not None:
1174
1219
  device['function']['scheduler'] = schedule['support']
1175
1220
  if schedule.get('maxGroupCount') is None:
1176
- output(f"getting maxGroupCount", 2)
1221
+ output(f"getting properties", 2)
1177
1222
  body = {'deviceSN': device_sn}
1178
1223
  response = signed_post(path="/op/v3/device/scheduler/get", body=body)
1179
1224
  if response.status_code != 200:
1180
- output(f"** get_flag() got response code getting maxGroupCount {response.status_code}: {response.reason}")
1225
+ output(f"** get_flag() got response code getting properties {response.status_code}: {response.reason}")
1181
1226
  return None
1182
1227
  result = response.json().get('result')
1183
1228
  if result is not None:
1184
1229
  schedule['maxGroupCount'] = result.get('maxGroupCount')
1185
1230
  max_periods = schedule['maxGroupCount']
1231
+ schedule['properties'] = result.get('properties')
1232
+ if schedule['properties'] is not None:
1233
+ schedule['maxsoc'] = schedule['properties'].get('maxsoc') is not None
1234
+ modes = schedule['properties'].get('workmode')
1235
+ if modes is not None:
1236
+ work_modes = sorted(modes['enumList'])
1237
+ settable_modes = [w for w in work_modes if 'Force' not in w]
1186
1238
  return schedule
1187
1239
 
1188
1240
  ##################################################################################################
@@ -1190,7 +1242,7 @@ def get_flag():
1190
1242
  ##################################################################################################
1191
1243
 
1192
1244
  # get the current schedule
1193
- def get_schedule():
1245
+ def get_schedule(filter=1):
1194
1246
  global device_sn, schedule, debug_setting, work_modes
1195
1247
  if get_flag() is None:
1196
1248
  return None
@@ -1199,7 +1251,7 @@ def get_schedule():
1199
1251
  return None
1200
1252
  output(f"getting schedule", 2)
1201
1253
  body = {'deviceSN': device_sn}
1202
- response = signed_post(path="/op/v2/device/scheduler/get", body=body)
1254
+ response = signed_post(path="/op/v3/device/scheduler/get", body=body)
1203
1255
  if response.status_code != 200:
1204
1256
  output(f"** get_schedule() got response code {response.status_code}: {response.reason}")
1205
1257
  return None
@@ -1212,13 +1264,13 @@ def get_schedule():
1212
1264
  enable = True if enable == 1 else False
1213
1265
  schedule['enable'] = enable
1214
1266
  schedule['periods'] = []
1215
- schedule['maxsoc'] = False
1216
1267
  # remove invalid work mode from periods
1217
1268
  for g in result['groups']:
1218
- if g['enable'] == 1 and g['workMode'] in work_modes:
1219
- schedule['periods'].append(g)
1220
- if g.get('extraParam') is not None and g['extraParam'].get('maxSoc') is not None:
1221
- schedule['maxsoc'] = True
1269
+ if g['workMode'] in work_modes:
1270
+ remain_mode = g['startHour'] == 0 and g['startMinute'] == 0 and g['endHour'] == 23 and g['endMinute'] == 59
1271
+ g['isRemainMode'] = remain_mode
1272
+ if not remain_mode or filter == 0:
1273
+ schedule['periods'].append(g)
1222
1274
  return schedule
1223
1275
 
1224
1276
  # build strategy using current schedule
@@ -1246,9 +1298,10 @@ def build_strategy_from_schedule():
1246
1298
  ##################################################################################################
1247
1299
 
1248
1300
  # 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, pv_limit=None, price=None, segment=None, enable=1, quiet=1):
1301
+ 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
1302
+ , price=None, segment=None, enable=1, quiet=1):
1250
1303
  global schedule, device
1251
- if schedule is None or schedule.get('maxsoc') is None:
1304
+ if schedule is None:
1252
1305
  get_schedule()
1253
1306
  if segment is not None and type(segment) is dict:
1254
1307
  start = segment.get('start')
@@ -1261,40 +1314,34 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1261
1314
  import_limit = segment.get('import_limit')
1262
1315
  export_limit = segment.get('export_limit')
1263
1316
  pv_limit = segment.get('pv_limit')
1317
+ reactive_power = segment.get('reactive_power')
1264
1318
  price = segment.get('price')
1319
+ if enable == 0:
1320
+ return None
1265
1321
  start = time_hours(start)
1266
1322
  # adjust exclusive time to inclusive
1267
1323
  end = time_hours(end)
1268
1324
  if start is None or end is None or start >= end:
1269
1325
  output(f"set_period(): ** invalid period times: {hours_time(start)}-{hours_time(end)}")
1270
1326
  return None
1327
+ remain_mode = start == 0 and end == 24
1271
1328
  end = round_time(end - 1/60)
1272
1329
  mode = 'SelfUse' if mode is None else mode
1273
1330
  if mode not in work_modes:
1274
1331
  output(f"** mode must be one of {work_modes}")
1275
1332
  return None
1333
+ properties = schedule.get('properties')
1276
1334
  min_soc = 10 if min_soc is None else min_soc
1277
- 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
1335
+ max_soc = None if properties.get('maxsoc') is None or 'ForceCharge' not in mode else 100 if max_soc is None else max_soc
1278
1336
  if 'ForceCharge' in mode and fdsoc is None:
1279
1337
  fdsoc = max_soc if max_soc is not None else 100
1280
- fdsoc = min_soc if fdsoc is None else fdsoc
1338
+ fdsoc = None if properties.get('fdsoc') is None or 'Force' not in mode else min_soc if fdsoc is None else fdsoc
1281
1339
  power = (device['power'] * 1000) if device.get('power') is not None else None
1282
- fdpwr = power if fdpwr is None and device.get('power') is not None and ('ForceCharge' in mode or 'ForceDischarge' in mode) else fdpwr
1283
- pv_limit = int(1.5 * power) if pv_limit is None and device.get('power') is not None and ('ForceCharge' in mode or 'ForceDischarge' in mode) else pv_limit
1284
- import_limit = 0 if import_limit is None and 'ForceDischarge' in mode else import_limit
1285
- fdpwr = 0 if fdpwr is None else fdpwr
1286
- if min_soc < 0 or min_soc > 100:
1287
- output(f"set_period(): ** min_soc must be between 0 and 100")
1288
- return None
1289
- if max_soc is not None and (max_soc < 10 or max_soc > 100):
1290
- output(f"set_period(): ** max_soc must be between 10 and 100")
1291
- return None
1292
- if fdpwr < 0 or fdpwr > 30000:
1293
- output(f"set_period(): ** fdpwr must be between 0 and 30000")
1294
- return None
1295
- if fdsoc < min_soc or fdsoc > 100:
1296
- output(f"set_period(): ** fdsoc must between {min_soc} and 100")
1297
- return None
1340
+ 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
1341
+ 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
1342
+ import_limit = None if properties.get('importlimit') is None else 0 if import_limit is None and 'ForceDischarge' in mode else import_limit
1343
+ export_limit = None if properties.get('exportlimit') is None else export_limit
1344
+ reactive_power = None if properties.get('reactivepower') is None else reactive_power
1298
1345
  if quiet == 0:
1299
1346
  s = f" {hours_time(start)}-{hours_time(end)} {mode}, minsoc {min_soc}%"
1300
1347
  s += f", maxsoc {max_soc}%" if max_soc is not None and 'ForceCharge' in mode else ""
@@ -1303,16 +1350,30 @@ def set_period(start=None, end=None, mode=None, min_soc=None, max_soc=None, fdso
1303
1350
  s += f", importLimit {int(import_limit)}W" if import_limit is not None else ""
1304
1351
  s += f", pvLimit {int(pv_limit)}W" if pv_limit is not None else ""
1305
1352
  s += f", {price:.2f}p/kWh" if price is not None else ""
1353
+ s += f", Remain Mode" if remain_mode else ""
1306
1354
  output(s, 1)
1307
1355
  start_hour, start_minute = split_hours(start)
1308
1356
  end_hour, end_minute = split_hours(end)
1309
- period = {'enable': enable, 'startHour': start_hour, 'startMinute': start_minute, 'endHour': end_hour, 'endMinute': end_minute, 'workMode': mode,
1310
- 'extraParam': {'minSocOnGrid': int(min_soc), 'fdSoc': int(fdsoc), 'fdPwr': int(fdpwr), 'maxSoc': max_soc, 'importLimit': import_limit,
1311
- 'exportLimit': export_limit, 'pvLimit': pv_limit}}
1357
+ period = {'startHour': start_hour, 'startMinute': start_minute, 'endHour': end_hour, 'endMinute': end_minute, 'workMode': mode, 'isRemainMode': remain_mode
1358
+ , 'extraParam': {'minSocOnGrid': min_soc}}
1359
+ if max_soc is not None:
1360
+ period['extraParam']['maxSoc'] = max_soc
1361
+ if fdsoc is not None:
1362
+ period['extraParam']['fdSoc'] = int(fdsoc)
1363
+ if fdpwr is not None:
1364
+ period['extraParam']['fdPwr'] = int(fdpwr)
1365
+ if import_limit is not None:
1366
+ period['extraParam']['importLimit'] = import_limit
1367
+ if export_limit is not None:
1368
+ period['extraParam']['exportLimit'] = export_limit
1369
+ if pv_limit is not None:
1370
+ period['extraParam']['pvLimit'] = pv_limit
1371
+ if reactive_power is not None:
1372
+ period['extraParam']['reactivePower'] = reactive_power
1312
1373
  return period
1313
1374
 
1314
1375
  # set a schedule from a period or list of time segment periods
1315
- def set_schedule(periods=None, enable=True):
1376
+ def set_schedule(periods=None, enable=True, is_default=False):
1316
1377
  global device_sn, debug_setting, schedule, max_periods
1317
1378
  if get_flag() is None:
1318
1379
  return None
@@ -1325,18 +1386,18 @@ def set_schedule(periods=None, enable=True):
1325
1386
  return None
1326
1387
  if type(enable) is int:
1327
1388
  enable = True if enable == 1 else False
1328
- if enable == False:
1329
- output(f"\nDisabling schedule", 1)
1330
- else:
1389
+ if enable:
1331
1390
  output(f"\nEnabling schedule", 1)
1391
+ else:
1392
+ output(f"\nDisabling schedule", 1)
1332
1393
  if periods is not None:
1333
1394
  if type(periods) is not list:
1334
1395
  periods = [periods]
1335
1396
  if len(periods) > max_periods:
1336
1397
  output(f"** set_schedule(): maximum of {max_periods} periods allowed, {len(periods)} provided")
1337
- body = {'deviceSN': device_sn, 'groups': periods[-max_periods:]}
1398
+ body = {'deviceSN': device_sn, 'isDefault': is_default, 'groups': periods[-max_periods:]}
1338
1399
  setting_delay()
1339
- response = signed_post(path="/op/v2/device/scheduler/enable", body=body)
1400
+ response = signed_post(path="/op/v3/device/scheduler/enable", body=body)
1340
1401
  if response.status_code != 200:
1341
1402
  output(f"** set_schedule() periods response code {response.status_code}: {response.reason}")
1342
1403
  return None
@@ -2734,10 +2795,10 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2734
2795
  elif 'ForceCharge' in period['mode']:
2735
2796
  s['max_soc'] = period.get('max_soc')
2736
2797
  elif period['mode'] == 'SelfUse' and period['hold'] == 1:
2737
- s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2738
- s['end'] = (start + 1 / steps_per_hour) % 24
2739
- for p in times:
2740
- p['min_soc'] = s['min_soc']
2798
+ s['mode'] = 'ForceDischarge'
2799
+ s['fdpwr'] = 0
2800
+ s['fdsoc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2801
+ s['min_soc'] = 10
2741
2802
  if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
2742
2803
  strategy.append(s)
2743
2804
  start = h
@@ -2790,7 +2851,7 @@ charge_config = {
2790
2851
  'use_today': 21.0, # hour when todays consumption and generation can be used
2791
2852
  'min_hours': 0.5, # minimum charge time in decimal hours
2792
2853
  'min_kwh': 0.5, # minimum to add in kwh
2793
- 'forecast_selection': 1, # 0 = use available forecast / generation, 1 only update settings with forecast
2854
+ 'forecast_selection': 0, # 0 = use available forecast / generation, 1 only update settings with forecast
2794
2855
  'annual_consumption': None, # optional annual consumption in kWh
2795
2856
  'timed_mode': 0, # 0 = None, 1 = timed mode, 2 = strategy mode
2796
2857
  '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.9
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>
@@ -102,7 +116,7 @@ f.get_charge()
102
116
  f.get_min()
103
117
  f.get_peakshaving()
104
118
  f.get_flag()
105
- f.get_schedule()
119
+ f.get_schedule(filter)
106
120
  f.get_named_settings(name)
107
121
  f.get_heating()
108
122
 
@@ -126,9 +140,10 @@ 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().
146
+ + filter: 1 = remove Remain Time period (00:00-23:59) from results. 0 = don't remove. Default 1.
132
147
 
133
148
  get_named_settings() returns the value of a named setting. If 'name' is a list, it returns a list of values.
134
149
  + f.named_settings is updated. This is dictionary of information and current value, indexed by 'name'.
@@ -143,7 +158,7 @@ You can change inverter settings using:
143
158
  f.set_min(minSocOnGrid, minSoc)
144
159
  f.set_charge(ch1, st1, en1, ch2, st2, en2, enable)
145
160
  f.set_period(start, end, mode, min_soc, max_soc, fdsoc, fdpwr, price, segment)
146
- f.set_schedule(periods, enable)
161
+ f.set_schedule(periods, enable, is_default)
147
162
  f.set_named_settings(name, value, force)
148
163
  f.set_heating(enable, start, end, time1, time2, time3)
149
164
  ```
@@ -172,7 +187,6 @@ set_period() returns a period structure that can be used to build a list for set
172
187
  + export_limit: optional, default is None (not set).
173
188
  + pv_limit: optional, default is 1.5 x inverter rating.
174
189
  + price: optional, default None. Used to display plunge pricing for time period.
175
- + enable: sets whether this time segment is enable (1) or disabled (0). The default is enabled.
176
190
  + segment: optional, allows the parameters for the period to be passed as a dictionary instead of individual values.
177
191
 
178
192
  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.
@@ -182,6 +196,7 @@ The variable f.max_periods is used to check the number of time periods allowed.
182
196
  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
183
197
  + periods: a time segment or list of time segments created using f.set_period().
184
198
  + enable: 1 to enable schedules, 0 to disable schedules. The default is 1.
199
+ + is_default: False (default): parameters not provided remain unchanged. True: parameters not provided are restored to system defaults
185
200
 
186
201
  set_named_settings() sets the 'name' setting to 'value'.
187
202
  + 'name' may also be a list of (name, value) pairs.
@@ -413,7 +428,7 @@ min_hours: 0.25 # minimum charge time to set (in decimal hours)
413
428
  min_kwh: 0.5 # minimum charge to add in kwh
414
429
  solcast_adjust: 100 # % adjustment to make to Solcast forecast
415
430
  solar_adjust: 100 # % adjustment to make to Solar forecast
416
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
431
+ forecast_selection: 0 # 1 = only update charge times if forecast is available, 0 = use best available data.
417
432
  annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
418
433
  timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
419
434
  special_contingency: 35 # contingency for special days when consumption might be higher
@@ -819,6 +834,23 @@ This setting can be:
819
834
 
820
835
  # Version Info
821
836
 
837
+ 2.9.9 - 2026/04/03<br>
838
+ Add 'isRemainMode' flag to time periods (not currently processed by Open API).
839
+ Add 'filter' to f.get_schedule() to drop Remain Time period (00:00-23:59) to avoid overlap with other time periods.
840
+
841
+ 2.9.8 - 2026/03/22<br>
842
+ ** potential breaking changes **
843
+ Update to use Mode Scheduler API v3.
844
+ This adds 'properties' to f.schedule that describes the work modes and parameters supported on different inverters.
845
+ f.work_modes and f.settable_modes are now set dynamically from the properties.
846
+ The period parameters that are sent are trimmed to the supported set and unused parameters for the mode are removed.
847
+ ** API v3 rejects unsupported parameters. This limits the testing I can do for different inverters. Please report any issues **
848
+ API v3 removes the previous ability to send disabled time periods. If you try to send a disabled period, set_period() will return None.
849
+ set_period() now supports 'reactive_power' as a parameter.
850
+ set_schedule() now supports 'is_default'. See Open API documentation for more info on what this does.
851
+ charge_needed() changed from using min_soc for battery hold to using ForceDischarge with fdpwr=0 when timed_mode=2.
852
+ charge_needed() default configuration now has forecast_selection=0 so settings will be updated if a forecast is not available.
853
+
822
854
  2.9.7 - 2026/03/18<br>
823
855
  Add pv_limit to set_period().
824
856
  Default import_linit to 0 for force discharge in set_period()
File without changes
File without changes