Zeitstempelberechnung

Einrichtung der openHAB Umgebung und allgemeine Konfigurationsthemen.

Moderatoren: seppy, udo1toni

Antworten
Boris
Beiträge: 14
Registriert: 19. Okt 2024 11:07
Answers: 0

Zeitstempelberechnung

Beitrag von Boris »

Hallo zusammen,
ich versuche mir aktuell eine Regel zu basteln, die mir zum Ende eines jeden Monats die erzeugte Energie zum Ende des letzten Monats berechnet. Die Regel funktioniert soweit auch schon ganz gut, und sieht wie folgt aus.

Code: Alles auswählen

var Number lastMonthValue = null // Variable, um den Wert des letzten Monats zu speichern

rule "PV Energy Differenz berechnen und E-Mail senden"
when
    Time cron "0 0 12 L * ?" // Jeden letzten Tag des Monats um 12 Uhr
then

    // Aktuellen Wert des Items abfragen
    val currentValue = PV_ges_Eltako_Energy.state as Number

    // Zeitstempel für den letzten Tag des Vormonats mit derselben Uhrzeit wie jetzt
    val lastDayOfLastMonthSameTime = now.minusMonths(1)
                                     .withDayOfMonth(now.minusMonths(1).dayOfMonth().maximumValue)
                                     .withTime(now.hourOfDay, now.minuteOfHour, now.secondOfMinute, 0)

    // Wert des Items zu diesem Zeitpunkt abrufen (InfluxDB wird verwendet)
    val historicState = PV_ges_Eltako_Energy.historicState(lastDayOfLastMonthSameTime, "influxdb")
    if (historicState === null) {
        logError("PV_Energy_Rule", "Kein historischer Wert für den letzten Tag des letzten Monats gefunden!")
        return
    }
    lastMonthValue = historicState.state as Number

    val currentValueInKWh = (currentValue.doubleValue() / 1000)
    val lastMonthValueInKWh = (lastMonthValue.doubleValue() / 1000)

    // Differenz berechnen
    val energyDifference = currentValueInKWh - lastMonthValueInKWh

    // E-Mail senden
    val emailBody = "Derzeugte Energie im letzten Monat beträgt: " + String.format("%.2f", energyDifference) + " kWh."
    val mailActions = getActions("mail","mail:smtp:websmtp")
    var success = mailActions.sendMail("xxx@xxx.com", "Monatlicher Energiebericht", emailBody)

    // Log-Eintrag zur Kontrolle
    logInfo("PV_Energy_Rule", "Monatlicher Energiebericht wurde erfolgreich gesendet. Differenz: " + String.format("%.2f", energyDifference) + " kWh")
end
Sobald ich den Zeitstempel so berechne, wird die Regel nicht mehr ausgeführt.

Code: Alles auswählen

    
    val lastDayOfLastMonthSameTime = now.minusMonths(1)
                                     .withDayOfMonth(now.minusMonths(1).dayOfMonth().maximumValue)
                                     .withTime(now.hourOfDay, now.minuteOfHour, now.secondOfMinute, 0)
Lasse ich die unteren zwei Zeile weg, wird sie ausgeführt.

Code: Alles auswählen

    val lastDayOfLastMonthSameTime = now.minusMonths(1)
Allerdings ist das ja falsch, weil ich nicht den Wert von vor einem Monat haben will, sondern vom letzten Tag des Vormonates zur selben Zeit. Der Tag kann ja variabel sein... 28., 30., 31.
Aber irgendwie scheint sich Openhab an der Syntax zu stören.
Jemand eine Idee?
Gruß,
Boris

Benutzeravatar
udo1toni
Beiträge: 15241
Registriert: 11. Apr 2018 18:05
Answers: 242
Wohnort: Darmstadt

Re: Zeitstempelberechnung

Beitrag von udo1toni »

Du suchst einen anderen Ausdruck. Den letzten Tag des Vormonats erhältst Du z.B. mit

Code: Alles auswählen

val lastdayOfLastMonth = now.withDayOfMonth(1).minusDays(1)
now liefert einen Zeitstempel mit Datum für "jetzt"
withDayOfMonth(1) liefert den Monatsersten.
minusDays(1) zieht einen Tag ab, womit wir beim Monatsletzten des Vormonats landen.

