Skip to content

Commit 03b8619

Browse files
authored
Improved handling of solar only inverter standby and additional Hybrid sensors (#296)
* Improve T Series handover and additional sensors for Hybrid For solar only inverters (T/G series etc..), this improves the handling when the datalogger does not go off-line overnight. Hybrid Inverters - adds PVEnergyTotal and SoH All inverters - sensor.foxess_inverter has additional attributes for Master, Slave & Manager firmware versions - Hybrids also get list of batteries fitted and firmware versions * Update sensor.py Added 162 to potential solar inverter lurking states * Update README Update for release Handles solar only inverter datalogger staying on-line and sending erroneous data (only accepts samples less than 6 minutes old) Hybrid inverters now support these sensors PVEnergyTotal - The total PV production today in kWh SoH - Battery state of health will be populated if BMS supports it All inveters - The sensor.foxess.inverter has additional attributes which include Master, Manager, Slave versions And where batteries are fitted a complete list of all batteries and their firmware versions.
1 parent e267da3 commit 03b8619

2 files changed

Lines changed: 90 additions & 41 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ sensor:
9090

9191
HA Entity | Measurement
9292
|---|---|
93-
Inverter | string `on-line/off-line/in-alarm`
93+
Inverter | string `on-line/off-line/in-alarm` - attributes Master, Manager, Slave versions & Battery details where fitted
9494
Generation Power | kW
9595
Grid Consumption Power | kW
9696
FeedIn Power | kW
@@ -125,6 +125,7 @@ T Freq | Hz
125125
T Power | kW
126126
T Volt | V
127127
Reactive Power | kVar
128+
PV Production Total | kWh
128129
Energy Generated | kWh
129130
Energy Generated Month | kWh
130131
Energy Throughput | kWh
@@ -137,6 +138,7 @@ Bat Discharge | kWh
137138
Bat SoC | % (single battery systems)
138139
Bat SoC1 | % (dual battery systems)
139140
Bat SoC2 | % (dual battery systems)
141+
Bat SoH | % (single battery systems where BMS supports it)
140142
Inverter Bat Power | kW (negative=charging, positive=discharging)
141143
Inverter Bat Power2 | kW (dual battery systems
142144
Bat Temperature | °C

custom_components/foxess/sensor.py

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@
7171
ATTR_PLANTNAME = "plantName"
7272
ATTR_MODULESN = "moduleSN"
7373
ATTR_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"
8078
ATTR_LASTCLOUDSYNC = "lastCloudSync"
8179

8280
BATTERY_LEVELS = {"High": 80, "Medium": 50, "Low": 25, "Empty": 10}
@@ -86,6 +84,7 @@
8684
CONF_DEVICEID = "deviceID"
8785
CONF_SYSTEM_ID = "system_id"
8886
CONF_EXTPV = "extendPV"
87+
CONF_XTZONE = "xtZone"
8988
CONF_GET_VARIABLES = "Restrict"
9089
RETRY_NEXT_SLOT = -1
9190

@@ -104,6 +103,7 @@
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
)
@@ -113,17 +113,19 @@
113113

114114
async 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+
16551700
class 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

Comments
 (0)