7171ATTR_PLANTNAME = "plantName"
7272ATTR_MODULESN = "moduleSN"
7373ATTR_DEVICE_TYPE = "deviceType"
74- ATTR_STATUS = "status"
75- ATTR_COUNTRY = "country"
76- ATTR_COUNTRYCODE = "countryCode"
77- ATTR_CITY = "city"
78- ATTR_ADDRESS = "address"
79- ATTR_FEEDINDATE = "feedinDate"
74+ ATTR_MASTER = "masterVersion"
75+ ATTR_MANAGER = "managerVersion"
76+ ATTR_SLAVE = "slaveVersion"
77+ ATTR_BATTERYLIST = "batteryList"
8078ATTR_LASTCLOUDSYNC = "lastCloudSync"
8179
8280BATTERY_LEVELS = {"High" : 80 , "Medium" : 50 , "Low" : 25 , "Empty" : 10 }
8684CONF_DEVICEID = "deviceID"
8785CONF_SYSTEM_ID = "system_id"
8886CONF_EXTPV = "extendPV"
87+ CONF_XTZONE = "xtZone"
8988CONF_GET_VARIABLES = "Restrict"
9089RETRY_NEXT_SLOT = - 1
9190
104103 vol .Required (CONF_DEVICEID ): cv .string ,
105104 vol .Optional (CONF_NAME , default = DEFAULT_NAME ): cv .string ,
106105 vol .Optional (CONF_EXTPV ): cv .boolean ,
106+ vol .Optional (CONF_XTZONE ): cv .boolean ,
107107 vol .Optional (CONF_GET_VARIABLES ): cv .boolean ,
108108 }
109109)
113113
114114async def async_setup_platform (hass , config , async_add_entities , discovery_info = None ):
115115 """Set up the FoxESS sensor."""
116- global LastHour , timeslice , last_api , RestrictGetVar
116+ global LastHour , timeslice , last_api , RestrictGetVar , xtzone
117117 name = config .get (CONF_NAME )
118118 deviceID = config .get (CONF_DEVICEID )
119119 devicesn = config .get (CONF_DEVICESN )
120120 apiKey = config .get (CONF_APIKEY )
121121 ExtPV = config .get (CONF_EXTPV )
122+ xtzone = config .get (CONF_XTZONE )
122123 RestrictGetVar = config .get (CONF_GET_VARIABLES )
123124 _LOGGER .debug ("API Key: %s" , apiKey )
124125 _LOGGER .debug ("Device SN: %s" , devicesn )
125126 _LOGGER .debug ("Device ID: %s" , deviceID )
126127 _LOGGER .debug ("FoxESS Scan Interval: %s minutes" , SCAN_MINUTES )
128+ _LOGGER .debug ("Cross Time Zone: %s" , xtzone )
127129 _LOGGER .debug ("Extended PV: %s" , ExtPV )
128130 if ExtPV is not True :
129131 ExtPV = False
@@ -156,9 +158,7 @@ async def async_update_data():
156158 global token , timeslice , LastHour
157159 hournow = datetime .now ().strftime ("%H" ) # update hour now
158160 _LOGGER .debug ("Time now: %s, last %s" , hournow , LastHour )
159- tslice = (
160- timeslice [devicesn ] + 1
161- ) # get the time slice for the current device and increment it
161+ tslice = timeslice [devicesn ] + 1 # increment current device time slice
162162 timeslice [devicesn ] = tslice
163163 if tslice % 5 == 0 :
164164 _LOGGER .debug ("Main Poll, interval: %s, %s" , devicesn , timeslice [devicesn ])
@@ -167,7 +167,7 @@ async def async_update_data():
167167 if tslice % 15 == 0 :
168168 # get device detail at startup, then every 15 minutes to save api calls
169169 geterror = await getOADeviceDetail (hass , allData , devicesn , apiKey )
170- await asyncio .sleep (2 ) # enforced sleep to limit demand on OpenAPI
170+ await asyncio .sleep (1 ) # OpenAPI demand
171171 if not geterror :
172172 if allData ["addressbook" ]["status" ] is not None :
173173 statetest = int (allData ["addressbook" ]["status" ])
@@ -180,28 +180,18 @@ async def async_update_data():
180180 allData ["online" ] = True
181181 if tslice == 0 :
182182 # read in battery settings if fitted at startup, then every 60 mins
183- addfail = await getOABatterySettings (
184- hass , allData , devicesn , apiKey
185- )
186- await asyncio .sleep (
187- 2
188- ) # enforced sleep to limit demand on OpenAPI
183+ await getOABatterySettings (hass , allData , devicesn , apiKey )
184+ await asyncio .sleep (1 ) # OpenAPI demand
189185 # main real time data fetch, followed by reports
190186 geterror = await getRaw (hass , allData , apiKey , devicesn )
191187 if not geterror :
192- if (
193- tslice % 15 == 0
194- ): # do this at startup, every 15 minutes and on the hour change
195- await asyncio .sleep (
196- 2
197- ) # enforced sleep to limit demand on OpenAPI
188+ if tslice % 15 == 0 : # do at startup and every 15 minutes
189+ await asyncio .sleep (1 ) # OpenAPI demand limit
198190 geterror = await getReport (hass , allData , apiKey , devicesn )
199191 if not geterror :
200192 if tslice == 0 :
201193 # get daily generation at startup, then every 60 minutes
202- await asyncio .sleep (
203- 2
204- ) # enforced sleep to limit demand on OpenAPI
194+ await asyncio .sleep (1 ) # OpenAPI demand
205195 geterror = await getReportDailyGeneration (
206196 hass , allData , apiKey , devicesn
207197 )
@@ -411,6 +401,7 @@ async def async_update_data():
411401 FoxESSBatSoC (coordinator , name , deviceID , "Bat SoC" , "bat-soc" , "SoC" ),
412402 FoxESSBatSoC (coordinator , name , deviceID , "Bat SoC1" , "bat-soc1" , "SoC_1" ),
413403 FoxESSBatSoC (coordinator , name , deviceID , "Bat SoC2" , "bat-soc2" , "SoC_2" ),
404+ FoxESSBatSoC (coordinator , name , deviceID , "Bat SoH" , "bat-soh" , "SOH" ),
414405 FoxESSPower (
415406 coordinator ,
416407 name ,
@@ -505,6 +496,7 @@ async def async_update_data():
505496 FoxESSEnergyBatCharge (coordinator , name , deviceID ),
506497 FoxESSEnergyBatDischarge (coordinator , name , deviceID ),
507498 FoxESSEnergyLoad (coordinator , name , deviceID ),
499+ FoxESSPVEnergyTotal (coordinator , name , deviceID ),
508500 FoxESSResidualEnergy (coordinator , name , deviceID ),
509501 FoxESSResponseTime (coordinator , name , deviceID ),
510502 FoxESSRunningState (
@@ -790,6 +782,7 @@ async def getOADeviceDetail(hass, allData, devicesn, apiKey):
790782 _LOGGER .debug ("OA Device Detail System has Battery: %s" , testBattery )
791783 else :
792784 _LOGGER .debug ("OA Device Detail System has No Battery: %s" , testBattery )
785+ allData ["addressbook" ][ATTR_BATTERYLIST ] = "No Battery"
793786 return False
794787 else :
795788 _LOGGER .error ("OA Device Detail Bad Response: %s" , response )
@@ -875,7 +868,7 @@ async def getReport(hass, allData, apiKey, devicesn):
875868 + now .strftime ("%Y" )
876869 + ',"month":'
877870 + month
878- + ',"dimension":"month","variables":["feedin","generation","gridConsumption","chargeEnergyToTal","dischargeEnergyToTal","loads"]}'
871+ + ',"dimension":"month","variables":["feedin","generation","gridConsumption","chargeEnergyToTal","dischargeEnergyToTal","loads","PVEnergyTotal" ]}'
879872 )
880873
881874 _LOGGER .debug ("getReport OA request: %s" , reportData )
@@ -1097,19 +1090,39 @@ async def getRaw(hass, allData, apiKey, devicesn):
10971090 tzoffsetsign = timercv [23 :24 ]
10981091 tzoffsethr = int (timercv [24 :26 ])
10991092 tzoffsetmin = int (timercv [26 :28 ])
1093+ tzfull = str (timercv [23 :28 ])
1094+ _LOGGER .debug (
1095+ "OA Variables tzoffsign: %s, hr: %s, min: %s, full: %s" ,
1096+ tzoffsetsign ,
1097+ tzoffsethr ,
1098+ tzoffsetmin ,
1099+ tzfull ,
1100+ )
11001101 if tzoffsetsign in ["+" ]:
11011102 tzoffset = (tzoffsethr * 3600 + tzoffsetmin * 60 ) * 1
11021103 else :
11031104 tzoffset = (tzoffsethr * 3600 + tzoffsetmin * 60 ) * - 1
11041105 tsrcv = (parser .parse (timercv , ignoretz = True )).timestamp ()
1105- tsrcv = tsrcv - tzoffset
1106- _LOGGER .debug (
1107- "OA Variables tsrcv stamp: %s, offset: %s " , tsrcv , tzoffset
1108- )
1106+ zulu = datetime .now ().astimezone ().strftime ("%z" )
1107+ if zulu != tzfull :
1108+ if xtzone :
1109+ _LOGGER .debug (
1110+ "OA Variables tsrcv applying offset: %s, offset: %s, zulu: %s" ,
1111+ tsrcv ,
1112+ tzoffset ,
1113+ zulu ,
1114+ )
1115+ tsrcv = tsrcv - tzoffset
1116+ else :
1117+ _LOGGER .debug (
1118+ "OA Variables tsrcv is local: %s, zulu: %s, offset: %s " ,
1119+ tsrcv ,
1120+ zulu ,
1121+ tzoffset ,
1122+ )
11091123 except :
11101124 tsrcv = 0
11111125 age = 0
1112- _LOGGER .debug ("OA Variables time: %s timestamp rcv:%s" , timercv , tsrcv )
11131126 if tsrcv != 0 :
11141127 testd = datetime .now ()
11151128 tsnow = round (time .time ())
@@ -1176,11 +1189,11 @@ async def getRaw(hass, allData, apiKey, devicesn):
11761189 allData ["online" ],
11771190 )
11781191 if variableValue is not None :
1179- if variableValue == "161" :
1192+ if variableValue == "161" or variableValue == "162" :
11801193 # waiting and solar only so set off-line flag
11811194 if age < 361 :
11821195 _LOGGER .debug (
1183- "Waiting but data less than 5 minutes old - allow sample, TestState : %s, hasBat: %s online: %s" ,
1196+ "Waiting but data less than 5 minutes old - allow sample, RunningState : %s, hasBat: %s online: %s" ,
11841197 variableValue ,
11851198 hasBat ,
11861199 allData ["online" ],
@@ -1652,6 +1665,38 @@ def native_value(self) -> str | None:
16521665 return None
16531666
16541667
1668+ class FoxESSPVEnergyTotal (CoordinatorEntity , SensorEntity ):
1669+ _attr_state_class : SensorStateClass = SensorStateClass .TOTAL_INCREASING
1670+ _attr_device_class = SensorDeviceClass .ENERGY
1671+ _attr_native_unit_of_measurement = UnitOfEnergy .KILO_WATT_HOUR
1672+
1673+ def __init__ (self , coordinator , name , deviceID ):
1674+ super ().__init__ (coordinator = coordinator )
1675+ _LOGGER .debug ("Initiating Entity - PV Energy Total" )
1676+ self ._attr_name = name + " - PVEnergyTotal"
1677+ self ._attr_unique_id = deviceID + "PVEnergyTotal"
1678+ self .status = namedtuple (
1679+ "status" ,
1680+ [
1681+ ATTR_DATE ,
1682+ ATTR_TIME ,
1683+ ],
1684+ )
1685+
1686+ @property
1687+ def native_value (self ) -> str | None :
1688+ if "PVEnergyTotal" not in self .coordinator .data ["report" ]:
1689+ _LOGGER .debug ("report PVEnergyTotal None" )
1690+ else :
1691+ if self .coordinator .data ["report" ]["PVEnergyTotal" ] == 0 :
1692+ energyload = 0
1693+ else :
1694+ energyload = self .coordinator .data ["report" ]["PVEnergyTotal" ]
1695+ # round
1696+ return round (energyload , 3 )
1697+ return None
1698+
1699+
16551700class FoxESSInverter (CoordinatorEntity , SensorEntity ):
16561701 def __init__ (self , coordinator , name , deviceID ):
16571702 super ().__init__ (coordinator = coordinator )
@@ -1668,12 +1713,10 @@ def __init__(self, coordinator, name, deviceID):
16681713 ATTR_PLANTNAME ,
16691714 ATTR_MODULESN ,
16701715 ATTR_DEVICE_TYPE ,
1671- ATTR_STATUS ,
1672- ATTR_COUNTRY ,
1673- ATTR_COUNTRYCODE ,
1674- ATTR_CITY ,
1675- ATTR_ADDRESS ,
1676- ATTR_FEEDINDATE ,
1716+ ATTR_MASTER ,
1717+ ATTR_MANAGER ,
1718+ ATTR_SLAVE ,
1719+ ATTR_BATTERYLIST ,
16771720 ATTR_LASTCLOUDSYNC ,
16781721 ],
16791722 )
@@ -1706,6 +1749,10 @@ def extra_state_attributes(self):
17061749 ATTR_PLANTNAME : self .coordinator .data ["addressbook" ][ATTR_PLANTNAME ],
17071750 ATTR_MODULESN : self .coordinator .data ["addressbook" ][ATTR_MODULESN ],
17081751 ATTR_DEVICE_TYPE : self .coordinator .data ["addressbook" ][ATTR_DEVICE_TYPE ],
1752+ ATTR_MASTER : self .coordinator .data ["addressbook" ][ATTR_MASTER ],
1753+ ATTR_MANAGER : self .coordinator .data ["addressbook" ][ATTR_MANAGER ],
1754+ ATTR_SLAVE : self .coordinator .data ["addressbook" ][ATTR_SLAVE ],
1755+ ATTR_BATTERYLIST : self .coordinator .data ["addressbook" ][ATTR_BATTERYLIST ],
17091756 ATTR_LASTCLOUDSYNC : datetime .now (),
17101757 }
17111758
0 commit comments