Je nachdem, welche Version von openHAB Du verwendest, könnte es gut sein, dass einer oder mehrere Ausdrücke veraltet (deprecated) sind, z.B. historicState heißt seit 4.2 persistedState.
openHAB4.3.3 stable in einem Debian-Container (bookworm) (Proxmox 8.3.5, LXC), mit openHABian eingerichtet

Boris
Beiträge: 14
Registriert: 19. Okt 2024 11:07
Answers: 0

Re: Zeitstempelberechnung

Beitrag von Boris »

Hi Udo,
megapfiffige Möglichkeit auf den letzten Tag des Vormonats zu kommen... manchmal ist es doch so einfach. :D
Meine Version OH ist 4.1.1 somit sollte ja noch er alte Befehl gelten.
Ich habe jedoch immer noch das Problem, dass bei Zugriff auf manche Variabeln das Script kommentarlos nicht mehr ausgeführt wird. Ist da irgendwas bekannt? Sollte ich auf 4.3 gehen?
Aktuell sieht der Script so aus.

Code: Alles auswählen

var Number lastMonthValuePV = null // Variable, um den Wert des letzten Monats für PV zu speichern
var Number lastMonthValueGridConsumption = null // Variable, um den Wert des letzten Monats für Grid Consumption zu speichern
var Number lastMonthValueGridFeedback = null // Variable, um den Wert des letzten Monats für Grid Feedback zu speichern

rule "PV Energy und Netz Differenzen berechnen und E-Mail senden"
when
    Time cron "0 0 12 L * ?" // Jeden Tag um 12 Uhr (UTC)
then
    // Aktuelle Werte der Items abrufen
    val currentValuePV = PV_ges_Eltako_Energy.state as Number
    val currentValueGridConsumption = EMCounter1_8_0.state as Number
    val currentValueGridFeedback = EMCounter2_8_0.state as Number

    // Zeitstempel für den letzten Tag des Vormonats mit derselben Uhrzeit wie jetzt
    val lastDayOfLastMonthSameTime = now.minusDays(1) // Erstmal zum testen, weil ich vor einem Monat noch keine Daten aufgezeichnet habe. Für nächsten Monat dann die auskommentierte Zeile nehmen!!!
// 1.1.2025 einkommentieren ==>>   val lastDayOfLastMonthSameTime = now.withDayOfMonth(1).minusDays(1)

    // Historische Werte der Items abrufen (InfluxDB wird verwendet)
    val historicStatePV = PV_ges_Eltako_Energy.historicState(lastDayOfLastMonthSameTime, "influxdb")
    val historicStateGridConsumption = EMCounter1_8_0.historicState(lastDayOfLastMonthSameTime, "influxdb")
    val historicStateGridFeedback = EMCounter2_8_0.historicState(lastDayOfLastMonthSameTime, "influxdb")

    if (historicStatePV === null || historicStateGridConsumption === null || historicStateGridFeedback === null) {
        logError("PV_Energy_Rule", "Kein historischer Wert für mindestens eines der Items gefunden!")
        return
    }

    // Historische Werte zuweisen
    lastMonthValuePV = historicStatePV.state as Number
    lastMonthValueGridConsumption = historicStateGridConsumption.state as Number
    lastMonthValueGridFeedback = historicStateGridFeedback.state as Number

    // Werte in kWh umrechnen, wenn nötig
    val currentValuePVInKWh = (currentValuePV.doubleValue() / 1000)
    val lastMonthValuePVInKWh = (lastMonthValuePV.doubleValue() / 1000)

    // Differenzen berechnen
    val energyDifferencePV = currentValuePVInKWh - lastMonthValuePVInKWh
    val energyDifferenceGridConsumption = currentValueGridConsumption - lastMonthValueGridConsumption
    val energyDifferenceGridFeedback = currentValueGridFeedback - lastMonthValueGridFeedback

