SolarManager unter Versionsverwaltung

Erster Stand der Hintergrundprozesse, die auf der Synology unter
/volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler
je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der
AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins
Web-Verzeichnis.

Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die
nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie
von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot),
zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das
Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat.

Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py,
gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso
gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte.

Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein
pip, sie muessen neben den Skripten liegen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 20:46:59 +02:00
co-authored by Claude Opus 5
commit 79843aa2ae
968 changed files with 261182 additions and 0 deletions
+561
View File
@@ -0,0 +1,561 @@
import asyncio
import json
import aiohttp
import sys
import logging
import sunspec2.modbus.client as client
import datetime
from typing import List
from dataclasses import dataclass
from dataclasses import field
from operator import attrgetter
from time import sleep
import traceback
_LOGGER = logging.getLogger(__name__)
batStat = {
1: "OFF",
2: "EMPTY",
3: "DISCHARGING",
4: "CHARGING",
5: "FULL",
6: "HOLDING",
7: "TESTING",
}
wrStat = {
1: "OFF",
2: "SLEEPING",
3: "STARTING",
4: "MPPT",
5: "THROTTLED",
6: "SHUTTING_DOWN",
7: "FAULT",
}
@dataclass
class ModbusRegisters:
ppv0_reg = ""
ppv1_reg = ""
pbat_crg_reg = ""
pbat_discrg_reg = ""
ubat_reg = ""
ibat_crg_reg = ""
ibat_discrg_reg = ""
pACinv_reg = ""
soc_reg = ""
battStat_reg = ""
wrStat_reg = ""
pACevu_reg = ""
pL1ACevu_reg = ""
pL2ACevu_reg = ""
pL3ACevu_reg = ""
iL1evu_reg = ""
iL2evu_reg = ""
iL3evu_reg = ""
pACog_reg = ""
pL1ACog_reg = ""
pL2ACog_reg = ""
pL3ACog_reg = ""
iL1og_reg = ""
iL2og_reg = ""
iL3og_reg = ""
limitChrgRate = ""
setChargeLimits = ""
setPowerLimitPercent = ""
setPowerLimitEN = ""
@dataclass
class InverterData:
p_PV:List[float] = field(default_factory=lambda: [0.0,0.0])
error:int = 0
load: float = 0.0
soc: float = 0.0
pbat: float = 0.0
ppv: float = 0.0
ppv2: float = 0.0
aut: float = 0.0
pgrid: float = 0.0
ibatt: float = 0.0
ubatt: float = 0.0
temp: float = 0.0
p_l1evu: float = 0.0
p_l2evu: float = 0.0
p_l3evu: float = 0.0
p_l1og: float = 0.0
p_l2og: float = 0.0
p_l3og: float = 0.0
i_l1evu: float = 0.0
i_l2evu: float = 0.0
i_l3evu: float = 0.0
i_l1og: float = 0.0
i_l2og: float = 0.0
i_l3og : float = 0.0
p_wr : float = 0.0
crgMaxPct : float = 0.0
wr_status : str = ""
batt_status : str = ""
pwrMaxPct : float = 0.0
p_AC : float = 0.0
@dataclass
class estimation:
estProd = 0.1
increaseTmr = 0
try:
inv = client.SunSpecModbusClientDeviceTCP(1, "192.168.179.155", 1502)
meter0 = client.SunSpecModbusClientDeviceTCP(200, "192.168.179.155", 1502)
meter1 = client.SunSpecModbusClientDeviceTCP(201, "192.168.179.155", 1502)
regs = ModbusRegisters()
estimations = estimation()
except client.SunSpecModbusClientError as e:
_LOGGER.error('Modbus TCP Error: %s' % e)
_LOGGER.erorr('trace: %s' % traceback.print_exc())
sys.exit(1)
async def connect():
if inv is not None and meter0 is not None and meter1 is not None:
print('Scanning: ')
# print( '\nTimestamp: %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())))
# read all models in the device
inv.scan()
meter0.scan()
meter1.scan()
#_LOGGER.debug(inv.get_text())
if(inv.common[0].Md.value == "Symo GEN24 10.0"):
regs.ppv0_reg = "inv.mppt[0].module[0].DCW"
regs.ppv1_reg = "inv.mppt[0].module[1].DCW"
regs.pbat_crg_reg = "inv.mppt[0].module[2].DCW"
regs.pbat_discrg_reg = "inv.mppt[0].module[3].DCW"
regs.ubat_reg = "inv.mppt[0].module[3].DCV"
regs.ibat_crg_reg = "inv.mppt[0].module[2].DCA"
regs.ibat_discrg_reg = "inv.mppt[0].module[3].DCA"
regs.pACinv_reg = "inv.inverter_three_phase[0].W"
regs.tmpCab_reg = "inv.inverter_three_phase[0].TmpCab"
regs.soc_reg = "inv.storage_basic[0].ChaState"
regs.battStat_reg = "inv.storage_basic[0].ChaSt"
regs.wrStat_reg = "inv.inverter_three_phase[0].St"
regs.limitChrgRate = "inv.storage_basic[0].InWRte"
regs.setChargeLimits = "inv.storage_basic[0].StorCtl_Mod"
regs.setPowerLimitPercent = "inv.controls[0].WMaxLimPct"
regs.setPowerLimitEN = "inv.controls[0].WMaxLim_Ena"
regs.pACevu_reg = "meter0.ac_meter_abcn[0].W"
regs.pL1ACevu_reg = "meter0.ac_meter_abcn[0].WphA"
regs.pL2ACevu_reg = "meter0.ac_meter_abcn[0].WphB"
regs.pL3ACevu_reg = "meter0.ac_meter_abcn[0].WphC"
regs.iL1evu_reg = "meter0.ac_meter_abcn[0].AphA"
regs.iL2evu_reg = "meter0.ac_meter_abcn[0].AphB"
regs.iL3evu_reg = "meter0.ac_meter_abcn[0].AphC"
regs.pACog_reg = "meter1.ac_meter_abcn[0].W"
regs.pL1ACog_reg = "meter1.ac_meter_abcn[0].WphA"
regs.pL2ACog_reg = "meter1.ac_meter_abcn[0].WphB"
regs.pL3ACog_reg = "meter1.ac_meter_abcn[0].WphC"
regs.iL1og_reg = "meter1.ac_meter_abcn[0].AphA"
regs.iL2og_reg = "meter1.ac_meter_abcn[0].AphB"
regs.iL3og_reg = "meter1.ac_meter_abcn[0].AphC"
else:
_LOGGER.error('undefined Inverter, please define inverter registers!')
sys.exit(1)
else:
_LOGGER.error('One of the expected modbus devices was unreachable')
sys.exit(1)
#while True:
async def get_runtime_data(estProduction, sunset) -> InverterData:
ret = InverterData()
try:
inv.mppt[0].read()
inv.inverter_three_phase[0].read()
inv.storage_basic[0].read()
meter0.ac_meter_abcn[0].read()
meter1.ac_meter_abcn[0].read()
#print(inv.controls[0].__dir__())
ret.p_PV[0] = eval(regs.ppv0_reg).cvalue
ret.p_PV[1] = eval(regs.ppv1_reg).cvalue
ret.error = 0
ret.load = eval(regs.pACevu_reg).cvalue + eval(regs.pbat_discrg_reg).cvalue + eval(regs.ppv0_reg).cvalue + eval(regs.ppv1_reg).cvalue
ret.soc = eval(regs.soc_reg).cvalue
ret.pbat = eval(regs.pbat_discrg_reg).cvalue
if(ret.pbat == 0):
ret.pbat = -eval(regs.pbat_crg_reg).cvalue
ret.crgMaxPct = (eval(regs.limitChrgRate).cvalue)*2.04 #references max charge of batt. (20,48kw) instead of INV (10kw)
ret.pwrMaxPct = eval(regs.setPowerLimitPercent).cvalue #references max charge of batt. (20,48kw) instead of INV (10kw)
ret.ppv = ret.p_PV[0]
ret.ppv2 = ret.p_PV[1]
ret.pgrid = eval(regs.pACevu_reg).cvalue
if(ret.pgrid < 0):
ret.aut = 100
else:
ret.aut = 100/ret.load * ret.pgrid
if(ret.aut < 0):
ret.aut=0
elif(ret.aut > 100):
ret.aut = 100
ret.ibatt = eval(regs.ibat_discrg_reg).cvalue
if(ret.ibatt == 0):
ret.ibatt = -eval(regs.ibat_crg_reg).cvalue
ret.ubatt = eval(regs.ubat_reg).cvalue
ret.p_l1evu = eval(regs.pL1ACevu_reg).cvalue
ret.p_l2evu = eval(regs.pL2ACevu_reg).cvalue
ret.p_l3evu = eval(regs.pL3ACevu_reg).cvalue
ret.p_l1og = eval(regs.pL1ACog_reg).cvalue
ret.p_l2og = eval(regs.pL2ACog_reg).cvalue
ret.p_l3og = eval(regs.pL3ACog_reg).cvalue
ret.i_l1evu = eval(regs.iL1evu_reg).cvalue
ret.i_l2evu = eval(regs.iL2evu_reg).cvalue
ret.i_l3evu = eval(regs.iL3evu_reg).cvalue
ret.i_l1og = eval(regs.iL1og_reg).cvalue
ret.i_l2og = eval(regs.iL2og_reg).cvalue
ret.i_l3og = eval(regs.iL3og_reg).cvalue
ret.p_AC = eval(regs.pACinv_reg).cvalue
ret.p_wr = ret.ppv + ret.ppv2 + ret.pbat - ret.p_AC
ret.temp = 0#no need to read reg, reads always null ... eval(regs.tmpCab_reg).cvalue
ret.batt_status = batStat[eval(regs.battStat_reg).cvalue]
ret.wr_status = wrStat[eval(regs.wrStat_reg).cvalue]
except Exception as error:
_LOGGER.warning(f"No modbus data received from Inverter: %s",error)
#_LOGGER.erorr('trace: %s' % traceback.print_exc())
try:
inv.connect() #reconnect the modbus client
meter0.connect() #reconnect the modbus client
meter1.connect() #reconnect the modbus client
_LOGGER.warning(f"Tried to reconnect")
except:
_LOGGER.warning(f"reconnect failed...")
# if(ret.soc > 90 and (ret.i_l1evu > 22 or ret.i_l2evu > 22 or ret.i_l3evu > 22)):
# eval(regs.setPowerLimitPercent).value = ret.pwrMaxPct*100 - 500; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV
# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2
# eval(regs.limitChrgRate).write()
# eval(regs.setChargeLimits).write()
# elif (ret.i_l1evu < 18 and ret.i_l2evu < 18 and ret.i_l3evu < 18 and ret.pwrMaxPct < 100):
# eval(regs.setPowerLimitPercent).value = ret.pwrMaxPct*100 + 500; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV
# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2
# eval(regs.limitChrgRate).write()
# eval(regs.setChargeLimits).write()
# if datetime.datetime.now().hour == 23 and ret.pwrMaxPct < 99: #reset power limit every night
# eval(regs.setPowerLimitPercent).value = 10000; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV
# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2
# eval(regs.limitChrgRate).write()
# eval(regs.setChargeLimits).write()
try:
if(estProduction != estimations.estProd and datetime.datetime.now().hour < 9):
estimations.estProd = estProduction
estimations.increaseTmr = 0
if(estimations.estProd > 35):
#_LOGGER.debug("High yeld predicted, reducing battery charge current")
if ret.soc > 45: #if SOC in the morning is above 45% wait with charging until midday
eval(regs.limitChrgRate).value = int(500/2.04) #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV
else: #if less than 45% SOC in the morning charge slow (SOC dependant) until midday
eval(regs.limitChrgRate).value = int((5000 - ret.soc*100)/2.04) #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV
eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2
eval(regs.limitChrgRate).write()
eval(regs.setChargeLimits).write()
elif ret.crgMaxPct < 70:
eval(regs.limitChrgRate).value = int(8000/2.04) #set charge rate to 80% if no high yeld is predicted
eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2
eval(regs.limitChrgRate).write()
eval(regs.setChargeLimits).write()
elif datetime.datetime.now().hour == 11 or datetime.datetime.now().hour == 12: #inclease charge rate at midday
estimations.increaseTmr = estimations.increaseTmr + 1
if estimations.increaseTmr > 300: #slowdown rate change to 300 * 3sec (15 min.)
estimations.increaseTmr = 0
maxMiddDayChrg = 30
if(int(sunset.strftime('%H')) < 18): #inclrease max charge rate during wintertime, where time to full SOC is less.
maxMiddDayChrg = 70
if ret.crgMaxPct < maxMiddDayChrg:
estimations.increaseTmr = 0
eval(regs.limitChrgRate).value = int((ret.crgMaxPct*100+500)/2.04) #increase by 5% every 15 min --> reach 30% after 5*15min max
eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2
eval(regs.limitChrgRate).write()
eval(regs.setChargeLimits).write()
_LOGGER.debug("Increasing charge rate to buffer feed peaks to " + str(int((ret.crgMaxPct*100+200)/2.04)) + "%")
if datetime.datetime.now().hour >= int(sunset.strftime('%H'))-4 and ret.soc < 70 and ret.crgMaxPct < 90:
eval(regs.limitChrgRate).value = int((10000)/2.04)
eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2
eval(regs.limitChrgRate).write()
eval(regs.setChargeLimits).write()
_LOGGER.debug("Increasing charge rate because of too low SOC " + str(int((10000)/2.04)) + "%")
return ret
#print(ret.p_wr)
#print("-----")
#sleep(1)
except Exception as error:
_LOGGER.warning(f"No modbus data received from Inverter: %s",error)
"""
Model: common (1)
ID 1
L 65
Mn Fronius
Md Symo GEN24 10.0
Opt None
Vr 1.34.2-1
SN 33095286
DA 1
Model: inverter_three_phase (103)
ID 103
L 50
A 22300 A
AphA 8115 A
AphB 7527 A
AphC 6658 A
A_SF -5
PPVphAB 4000 V
PPVphBC 4015 V
PPVphCA 4033 V
PhVphA 2310 V
PhVphB 2317 V
PhVphC 2328 V
V_SF -1
W -5145 W
W_SF -2
Hz 4998 Hz
Hz_SF -2
VA 5145 VA
VA_SF -2
VAr 31275 var
VAr_SF -5
PF 1000 Pct
PF_SF -1
WH 3939319098 Wh
WH_SF -2
DCA None A
DCA_SF None
DCV None V
DCV_SF None
DCW 25076 W
DCW_SF -5
TmpCab 526 C
TmpSnk None C
TmpTrns None C
TmpOt None C
Tmp_SF -1
St 4
StVnd 4
Evt1 0
Evt2 0
EvtVnd1 0
EvtVnd2 0
EvtVnd3 None
EvtVnd4 None
Model: nameplate (120)
ID 120
L 26
DERTyp 82
WRtg 1000 W
WRtg_SF 1
VARtg 1000 VA
VARtg_SF 1
VArRtgQ1 714 var
VArRtgQ2 714 var
VArRtgQ3 -714 var
VArRtgQ4 -714 var
VArRtg_SF 1
ARtg 1443 A
ARtg_SF -2
PFRtgQ1 -700 cos()
PFRtgQ2 700 cos()
PFRtgQ3 -700 cos()
PFRtgQ4 700 cos()
PFRtg_SF -3
WHRtg 22118 Wh
WHRtg_SF 0
AhrRtg None AH
AhrRtg_SF None
MaxChaRte 20480 W
MaxChaRte_SF 0
MaxDisChaRte 20480 W
MaxDisChaRte_SF 0
Pad 32768
Model: settings (121)
ID 121
L 30
WMax 1000 W
VRef 231 V
VRefOfs 0 V
VMax None V
VMin None V
VAMax 1000 VA
VArMaxQ1 714 var
VArMaxQ2 714 var
VArMaxQ3 -714 var
VArMaxQ4 -714 var
WGra None % WMax/sec
PFMinQ1 -700 cos()
PFMinQ2 700 cos()
PFMinQ3 -700 cos()
PFMinQ4 700 cos()
VArAct None
ClcTotVA None
MaxRmpRte None % WGra
ECPNomHz None Hz
ConnPh None
WMax_SF 1
VRef_SF 0
VRefOfs_SF 0
VMinMax_SF None
VAMax_SF 1
VArMax_SF 1
WGra_SF None
PFMin_SF -3
MaxRmpRte_SF None
ECPNomHz_SF None
Model: status (122)
ID 122
L 44
PVConn 7
StorConn 7
ECPConn 1
ActWh 39393190 Wh
ActVAh None VAh
ActVArhQ1 None varh
ActVArhQ2 None varh
ActVArhQ3 None varh
ActVArhQ4 None varh
VArAval None var
VArAval_SF None
WAval None var
WAval_SF None
StSetLimMsk None
StActCtl 0
TmSrc RTC
Tms 791196551 Secs
RtSt None
Ris None ohms
Ris_SF None
Model: controls (123)
ID 123
L 24
Conn_WinTms 0 Secs
Conn_RvrtTms 0 Secs
Conn 1
WMaxLimPct 10000 % WMax
WMaxLimPct_WinTms 0 Secs
WMaxLimPct_RvrtTms 0 Secs
WMaxLimPct_RmpTms 0 Secs
WMaxLim_Ena 0
OutPFSet 1000 cos()
OutPFSet_WinTms 0 Secs
OutPFSet_RvrtTms 0 Secs
OutPFSet_RmpTms 0 Secs
OutPFSet_Ena 0
VArWMaxPct None % WMax
VArMaxPct 100 % VArMax
VArAvalPct None % VArAval
VArPct_WinTms 0 Secs
VArPct_RvrtTms 0 Secs
VArPct_RmpTms 0 Secs
VArPct_Mod 2
VArPct_Ena 0
WMaxLimPct_SF -2
OutPFSet_SF -3
VArPct_SF 0
Model: mppt (160)
ID 160
L 88
DCA_SF -4
DCV_SF -2
DCW_SF -1
DCWH_SF -2
Evt None
N 4
TmsPer None
01:ID 1
01:IDStr MPPT 1
01:DCA 22696 A
01:DCV 55400 V
01:DCW 12574 W
01:DCWH 3156686802 Wh
01:Tms 791196551 Secs
01:Tmp None C
01:DCSt None
01:DCEvt None
02:ID 2
02:IDStr MPPT 2
02:DCA 15510 A
02:DCV 32689 V
02:DCW 5070 W
02:DCWH 989224180 Wh
02:Tms 791196551 Secs
02:Tmp None C
02:DCSt None
02:DCEvt None
03:ID 3
03:IDStr StCha 3
03:DCA 42263 A
03:DCV 41721 V
03:DCW 17641 W
03:DCWH 877835025 Wh
03:Tms 791196551 Secs
03:Tmp None C
03:DCSt None
03:DCEvt None
04:ID 4
04:IDStr StDisCha 4
04:DCA 0 A
04:DCV 41721 V
04:DCW 0 W
04:DCWH 824803735 Wh
04:Tms 791196551 Secs
04:Tmp None C
04:DCSt None
04:DCEvt None
Model: storage_basic (124)
ID 124
L 24
WChaMax 20480 W
WChaGra 100 % WChaMax/sec
WDisChaGra 100 % WChaMax/sec
StorCtl_Mod 0
VAChaMax None VA
MinRsvPct 0 % WChaMax
ChaState 1240 % AhrRtg
StorAval None AH
InBatV None V
ChaSt 4
OutWRte 10000 % WDisChaMax
InWRte 10000 % WChaMax
InOutWRte_WinTms None Secs
InOutWRte_RvrtTms 0 Secs
InOutWRte_RmpTms None Secs
ChaGriSet 1
WChaMax_SF 0
WChaDisChaGra_SF 0
VAChaMax_SF None
MinRsvPct_SF -2
ChaState_SF -2
StorAval_SF None
InBatV_SF None
InOutWRte_SF -2
"""