import json import asyncio import aiohttp import sys import logging import math from typing import List from dataclasses import dataclass from dataclasses import field import copy _LOGGER = logging.getLogger(__name__) @dataclass class DTUInvData: p_PV:List[float] = field(default_factory=list) p_AC:float = 0.0 temp:float = 0.0 error:int = 0 name:str = "" estimated:bool = False # Werte hochgerechnet statt gemessen p_PV_est:float = 0.0 # davon geschaetzte DC-Leistung in W limit:float = 0.0 # limit_absolute der DTU, W - Deckel der Schaetzung @dataclass class DTUData: inverter:List[DTUInvData] = field(default_factory=list)#default_factory=lambda: [DTUInvData(), DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData()]) p_AC_sum:float = 0 ret = DTUData() # Stand aus dem letzten Moment, in dem ALLE Wechselrichter frische Daten # geliefert haben. Faellt spaeter einer aus, laesst sich sein Beitrag daraus # hochrechnen: die uebrigen stehen daneben und sehen dieselbe Sonne, ihr # Verhaeltnis zu diesem Stand traegt Sonnenstand und Bewoelkung mit. _ref = {"p_AC": {}, "p_PV": {}} def schaetzeAusfaelle(stale): """Beitrag ausgefallener Wechselrichter aus den gesunden hochrechnen. Tritt an die Stelle der frueheren _CP-Kopien: die galten nur fuer das Paar 1/2 und uebernahmen den Nachbarwert unskaliert. Grundlage ist _ref, der letzte Moment mit lueckenlosen Daten. Der Faktor ist das Verhaeltnis, in dem die weiterhin gesunden Geraete seither gestiegen oder gefallen sind - damit traegt die Schaetzung Sonnenstand und Bewoelkung mit. Ohne Referenz oder ohne gesunden Nachbarn wird wie frueher genullt. Gedeckelt wird auf limit_absolute, die von der DTU gemeldete Wirkleistungsgrenze des Geraets - die steht auch dann noch in der Uebersichtsantwort, wenn die Detailabfrage schon ins Leere laeuft. """ for i in range(len(ret.inverter)): ret.inverter[i].estimated = False ret.inverter[i].p_PV_est = 0.0 gesund = [i for i in range(len(ret.inverter)) if i not in stale] if not stale: for i in gesund: # Referenz nachfuehren _ref["p_AC"][i] = ret.inverter[i].p_AC _ref["p_PV"][i] = list(ret.inverter[i].p_PV) return refSum = sum(_ref["p_AC"].get(i,0.0) for i in gesund) nowSum = sum(ret.inverter[i].p_AC for i in gesund) faktor = nowSum/refSum if refSum > 0 else 0.0 for i in stale: if _ref["p_AC"].get(i,0.0) > 0 and faktor > 0: pAC = _ref["p_AC"][i]*faktor if ret.inverter[i].limit > 0: pAC = min(pAC, ret.inverter[i].limit) skala = pAC/_ref["p_AC"][i] ret.inverter[i].p_AC = round(pAC,2) ret.inverter[i].p_PV = [round(v*skala,2) for v in _ref["p_PV"].get(i,[])] ret.inverter[i].p_PV_est = round(sum(ret.inverter[i].p_PV),2) ret.inverter[i].estimated = True else: # keine Grundlage - wie bisher nullen ret.inverter[i].p_AC = 0 ret.inverter[i].p_PV = [0]*len(ret.inverter[i].p_PV) async def gatherData() -> DTUData: #print("get charger status..."); stale = set() timeout = aiohttp.ClientTimeout(total=3) try: async with aiohttp.ClientSession() as session: async with session.get('http://192.168.179.36/api/livedata/status',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car if response.status == 200: html = await response.text() #logging.warning(html) state = json.loads(html) ret.p_AC_sum = state["total"]["Power"]["v"] invNum = 0 for inverter in state["inverters"]: if len(ret.inverter) <= invNum: ret.inverter.append(DTUInvData()) ret.inverter[invNum].name = inverter["name"] ret.inverter[invNum].limit = inverter.get("limit_absolute",0) or 0 try: async with session.get('http://192.168.179.36/api/livedata/status?inv='+inverter["serial"],timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car html = await response.text() invState = json.loads(html) if(invState["inverters"][0]["data_age"] < 60): ret.inverter[invNum].p_AC = invState["inverters"][0]["AC"]["0"]["Power"]["v"] ret.inverter[invNum].temp = invState["inverters"][0]["INV"]["0"]["Temperature"]["v"] ret.inverter[invNum].error = 0 pvNum = 0 for key, pvs in invState["inverters"][0]["DC"].items(): if len(ret.inverter[invNum].p_PV) <= pvNum: ret.inverter[invNum].p_PV.append(pvs["Power"]["v"]) else: ret.inverter[invNum].p_PV[pvNum] = pvs["Power"]["v"] pvNum = pvNum + 1 else: ret.inverter[invNum].error += 1 stale.add(invNum) except: ret.inverter[invNum].error += 1 stale.add(invNum) _LOGGER.warning("Inverter data error: "+inverter["serial"]) invNum = invNum + 1 schaetzeAusfaelle(stale) except: # Die DTU selbst antwortet nicht - es gibt keinen gesunden Nachbarn, aus # dem sich etwas hochrechnen liesse. Beide Seiten werden genullt, damit # p_AC und p_PV zueinander passen; frueher blieb p_AC auf dem alten Wert # stehen, waehrend p_PV auf 0 fiel. Der error-Zaehler traegt es nach aussen. for inv in ret.inverter: inv.error += 1 inv.estimated = False inv.p_PV_est = 0.0 inv.p_AC = 0 try: inv.p_PV = [0]*len(inv.p_PV) except: pass _LOGGER.debug("OpenDTU not working") return ret #async def main(): # await gatherNeededStatus() #loop = asyncio.get_event_loop() #loop.run_until_complete(main()) """ { "inverters":[ { "serial":"116491626890", "name":"Terrassendach", "order":0, "data_age":1, "poll_enabled":true, "reachable":true, "producing":true, "limit_relative":100, "limit_absolute":1600, "AC":{ "0":{ "Power":{ "v":74.09999847, "u":"W", "d":1 }, "Voltage":{ "v":235.3000031, "u":"V", "d":1 }, "Current":{ "v":0.310000002, "u":"A", "d":2 }, "Power DC":{ "v":77.80000305, "u":"W", "d":1 }, "YieldDay":{ "v":179, "u":"Wh", "d":0 }, "YieldTotal":{ "v":8.732000351, "u":"kWh", "d":3 }, "Frequency":{ "v":50.00999832, "u":"Hz", "d":2 }, "PowerFactor":{ "v":1, "u":"", "d":3 }, "ReactivePower":{ "v":0, "u":"var", "d":1 }, "Efficiency":{ "v":95.24420929, "u":"%", "d":3 } } }, "DC":{ "0":{ "name":{ "u":"" }, "Power":{ "v":12.80000019, "u":"W", "d":1 }, "Voltage":{ "v":21.20000076, "u":"V", "d":1 }, "Current":{ "v":0.610000014, "u":"A", "d":2 }, "YieldDay":{ "v":31, "u":"Wh", "d":0 }, "YieldTotal":{ "v":1.710000038, "u":"kWh", "d":3 } }, "1":{ "name":{ "u":"" }, "Power":{ "v":26.60000038, "u":"W", "d":1 }, "Voltage":{ "v":43.20000076, "u":"V", "d":1 }, "Current":{ "v":0.610000014, "u":"A", "d":2 }, "YieldDay":{ "v":62, "u":"Wh", "d":0 }, "YieldTotal":{ "v":2.496000051, "u":"kWh", "d":3 } }, "2":{ "name":{ "u":"" }, "Power":{ "v":12.5, "u":"W", "d":1 }, "Voltage":{ "v":21.20000076, "u":"V", "d":1 }, "Current":{ "v":0.589999974, "u":"A", "d":2 }, "YieldDay":{ "v":26, "u":"Wh", "d":0 }, "YieldTotal":{ "v":1.179999948, "u":"kWh", "d":3 } }, "3":{ "name":{ "u":"" }, "Power":{ "v":25.89999962, "u":"W", "d":1 }, "Voltage":{ "v":43, "u":"V", "d":1 }, "Current":{ "v":0.600000024, "u":"A", "d":2 }, "YieldDay":{ "v":60, "u":"Wh", "d":0 }, "YieldTotal":{ "v":3.345999956, "u":"kWh", "d":3 } } }, "INV":{ "0":{ "Temperature":{ "v":12.5, "u":"°C", "d":1 } } }, "events":1 } ], "total":{ "Power":{ "v":74.09999847, "u":"W", "d":1 }, "YieldDay":{ "v":179, "u":"Wh", "d":0 }, "YieldTotal":{ "v":8.732000351, "u":"kWh", "d":3 } }, "hints":{ "time_sync":false, "radio_problem":false, "default_password":true } }"""