//
    // Zusätzliche Berechnung: Unterschied der Differenzen zwischen PV und Grid Feedback
    val directPVConsumption = energyDifferencePV - energyDifferenceGridFeedback

    // E-Mail-Inhalt erstellen
    val emailBody = new StringBuilder()
    emailBody.append("Monatlicher Energiebericht:\n\n")
    emailBody.append("PV-Energie: " + String.format("%.2f", energyDifferencePV) + " kWh.\n")
    emailBody.append("Netzbezug: " + String.format("%.2f", energyDifferenceGridConsumption) + " kWh.\n")
    emailBody.append("Rückspeisung " + String.format("%.2f", energyDifferenceGridFeedback) + " kWh.\n")
    emailBody.append("Direktverbrauch: " + String.format("%.2f", directPVConsumption) + " kWh.\n")
    emailBody.append("\n")
    emailBody.append("Zählerstände heute\n")
    emailBody.append("PV-Zähler Eltako: " + String.format("%.2f", currentValuePVInKWh) + " kWh.\n")
/*
    emailBody.append("Hauptzähler 1.8.0: " + String.format("%.2f", currentValueGridConsumption) + " kWh.\n")
    emailBody.append("Hauptzähler 2.8.0: " + String.format("%.2f", currentValueGridFeedback) + " kWh.\n")
*/
    emailBody.append("\n")
    emailBody.append("Zählerstände letzter Monat\n")
    emailBody.append("PV-Zähler Eltako: " + String.format("%.2f", lastMonthValuePVInKWh) + " kWh.\n")
/*
    emailBody.append("Hauptzähler 1.8.0: " + String.format("%.2f", currentValueGridConsumption) + " kWh.\n")
    emailBody.append("Hauptzähler 2.8.0: " + String.format("%.2f", currentValueGridFeedback) + " kWh.\n")
*/
    // E-Mail senden
    val mailActions = getActions("mail", "mail:smtp:websmtp")
    var success = mailActions.sendMail("boris.henn@klotz-gangloff.com, boris.henn@t-online.de", "Monatlicher Energiebericht", emailBody.toString())

    // Log-Eintrag zur Kontrolle
    logInfo("PV_Energy_Rule", "Monatlicher Energiebericht wurde erfolgreich gesendet:\n" + emailBody.toString())
end
Wenn ich auch nur eine der Zeilen wie z.B.

Code: Alles auswählen

 emailBody.append("Hauptzähler 1.8.0: " + String.format("%.2f", currentValueGridConsumption) + " kWh.\n")
einkommentiere läuft der Script nicht mehr... ich starte den Script aktuell mit der Funktion "Run now" in der GUI. Will ja nicht immer einen Monat warten... :roll:

Benutzeravatar
udo1toni
Beiträge: 15241
Registriert: 11. Apr 2018 18:05
Answers: 242
Wohnort: Darmstadt

Re: Zeitstempelberechnung

Beitrag von udo1toni »

Was ist EMCounter1_8_0, EMCounter2_8_0 und PV_ges_Eltako_Energy für ein Itemtyp?

openHAB verwendet inzwischen an vielen Stellen bevorzugt UoM Items (also solche, die die Einheit mitführen) und je nachdem, was Du mit den Werten anstellst, kann diese Einheit da Probleme bereiten.

Aber zunächst mal ist festzustellen, dass Du Dir vermutlich zu viel Arbeit machst. Du möchtest den Konsum bzw. die Einspeisung über den aktuellen Monat bestimmen. Die Items enthalten jeweils einen Zählerstand, und Du bestimmst den Zählerstand zum Beginn des Zeitraums und ziehst diesen vom aktuellen Zählerstand ab. Soweit, so gut, aber die Persistence kann das auch selbst. Statt

Code: Alles auswählen

    val lastDayOfLastMonthSameTime      = now.withDayOfMonth(1).minusDays(1)
    val currentValueGridConsumption     = EMCounter1_8_0.state as Number
    val historicStateGridConsumption    = EMCounter1_8_0.historicState(lastDayOfLastMonthSameTime, "influxdb")
          lastMonthValueGridConsumption = historicStateGridConsumption.state as Number
    val energyDifferenceGridConsumption = currentValueGridConsumption - lastMonthValueGridConsumption
wäre

Code: Alles auswählen

    val lastDayOfLastMonthSameTime      = now.withDayOfMonth(1).minusDays(1)
    val energyDifferenceGridConsumption = EMCounter1_8_0.deltaSince(lastDayOfLastMonthSameTime,"influxdb")
