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.
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/PKG-INFO +24 -6
- foxesscloud-2.9.7/src/foxesscloud.egg-info/PKG-INFO → foxesscloud-2.9.9/README.md +23 -19
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/pyproject.toml +1 -1
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/src/foxesscloud/openapi.py +108 -47
- foxesscloud-2.9.7/README.md → foxesscloud-2.9.9/src/foxesscloud.egg-info/PKG-INFO +37 -5
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/LICENCE +0 -0
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/setup.cfg +0 -0
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/src/foxesscloud/foxesscloud.py +0 -0
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/src/foxesscloud.egg-info/SOURCES.txt +0 -0
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/src/foxesscloud.egg-info/dependency_links.txt +0 -0
- {foxesscloud-2.9.7 → foxesscloud-2.9.9}/src/foxesscloud.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: foxesscloud
|
|
3
|
-
Version: 2.9.
|
|
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
|
-
+
|
|
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:
|
|
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
|
-
+
|
|
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:
|
|
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()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
##################################################################################################
|
|
2
2
|
"""
|
|
3
3
|
Module: Fox ESS Cloud using Open API
|
|
4
|
-
Updated:
|
|
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.
|
|
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':
|
|
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
|
|
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
|
|
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/
|
|
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['
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 ('
|
|
1283
|
-
pv_limit = int(1.5 * power) if pv_limit is None and device.get('power') is not None and ('
|
|
1284
|
-
import_limit = 0 if import_limit is None and 'ForceDischarge' in mode else import_limit
|
|
1285
|
-
|
|
1286
|
-
|
|
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 = {'
|
|
1310
|
-
'extraParam': {'minSocOnGrid':
|
|
1311
|
-
|
|
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
|
|
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/
|
|
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['
|
|
2738
|
-
s['
|
|
2739
|
-
|
|
2740
|
-
|
|
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':
|
|
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
|
-
+
|
|
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:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|