@@ -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