besser. Natürlich muss auch hier sichergestellt werden, dass ein gültiger Wert geliefert wird :) aber Du benötigst nur eine Zeile statt vier Zeilen (den Zeitstempel zu berechnen mal nicht mitgezählt). Natürlich benötigst Du den Zählerstand vom Vormonat zusätzlich, wenn Du diesen mit ausgeben willst. Aber auc hda böte es sich an, den direkt abzufragen:

Code: Alles auswählen

lastMonthValueGridConsumption = EMCounter1_8_0.historicState(lastDayOfLastMonthSameTime, "influxdb").state 
und diesen Wert auf instanceof Number zu prüfen. Das müsstest Du streng genommen für jeden Einzelwert machen...

Dein Ausstieg aus der Rule ist übrigens fehlerhaft. return muss zwingend mit einem ; abgeschlossen werden. Dies ist der einzige Befehl in der DSL, bei dem das so ist (das liegt daran, dass return einen Rückgabewert liefert. Das wäre der Wert hinter return, womit dann evtl. späterer Code falsch interpretiert wird. Dass dies hier folgenlos bleibt, liegt mutmaßlich an der Klammerung. Gibt man einen Rückgabewert an, wird der Code ebenfalls einen Fehler verursachen, weil die aufrufende Routine nicht mit einem Rückgabewert rechnet. Das Semikolon erzwingt einen null-String als Rückgabewert.)
openHAB4.3.3 stable in einem Debian-Container (bookworm) (Proxmox 8.3.5, LXC), mit openHABian eingerichtet

Boris
Beiträge: 14
Registriert: 19. Okt 2024 11:07
Answers: 0

Re: Zeitstempelberechnung

Beitrag von Boris »

Hallo und ein frohes Neues, Udo,
die Items sind wie folgt deklariert.

Code: Alles auswählen

Number EMCounter1_8_0              "Zähler 1.8.0 Bezug [%.0f kWh]"         <energy> (emPower)
Number EMCounter2_8_0              "Zähler 2.8.0 Rückspeisung [%.0f kWh]"  <energy> (emPower)
Group:Number:Energy:SUM PV_ges_Eltako_Energy "Gesamt Eltako Zählerstand" (Eltako)  ["Measurement","Energy"] {unit="Wh", stateDescription=""[pattern="%.0f kWh"]}
Die EMCounter werdenmit den Rohdaten der tatsächlichen Zählerstände, plus einem Offset gefüttert.

Vielen Dank für deine Vorschläge zur Code-Verbesserung. Die Zählerstände selbst, also auch die von Vormonat, wollte ich nur zu Debugzwecken ausgeben. Mich interessieren am Ende nur die Deltas.
Gruß,
Boris

Benutzeravatar
udo1toni
Beiträge: 15241
Registriert: 11. Apr 2018 18:05
Answers: 242
Wohnort: Darmstadt

Re: Zeitstempelberechnung

Beitrag von udo1toni »

Ohne Anspruch auf Korrektheit "meine" Version Deines Codes:

Code: Alles auswählen

rule "PV Energy und Netz Differenzen berechnen und E-Mail senden"
when
    Time cron "0 0 12 L * ?" // Am Monatsletzten um 12 Uhr (Systemzeit!)
