Skip to content

Commit 6014f8d

Browse files
Fox fixes (#2660)
* Fox fixes * [pre-commit.ci lite] apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent da02736 commit 6014f8d

1 file changed

Lines changed: 123 additions & 7 deletions

File tree

‎apps/predbat/fox.py‎

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,17 @@ async def start(self):
104104
if first and self.automatic:
105105
await self.automatic_config()
106106

107-
if first or (count_seconds % (5 * 60)) == 0:
107+
if first or (count_seconds % (10 * 60)) == 0:
108108
for device in self.device_list:
109109
sn = device.get("deviceSN", None)
110110
if sn:
111-
# await self.get_device_power_generation(sn)
112111
await self.get_device_history(sn)
112+
113+
if first or (count_seconds % (5 * 60)) == 0:
114+
for device in self.device_list:
115+
sn = device.get("deviceSN", None)
116+
if sn:
117+
await self.get_real_time_data(sn)
113118
await self.publish_data()
114119

115120
if not self.api_started:
@@ -224,6 +229,88 @@ async def get_available_variables(self):
224229
available_data[variable_id] = variable
225230
self.available_variables = available_data
226231

232+
async def get_real_time_data(self, deviceSN):
233+
"""
234+
Get real-time data
235+
[
236+
{'datas':
237+
[
238+
{'unit': 'kW', 'name': 'PVPower', 'variable': 'pvPower', 'value': 1.8559999999999999},
239+
{'unit': 'V', 'name': 'PV1Volt', 'variable': 'pv1Volt', 'value': 372.8},
240+
{'unit': 'A', 'name': 'PV1Current', 'variable': 'pv1Current', 'value': 3.0},
241+
{'unit': 'kW', 'name': 'PV1Power', 'variable': 'pv1Power', 'value': 1.128},
242+
{'unit': 'V', 'name': 'PV2Volt', 'variable': 'pv2Volt', 'value': 236.4},
243+
{'unit': 'A', 'name': 'PV2Current', 'variable': 'pv2Current', 'value': 3.0},
244+
{'unit': 'kW', 'name': 'PV2Power', 'variable': 'pv2Power', 'value': 0.728},
245+
{'unit': 'V', 'name': 'PV3Volt', 'variable': 'pv3Volt', 'value': 3.4},
246+
{'unit': 'A', 'name': 'PV3Current', 'variable': 'pv3Current', 'value': 0.0},
247+
{'unit': 'kW', 'name': 'PV3Power', 'variable': 'pv3Power', 'value': 0.0},
248+
{'unit': 'V', 'name': 'PV4Volt', 'variable': 'pv4Volt', 'value': 0.0},
249+
{'unit': 'A', 'name': 'PV4Current', 'variable': 'pv4Current', 'value': 0.0},
250+
{'unit': 'kW', 'name': 'PV4Power', 'variable': 'pv4Power', 'value': 0.0},
251+
{'unit': 'kW', 'name': 'EPSPower', 'variable': 'epsPower', 'value': 0.0},
252+
{'unit': 'A', 'name': 'EPS-RCurrent', 'variable': 'epsCurrentR', 'value': 1.0},
253+
{'unit': 'V', 'name': 'EPS-RVolt', 'variable': 'epsVoltR', 'value': 246.4},
254+
{'unit': 'kW', 'name': 'EPS-RPower', 'variable': 'epsPowerR', 'value': 0.0},
255+
{'unit': 'A', 'name': 'RCurrent', 'variable': 'RCurrent', 'value': 32.3},
256+
{'unit': 'V', 'name': 'RVolt', 'variable': 'RVolt', 'value': 247.4}
257+
,{'unit': 'Hz', 'name': 'RFreq', 'variable': 'RFreq', 'value': 49.97}
258+
,{'unit': 'kW', 'name': 'RPower', 'variable': 'RPower', 'value': 7.993}
259+
,{'unit': '℃', 'name': 'AmbientTemperature', 'variable': 'ambientTemperation', 'value': 33.5}
260+
,{'unit': '℃', 'name': 'InvTemperation', 'variable': 'invTemperation', 'value': 29.7}
261+
,{'unit': '℃', 'name': 'batTemperature', 'variable': 'batTemperature', 'value': 33.6}
262+
,{'unit': 'kW', 'name': 'Load Power', 'variable': 'loadsPower', 'value': 17.046}
263+
,{'unit': 'kW', 'name': 'Output Power', 'variable': 'generationPower', 'value': 7.993}
264+
,{'unit': 'kW', 'name': 'Feed-in Power', 'variable': 'feedinPower', 'value': 0.0}
265+
,{'unit': 'kW', 'name': 'GridConsumption Power', 'variable': 'gridConsumptionPower', 'value': 9.053}
266+
,{'unit': 'V', 'name': 'InvBatVolt', 'variable': 'invBatVolt', 'value': 400.6}
267+
,{'unit': 'A', 'name': 'InvBatCurrent', 'variable': 'invBatCurrent', 'value': 16.4}
268+
,{'unit': 'kW', 'name': 'invBatPower', 'variable': 'invBatPower', 'value': 6.604}
269+
,{'unit': 'kW', 'name': 'Charge Power', 'variable': 'batChargePower', 'value': 0.0}
270+
,{'unit': 'kW', 'name': 'Discharge Power', 'variable': 'batDischargePower', 'value': 6.604}
271+
,{'unit': 'V', 'name': 'BatVolt', 'variable': 'batVolt', 'value': 399.1}
272+
,{'unit': 'A', 'name': 'BatCurrent', 'variable': 'batCurrent', 'value': 3.9}
273+
,{'unit': 'kW', 'name': 'MeterPower', 'variable': 'meterPower', 'value': 9.053}
274+
,{'unit': 'kW', 'name': 'Meter2Power', 'variable': 'meterPower2', 'value': 0.0}
275+
,{'unit': '%', 'name': 'SoC', 'variable': 'SoC', 'value': 26.0}
276+
,{'unit': 'kWh', 'name': 'Cumulative power generation', 'variable': 'generation', 'value': 6133.3}
277+
,{'unit': '0.01kWh', 'name': 'Battery Residual Energy', 'variable': 'ResidualEnergy', 'value': 10.34}
278+
,{'name': 'Running State', 'variable': 'runningState', 'value': '163'}
279+
,{'name': 'Battery Status', 'variable': 'batStatus', 'value': '1'}
280+
,{'name': 'Battery Status Name', 'variable': 'batStatusV2', 'value': 'Charge'}
281+
,{'name': 'The current error code is reported', 'variable': 'currentFault', 'value': ''}
282+
,{'name': 'The number of errors', 'variable': 'currentFaultCount', 'value': '0'}
283+
,{'unit': 'kWh', 'name': 'Battery throughput', 'variable': 'energyThroughput', 'value': 2255.872}
284+
,{'unit': '%', 'name': 'SOH', 'variable': 'SOH', 'value': 99.0}
285+
,{'unit': 'kWh', 'name': 'Total grid electricity consumption', 'variable': 'gridConsumption', 'value': 1712.7}
286+
,{'unit': 'kWh', 'name': 'Load power consumption', 'variable': 'loads', 'value': 4012.8}
287+
,{'unit': 'kWh', 'name': 'The total energy of the feeder', 'variable': 'feedin', 'value': 3730.6}
288+
,{'unit': 'kWh', 'name': 'Total charge energy', 'variable': 'chargeEnergyToTal', 'value': 1061.0},
289+
{'unit': 'kWh', 'name': 'Total discharge energy', 'variable': 'dischargeEnergyToTal', 'value': 1532.6}
290+
],
291+
'time': '2025-09-14 18:43:09 BST+0100', 'deviceSN': '60KE8020479C034'}
292+
]
293+
"""
294+
GET_REAL_TIME_DATA = "/op/v1/device/real/query"
295+
query = {"lang": FOX_LANG, "sns": [deviceSN]}
296+
result = await self.request_get(GET_REAL_TIME_DATA, post=True, datain=query)
297+
if result and isinstance(result, list):
298+
for item in result:
299+
if "datas" in item:
300+
timestamp = item.get("time", "")
301+
datas = item["datas"]
302+
for data_item in datas:
303+
unit = data_item.get("unit", "")
304+
name = data_item.get("name", "")
305+
variable = data_item.get("variable", "")
306+
value = data_item.get("value", None)
307+
if unit == "℃":
308+
unit = "°C"
309+
if name and (value is not None):
310+
if deviceSN not in self.device_values:
311+
self.device_values[deviceSN] = {}
312+
self.device_values[deviceSN][variable] = {"timestamp": timestamp, "value": value, "unit": unit, "name": name}
313+
227314
async def get_device_history(self, deviceSN):
228315
"""
229316
Get device history
@@ -921,29 +1008,50 @@ async def write_battery_schedule_event(self, entity_id, value):
9211008
self.log("Warn: Fox: Event, unknown direction for {}: {}".format(entity_id, sn))
9221009
return
9231010

1011+
if serial not in self.local_schedule:
1012+
self.local_schedule[serial] = {}
1013+
if direction not in self.local_schedule[serial]:
1014+
self.local_schedule[serial][direction] = {}
1015+
9241016
if "_soc" in entity_id:
9251017
try:
9261018
value = int(value)
9271019
except ValueError:
9281020
value = 100 if direction == "charge" else self.fdsoc_min.get(serial, 10)
1021+
if value == self.local_schedule[serial][direction].get("soc", None):
1022+
# Skip null change
1023+
return
9291024
self.local_schedule[serial][direction]["soc"] = value
9301025
elif "_power" in entity_id:
9311026
try:
9321027
value = int(value)
9331028
except ValueError:
9341029
value = self.fdpwr_max.get(serial, 8000)
1030+
if value == self.local_schedule[serial][direction].get("power", None):
1031+
# Skip null change
1032+
return
9351033
self.local_schedule[serial][direction]["power"] = value
9361034
elif "_start_time" in entity_id:
9371035
if value not in OPTIONS_TIME_FULL:
9381036
value = "00:00:00"
1037+
if value == self.local_schedule[serial][direction].get("start_time", None):
1038+
# Skip null change
1039+
return
9391040
self.local_schedule[serial][direction]["start_time"] = value
9401041
elif "_end_time" in entity_id:
9411042
if value not in OPTIONS_TIME_FULL:
9421043
value = "00:00:00"
1044+
if value == self.local_schedule[serial][direction].get("end_time", None):
1045+
# Skip null change
1046+
return
9431047
self.local_schedule[serial][direction]["end_time"] = value
9441048
elif "_enable" in entity_id:
9451049
enable = True if self.local_schedule[serial][direction].get("enable", 0) else False
946-
self.local_schedule[serial][direction]["enable"] = 1 if self.apply_service_to_toggle(enable, value) else 0
1050+
new_enable = 1 if self.apply_service_to_toggle(enable, value) else 0
1051+
if new_enable == self.local_schedule[serial][direction].get("enable", None):
1052+
# Skip null change
1053+
return
1054+
self.local_schedule[serial][direction]["enable"] = new_enable
9471055
else:
9481056
self.log("Warn: Fox: Event, unknown attribute for {}: {}".format(entity_id, serial))
9491057
return
@@ -963,6 +1071,15 @@ async def write_battery_schedule_event(self, entity_id, value):
9631071
end_hour, end_minute = self.time_string_to_hour_minute(end_time, 0, 0)
9641072
minSocOnGrid = self.device_settings.get(serial, {}).get("MinSocOnGrid", {}).get("value", 10)
9651073

1074+
start_minutes = start_hour * 60 + start_minute
1075+
end_minutes = end_hour * 60 + end_minute
1076+
minutes_now = datetime.now().hour * 60 + datetime.now().minute
1077+
if start_minutes <= minutes_now and end_minutes > minutes_now + 1:
1078+
# Schedule seems to not take effect if start time is in the past, so move it forward
1079+
start_minutes = minutes_now + 1
1080+
start_hour = start_minutes // 60
1081+
start_minute = start_minutes % 60
1082+
9661083
if direction == "charge":
9671084
new_schedule.append(
9681085
{
@@ -975,7 +1092,7 @@ async def write_battery_schedule_event(self, entity_id, value):
9751092
"fdSoc": 100,
9761093
"maxSoc": soc,
9771094
"fdPwr": self.fdpwr_max.get(serial, 8000),
978-
"minSocOnGrid": minSocOnGrid,
1095+
"minSocOnGrid": soc,
9791096
}
9801097
)
9811098
elif direction == "discharge":
@@ -1009,9 +1126,7 @@ async def automatic_config(self):
10091126
if hasPV:
10101127
pvs.append(sn.lower())
10111128

1012-
print("SN {} hasBattery {} hasScheduler {} capacity {}".format(sn, hasBattery, hasScheduler, capacity))
1013-
1014-
print("Found {} batteries and {} PVs".format(len(batteries), len(pvs)))
1129+
self.log("Fox API: Found {} batteries and {} PVs".format(len(batteries), len(pvs)))
10151130

10161131
num_inverters = len(batteries)
10171132
self.base.args["inverter_type"] = ["FoxCloud" for _ in range(num_inverters)]
@@ -1024,6 +1139,7 @@ async def automatic_config(self):
10241139
self.base.args["battery_rate_max"] = [f"sensor.predbat_fox_{device}_battery_rate_max" for device in batteries]
10251140
self.base.args["battery_power"] = [f"sensor.predbat_fox_{device}_invbatpower" for device in batteries]
10261141
self.base.args["grid_power"] = [f"sensor.predbat_fox_{device}_gridconsumptionpower" for device in batteries]
1142+
self.base.args["grid_power_invert"] = [True for device in batteries]
10271143
self.base.args["pv_power"] = [f"sensor.predbat_fox_{device}_pvpower" for device in pvs]
10281144
self.base.args["load_power"] = [f"sensor.predbat_fox_{device}_loadspower" for device in batteries]
10291145
self.base.args["soc_percent"] = [f"sensor.predbat_fox_{device}_soc" for device in batteries]

0 commit comments

Comments
 (0)