then
    // Zeitstempel für den letzten Tag des Vormonats mit derselben Uhrzeit wie jetzt
    val dtStart = now.withDayOfMonth(1).minusDays(1)

    // Variablen definieren
    var Number nCurPV           = null
    var Number nCurGridImport   = null
    var Number nCurGridExport   = null
    var Number nDeltaPV         = null
    var Number nDeltaGridImport = null
    var Number nDeltaGridExport = null
    var Number nInternalPV      = null

    // Aktuelle Werte der Items abrufen
    if(PV_ges_Eltako_Energy.state instanceof Number)
        nCurPV = (PV_ges_Eltako_Energy.state as Number).floatValue / 1000
    if(EMCounter1_8_0.state instanceof Number)
        nCurGridImport = (EMCounter1_8_0.state as Number).floatValue
    if(EMCounter2_8_0.state instanceof Number)
        nCurGridExport = (EMCounter2_8_0.state as Number).floatValue

    // Delta der Items abrufen
    if(PV_ges_Eltako_Energy.deltaSince(dtStart,"influxdb") instanceof Number)
        nDeltaPV = (PV_ges_Eltako_Energy.deltaSince(dtStart,"influxdb") as Number).floatValue / 1000
    if(EMCounter1_8_0.deltaSince(dtStart,"influxdb") instanceof Number)
        nDeltaGridImport = (EMCounter1_8_0.deltaSince(dtStart,"influxdb") as Number).floatValue
    if(EMCounter2_8_0.deltaSince(dtStart,"influxdb") instanceof Number)
        nDeltaGridExport = (EMCounter2_8_0.deltaSince(dtStart,"influxdb") as Number).floatValue

    // Eigenverbrauch bestimmen
    if(nDeltaPV !== null && nDeltaGridExport !== null)
        nInternalPV = nDeltaPV - nDeltaGridExport

    // E-Mail-Inhalt erstellen
    val emailBody = new StringBuilder()
    emailBody.append("Monatlicher Energiebericht:\n\nPV-Energie: ")
    emailBody.append(if(nDeltaPV !== null) String.format("%.2f", nDeltaPV) else "N.N.")
    emailBody.append(" kWh.\nNetzbezug: ")
    emailBody.append(if(nDeltaGridImport !== null) String.format("%.2f", nDeltaGridImport) else "N.N.")
    emailBody.append(" kWh.\nEinspeisung ")
    emailBody.append(if(nDeltaGridExport !== null) String.format("%.2f", nDeltaGridExport) else "N.N.")
    emailBody.append(" kWh.\nEigenverbrauch: ")
    emailBody.append(if(nDeltaGridImport !== null) String.format("%.2f", nInternalPV) else "N.N.")
    emailBody.append("kWh.\n\nZählerstände heute\nPV-Zähler Eltako: ")
    emailBody.append(if(nCurPV !== null) String.format("%.2f", nCurPV) else "N.N.")
    emailBody.append(" kWh.\nHauptzähler 1.8.0: ")
    emailBody.append(if(nDCurGridImport !== null) String.format("%.2f", nCurGridImport) else "N.N.")
    emailBody.append(" kWh.\nHauptzähler 2.8.0: ")
    emailBody.append(if(nCurGridExport !== null) String.format("%.2f", nCurGridExport) else "N.N.")
    emailBody.append(" kWh.\n\nN.N.: Wert wurde nicht geliefert.")

    // E-Mail senden
    val mailActions = getActions("mail", "mail:smtp:websmtp")
    val bSuccess = mailActions.sendMail("boris.henn@klotz-gangloff.com, boris.henn@t-online.de", "Monatlicher Energiebericht", emailBody.toString)

    // Log-Eintrag zur Kontrolle
    logInfo("pvEnergy", "Monatlicher Energiebericht wurde {}erfolgreich gesendet:\n{}",if(bSuccess != true) "nicht " else "" , emailBody.toString())
end
Die wichtigsten Unterschiede zu Deiner Rule: alle Werte werden systematisch auf Gültigkeit geprüft (instanceof Number). Alle Werte werden als reine Zahlen betrachtet - ohne Einheiten (.floatValue). Die Mail wird immer versendet, auch wenn einzelne oder gar alle Werte fehlen. Fehlende Werte werden als N.N. gekennzeichnet.
Wenn man die Rückmeldung der Action sendMail() schon speichert, kann man diese auch auswerten :)
Ansonsten bevorzuge ich kürzere Variablennamen, wie man sehen kann :) Das ist natürlich Geschmacksache.
Die Number Variablen müssen stark definiert werden, damit die Zuweisung von null den Datentyp nicht verändern kann.
Bei den logX() Befehlen verweise ich immer gerne darauf, dass der erste String der letzte Teil des Loggernamens ist. Für alle Rules gilt, dass der Loggername mit "org.openhab.core.model.script." beginnt. Dieser Teilstring ist eindeutig. Es ist also unnötig, noch extra ein "Rules" anzuhängen. Und auch hier gilt die Empfehlung, camelCase Schreibweise zu nutzen, wobei das erste Wort klein geschrieben wird. Entsprechend wäre pvEnergy der empfohlene Teilstring.
openHAB4.3.3 stable in einem Debian-Container (bookworm) (Proxmox 8.3.5, LXC), mit openHABian eingerichtet

Antworten