Script friert ein /stürzt ab auf Raspberry

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

hi,

bestimmt gibts die Frage schon 100 mal, hab aber nichts passendes gefunden.

ich ärgere mich schon jetzt einige Tage damit rum, dass ein Script für eine sog. "Nulleinspeisung" -> es geht um PV, immer wieder hängen bleibt.
System ist ein Raspi 3 mit 4 cores
Python ist 3.5.x

Das script ist als Service unit gestartet damit es im hintergrund läuft.
nun bleibt das teil einfach ohne Meldung oder Fehler sporadisch stehen. Die Unit crasht NICHT. also Status ist weiterhin auf "Active"
Auch das Log über Journalctrl vom Service weist keinerlei Fehler aus.
Habe auch an sämtlichen Try und except stellen über Logging in eine Datei geschrieben. Auch hier ist zum Ausfallzeitpunkt nichts eingetragen

Was und wie kann ich noch was prüfen?

Besten Dank im Vorraus!

Martin
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Hallo Martin,
da bleibt dir nur gutes altes Fehlersuchen und einkreisen, denn aus der recht oberflächlichen Beschreibung lässt sich leider nicht viel sagen.

Grundsätzlich ein paar Denk anstoße:
Python 3.5 ist seit Oktober 2020 im Status End of Life , aber das wird nicht der Fehler sein.
Wie stellst du fest, dass dein Script nicht läuft?
Wie loggst du ins journalctl, kannst du vielleicht das Script mit mehr oder Debug Meldungen starten?
Ggf. hilft es auch das Script von Hand in einer Konsole oder sogar einen Debugger zu starten, um mehr Informationen zum Event Zeitpunkt zu bekommen.
Kannst du ein Testaufbau, erzeugen der Deterministisch zu dem Fehlerbild führt.
Wenn du einen reproduzierbaren Testcase für den Fehler hast, kannst du ihn so weit vereinfachen, dass du ihn hier im Forum posten kannst und das Fehlerbild ist immer noch das Gleiche?
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

Hallo imonbln,

- Ich könnte die Version mal updaten, ja. Habe es auch auf einem zweiten Pi probiert, da läuft 3.7.x -> genau das selbe Problem dort.
- Wie stelle ich das fest? Wenn ich Journalctrl vom Service aufrufe ist der letzte Eintrag mehr als die Zykluszeit (alle 5Sec) alt. Und natürlich funktioniert meine Einspeisung nicht.
- wie ich logge? -> mit den Print befehlen
- kannst du einem Debugger empfehlen? Gibts was für windows?
- Verstehe die Frage nicht
- soll ich den Code posten?

Ich hab mir seit gestern jetzt so geholfen:
Über ein kleine SH Script was per CronTab gestartet wird, prüfe ich, ob die Ausgabe von Journalctrl von der Service Unit älter als 10 Sekunden ist. Wenn ja mache ich auf die Service Unit einen restart. Somit habe zumindestens eine zuverlässige Anlage. Zufrieden stellt mich das jedoch in keinster Weise, wenn das Programm an sich immer ausfällt.

grüße
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Code würde schon helfen. Beim debugger bin ich mir weniger sicher. Hast du durch Print-debugging schon die Zeile identifiziert, die hängt?
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Code würde helfen, allerdings sinkt die Motivation bei ein paar 1000 Codezeilen stark ab, das zu lesen und zu verstehen.

Wichtig ist es, die Zeile zu finden, an welcher dein Python Script hängt. Dazu kannst du dein Skript voll mit Debugausgaben schreiben, um den Fehler weiter einzukreisen. Statt des Print Debugging, solltest du vielleicht auf einen Python Logger umsteigen, der Vorteil ist, dass viele Python Module selbst eine Logger Instanz verwenden und bei entsprechender Parametrierung eine Menge Debuginformationen Preis geben.

Einen guten Debugger kann ich dir nicht empfehlen und ich würde auch nicht auf Windows arbeiten wollen, das andere Betriebssystem kann Auswirkungen auf das Fehlerbild haben. Daher würde ich an deiner Stelle das Problem auf den Rasperry Pi untersuchen. Was aber helfen kann ist, wenn du das Programm nicht als Service laufen lässt, sondern in der Konsole (aka Terminal) vielleicht indem du Gnu/Screen oder Tmux auf den Rasperry Pi installiert, vielleicht siehst du im Terminal mehr als im journalctl.

Kannst du ein Testaufbau, erzeugen der Deterministisch zu dem Fehlerbild führt, hiermit meine ich kannst du eine einfache Schritt-für-Schritt-Anleitung schreiben, um den Fehler zu provozieren.
Also hypothetisch: Starte den Raspberry, warte 2 Stunden, steck einen USB-Stick an, drücke Reset und dann kommt der Fehler oder wenn auf GPIO-X ein High Signal anliegt und gleichzeitig und dann auf GPIO-Y eine low Flanke kommt, dann hängt mein Programm. Eben irgendeine Handlungsanweisung um diesen Fehler zu triggern.
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

__deets__ hat geschrieben: Montag 10. Juli 2023, 14:16 Der Code würde schon helfen. Beim debugger bin ich mir weniger sicher. Hast du durch Print-debugging schon die Zeile identifiziert, die hängt?
Hi, ne leider nicht. Das Print out gibt einfach keine Meldung und bleibt einfach nur stehen.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie imonbln schon sagte: im Zweifel durch viele viele Prints genau den Ort eingrenzen. Oder gibt es Rückrufaktionen, die nicht kommen?

Der Code würde da durchaus Klarheit bringen…
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

imonbln hat geschrieben: Montag 10. Juli 2023, 14:53 Code würde helfen, allerdings sinkt die Motivation bei ein paar 1000 Codezeilen stark ab, das zu lesen und zu verstehen.

Wichtig ist es, die Zeile zu finden, an welcher dein Python Script hängt. Dazu kannst du dein Skript voll mit Debugausgaben schreiben, um den Fehler weiter einzukreisen. Statt des Print Debugging, solltest du vielleicht auf einen Python Logger umsteigen, der Vorteil ist, dass viele Python Module selbst eine Logger Instanz verwenden und bei entsprechender Parametrierung eine Menge Debuginformationen Preis geben.
nagut, es sind nicht mal 1000.
wie ich oben erwähnt habe, benutze ich parallel ja bereits logger und schreibe eine Datei mit. Die Sagt mir zum aufallzeitpunkt auch genau gar nix. Oder ich muss auch die "Info" ebene wieder aktivieren. Habe nur die Warning aktiv.
Einen guten Debugger kann ich dir nicht empfehlen und ich würde auch nicht auf Windows arbeiten wollen, das andere Betriebssystem kann Auswirkungen auf das Fehlerbild haben. Daher würde ich an deiner Stelle das Problem auf den Rasperry Pi untersuchen. Was aber helfen kann ist, wenn du das Programm nicht als Service laufen lässt, sondern in der Konsole (aka Terminal) vielleicht indem du Gnu/Screen oder Tmux auf den Rasperry Pi installiert, vielleicht siehst du im Terminal mehr als im journalctl.
Das wäre ein ansatz. Ich wollte mir screen schonmal laden.
Kannst du ein Testaufbau, erzeugen der Deterministisch zu dem Fehlerbild führt, hiermit meine ich kannst du eine einfache Schritt-für-Schritt-Anleitung schreiben, um den Fehler zu provozieren.
Also hypothetisch: Starte den Raspberry, warte 2 Stunden, steck einen USB-Stick an, drücke Reset und dann kommt der Fehler oder wenn auf GPIO-X ein High Signal anliegt und gleichzeitig und dann auf GPIO-Y eine low Flanke kommt, dann hängt mein Programm. Eben irgendeine Handlungsanweisung um diesen Fehler zu triggern.
Nein, genau das geht so gar nicht. Das Script macht eigentlich nichts anderes als: Per request.get Variablen Lesen und per Post Variablen / Daten schreiben. Quer durch netzwer und an und von verschiedenen Teilnehmern. Dafür benutze ich ja auch try und exept, da ich nicht sicherstellen kann ob die Teilnehmer auch immer erreichbar sind. Und nein, das Script bleicht nicht hängen wenn ein Teilnehmer nicht erreichbar ist. Es kommt eine Fehlermeldung und beim nächsten Zyklus ist er wieder da und weiter gehts. Ich könnte einfach mal den Code Posten.
Benutzeravatar
hyle
User
Beiträge: 96
Registriert: Sonntag 22. Dezember 2019, 23:19
Wohnort: Leipzig

Wenn es das Skript ist, welches ich vermute, dann wundert es mich auch nicht, dass dabei keine Fehlermeldung raus kommt. Da sind ja nur nackte except enthalten. :shock:

Dann lieber ganz raus damit!
Alles was wir sind ist Sand im Wind Hoschi.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Auch wenn die Fehlerbehandlung Mist ist, erklärt sie nicht das beobachtete Verhalten. Da wird eher irgendwie das Netzwerk bockig sein. Vor und hinter den requests Aufrufen eindeutige Print statements sollten das zumindest eingrenzen können.
nezzcarth
User
Beiträge: 1647
Registriert: Samstag 16. April 2011, 12:47

hyle hat geschrieben: Montag 10. Juli 2023, 16:51 Da sind ja nur nackte except enthalten. :shock:
Zudem wird bei allen Requests im selben Statement unmittelbar mit der Antwort gearbeitet, statt wenigstens raise_for_status zu verwenden. Wenn es denn das Skript ist, ansonsten bitte den Code posten. Ohne kann man schwer weiterhelfen.
Benutzeravatar
hyle
User
Beiträge: 96
Registriert: Sonntag 22. Dezember 2019, 23:19
Wohnort: Leipzig

Vielleicht schlägt auch einfach das Mysterium zu. 🤷‍♂️ Wir wissen ja auch nicht welcher RPi das ist und welches Netzteil verwendet wird.
Alles was wir sind ist Sand im Wind Hoschi.
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

Der code basiert auf dem hier:

https://selbstbau-pv.de/wissensbasis/nu ... steuerung/

hab natürlich noch stark umgeschrieben. aber die posts und requests sind glaube nach dem muster.

komme grad nicht an den Code.
Sirius3
User
Beiträge: 17830
Registriert: Sonntag 21. Oktober 2012, 17:20

Der Code benutzt nackte Excepts, die jeglicher Fehlersuche verunmöglichen. Dann wird mit fehlerhaften oder fehlenden Daten weiter gearbeitet, was im besten Fall zum Programmabbruch führt.
Das Setzen eines Content-Type bei einem GET-Request ist ziemlich nutzlos. Wahrscheinlich wollte man hier Accept benutzen.
Bei der Ausgabe rundet man nicht mit round, sondern gibt einfach die passende Formatangabe an.
Man baut kein JSON per Hand zusammen, sondern nutzt den entsprechenden Parameter von Requests. Ich sehe gerade, die API ist so kaputt, dass die JSON kodiert in einem Formular geschickt werden muß.
Das stdout.flush kommt viel zu spät.

Code: Alles auswählen

#!/usr/bin/env python3
import time
import json
import requests
from requests.auth import HTTPBasicAuth


# Diese Daten müssen angepasst werden:
SERIAL = "112100000000" # Seriennummer des Hoymiles Wechselrichters
MAXIMUM_WR = 300 # Maximale Ausgabe des Wechselrichters
MINIMUM_WR = 100 # Minimale Ausgabe des Wechselrichters

DTU_IP = '192.100.100.20'
DTU_USER = 'admin'
DTU_PASSWORD = 'openDTU42'

SHELLY_IP = '192.100.100.30'


def read_inverter(host):
    # Nimmt Daten von der openDTU Rest-API und übersetzt sie in ein json-Format
    response = requests.get(f'http://{host}/api/livedata/status/inverters')
    response.raise_for_status()
    inverter = response.json()['inverters'][0]

    # Selektiert spezifische Daten aus der json response
    reachable = inverter['reachable'] # Ist DTU erreichbar?
    producing = inverter['producing']) # Produziert der Wechselrichter etwas?
    altes_limit = inverter['limit_absolute']) # Altes Limit
    power_dc = inverter['AC']['0']['Power DC']['v']  # Lieferung DC vom Panel
    power = inverter['AC']['0']['Power']['v'] # Abgabe BKW AC in Watt
    return reachable, producing, altes_limit, power_dc, power


def read_phase(host, emeter):
    response = requests.get(f'http://{host}/emeter/{emeter}', headers={'Accept': 'application/json'})
    response.raise_for_status()
    return response.json()['power']


def main():

    while True:
        reachable, producing, altes_limit, power_dc, power = read_inverter(DTU_IP)
        phases = [
            read_phase(SHELLY_IP, emeter)
            for emeter in [0, 1, 2]
        ]
        grid_sum = sum(phases)  # Aktueller Bezug - rechnet alle Phasen zusammen

        # Werte setzen
        print(f'\nBezug: {grid_sum:.1f} W, Produktion: {power:.1f} W, Verbrauch: {grid_sum + power:.1f} W')
        if reachable:
            setpoint = grid_sum + altes_limit - 5  # Neues Limit in Watt

            # Fange oberes Limit ab
            if setpoint > MAXIMUM_WR:
                setpoint = MAXIMUM_WR
                print(f'Setpoint auf Maximum: {MAXIMUM_WR} W')
            # Fange unteres Limit ab
            elif setpoint < MINIMUM_WR:
                setpoint = MINIMUM_WR
                print(f'Setpoint auf Minimum: {MINIMUM_WR} W')
            else:
                print(f'Setpoint berechnet: {grid_sum:.1f} W + {altes_limit:.1f} W - 5 W = {setpoint:.1f} W')

            if setpoint != altes_limit:
                print(f'Setze Inverterlimit von {altes_limit:.1f} W auf {setpoint:.1f} W... ')
                # Neues Limit setzen
                payload = {
                    "serial": SERIAL,
                    limit_type":0,
                    "limit_value": setpoint
                }
                response = requests.post(
                    url=f'http://{DTU_IP}/api/limit/config',
                    params = {'data': json.dumps(payload)}
                    auth = HTTPBasicAuth(DTU_USER, DTU_PASSWORD)
                )
                response.raise_for_status()
                print(f'Konfiguration gesendet ({response.json()["type"]})')
        time.sleep(5)

if __name__ == "__main__":
    main()
Hängen können da maximal die Requests, das sollte sich durch ein passendes Timeout aber beheben lassen.
Benutzeravatar
__blackjack__
User
Beiträge: 13242
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

[Hier wiederholt sich viel was Sirius3 schon geschrieben hat. Ich bin jetzt aber zu faul da noch mal drüber zu gehen. 😎]

Die ganzen `get()`/`post()`-Aufrufe können beliebig lange hängen bleiben falls die Gegenseite erreichtbar ist aber nicht antwortet.

Das Hauptprogramm sollte in einer Funktion verschwinden und Konstanten schreibt man KOMPLETT_GROSS.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Manchmal kann man Kommentare auch durch bessere Namen loswerden.

Ein Problem mit der ”Fehlerbehandlung” ist auch, dass einfach weitergemacht wird. Wenn so ein ``try``-Block nicht komplett durchläuft, wird danach trotzdem einfach auf Namen zugegriffen die dort nicht (neu) definiert wurden, als wäre nichts passiert. In den ersten Durchläufen kann das mit Ausnahmen enden, oder es wird mit Daten weitergearbeitet die alt sind, und von vorherigen Schleifendurchläufen stammen. Das kann doch so nicht gewollt sein.

Das `r` an einigen Stellen für das `Response`-Objekt steht und an anderen Stellen im gleichen Namensraum für eine Datenstruktur die mal JSON war, ist verwirrend. `r` ist als Name sowieso ein bisschen kurz und kryptisch.

Von den Wechselrichter-Daten die da an Namen gebunden werden, wird gar nicht alles verwendet.

Das Denglisch ist unschön. Wonach wurde denn entschieden welche Namen Deutsch und welche Englisch sein sollen? Das sieht sehr willkürlich aus.

`round()` verwendet man nur wenn man tatsächlich den gerundeten Wert für weitere Rechnungen braucht. Für die Ausgabe gibt es kürzere, übersichtlichere Formatierungsangaben für die Anzahl der Nachkommastellen.

Beim `post()` ist es sehr unschön `data` komplett manuell selbst zusammenzubasteln. Was ja auch dazu führt, dass man den Content-Type selbst angeben muss. Das würde man besser alles `requests` machen lassen.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import json
import time

import requests
from loguru import logger
from requests.auth import HTTPBasicAuth

INVERTER_SERIAL = "112100000000"
INVERTER_MIN_OUTPUT = 100  # in W.
INVERTER_MAX_OUTPUT = 300  # in W.

DTU_IP = "192.100.100.20"
DTU_USERNAME = "admin"
DTU_PASSWORD = "openDTU42"

SHELLY_IP = "192.100.100.30"  # IP Adresse von Shelly 3EM


def get_inverter_status(dtu_ip):
    response = requests.get(
        f"http://{dtu_ip}/api/livedata/status/inverters", timeout=None
    )
    response.raise_for_status()
    inverter_data = response.json()["inverters"][0]
    return (
        inverter_data["reachable"],
        int(inverter_data["limit_absolute"]),
        inverter_data["AC"]["0"]["Power"]["v"],
    )


def set_inverter_limit(dtu_ip, value):
    response = requests.post(
        f"http://{dtu_ip}/api/limit/config",
        data={
            "data": json.dumps(
                {
                    "serial": INVERTER_SERIAL,
                    "limit_type": 0,
                    "limit_value": value,
                }
            )
        },
        auth=HTTPBasicAuth(DTU_USERNAME, DTU_PASSWORD),
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["type"]


def get_power(shelly_ip, emeter_index):
    response = requests.get(
        f"http://{shelly_ip}/emeter/{emeter_index}",
        headers={"Content-Type": "application/json"},
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["power"]


def main():
    while True:
        with logger.catch():
            is_dtu_reachable, old_limit, power = get_inverter_status(DTU_IP)
            grid_sum = sum(get_power(SHELLY_IP, i) for i in range(3))
            logger.info(
                "Bezug: {0:.1f} W, Produktion: {1:.1f} W, Verbrauch: {2:.1f} W",
                grid_sum,
                power,
                grid_sum + power,
            )
            if is_dtu_reachable:
                setpoint = grid_sum + old_limit - 5  # Neues Limit in Watt

                if setpoint > INVERTER_MAX_OUTPUT:
                    setpoint = INVERTER_MAX_OUTPUT
                    logger.info(
                        f"Setpoint auf Maximum: {INVERTER_MAX_OUTPUT} W"
                    )
                elif setpoint < INVERTER_MIN_OUTPUT:
                    setpoint = INVERTER_MIN_OUTPUT
                    logger.info(
                        f"Setpoint auf Minimum: {INVERTER_MIN_OUTPUT} W"
                    )
                else:
                    #
                    # XXX Das hier Code in einer Ausgabe wiederholt wird ist
                    #   unschön.  Wenn man die `setpoint`-Berechnung irgendwann
                    #   einmal anpassen will, muss man immer daran denken auch
                    #   diese Ausgabe anzupassen.
                    #
                    logger.info(
                        "Setpoint berechnet: {0:.1f} W + {1:.1f} W - 5 W ="
                        " {2:.1f} W",
                        grid_sum,
                        old_limit,
                        setpoint,
                    )

                if setpoint != old_limit:
                    logger.info(
                        "Setze Inverterlimit von {0:.1f} W auf {1:.1f} W... ",
                        old_limit,
                        setpoint,
                    )
                    type_ = set_inverter_limit(DTU_IP, setpoint)
                    logger.info("Konfiguration gesendet ({0})", type_)

        time.sleep(5)


if __name__ == "__main__":
    main()
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

Hey,

mega cool.
Ich versuche mal in diesem Stil mein Programm zu adaptieren. Vielleicht läuft es ja dann :-)

Schonmal besten Dank!
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

Hi,

hab gestern mal schnelle tests gemacht. Script hat auch schon mal gelaufen aber jetzt schmiert es immer ab mit diesem Fehlercode:

Code: Alles auswählen

Jul 18 08:12:54 raspvpn python3[30791]: 2023-07-18 08:12:54.433 | INFO     | __main__:main:153 - Setze Inverterlimit von 0.0 W auf 106.2 W...
Jul 18 08:12:54 raspvpn python3[30791]: --- Logging error in Loguru Handler #0 ---
Jul 18 08:12:54 raspvpn python3[30791]: Record was: {'elapsed': datetime.timedelta(microseconds=328301), 'exception': (type=<class 'TypeError'>, value=TypeError('Object of type set is not JSON serializable'), traceback=<traceback object at 0x75d48f80>), 'extra': {}, 'file': (name='nulleinspeisung.py', path='/home/martin/nulleinspeisung.py'), 'function': 'main', 'level': (name='ERROR', no=40, icon='❌'), 'line': 156, 'message': "An error has been caught in function 'main', process 'MainProcess' (30791), thread 'MainThread' (1996327744):", 'module': 'nulleinspeisung', 'name': '__main__', 'process': (id=30791, name='MainProcess'), 'thread': (id=1996327744, name='MainThread'), 'time': datetime(2023, 7, 18, 8, 12, 54, 435283, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'CEST'))}
script:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests, time, sys, datetime, logging, json, subprocess
from requests.auth import HTTPBasicAuth
from loguru import logger

#logging.basicConfig(filename='/home/martin/log.log', level=logging.WARNING, format='%(asctime)s - %(message)s')
#WARNING
#INFO

# Diese Daten müssen angepasst werden:
hass_token = "sdfdsfsfsff"
InverterId = "000000000000" # Seriennummer des Hoymiles Wechselrichters
INVERTER_MAX_OUTPUT = 200 # Maximale Ausgabe des Wechselrichters
INVERTER_MIN_OUTPUT = 10  # Minimale Ausgabe des Wechselrichters

# IP Adressen

SHELLY_IP = '192.168.1.62' # IP Adresse von Shelly Plug S
SHELLY_3EM_IP = '192.168.1.14' # IP Adresse von Shelly Plug S
AHOI_IP = '192.168.1.106'  # IP Adresse von Ahoy-DTU



def check_address_reachable(address):
    command = ['ping', '-c', '1', address]  # Anpassung für Windows: ['ping', '-n', '1', address]
    try:
        subprocess.check_output(command)
        return True  # Adresse ist erreichbar
    except subprocess.CalledProcessError:
        return False  # Adresse ist nicht erreichbar


def get_power(shelly_3em_ip, emeter_index):
    response = requests.get(
        f"http://{shelly_3em_ip}/emeter/{emeter_index}",
        headers={"Content-Type": "application/json"},
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["power"]

def get_power_shelly(shelly_ip):
    response = requests.get(
        f"http://{shelly_ip}/meter/0",
        headers={"Content-Type": "application/json"},
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["power"]
    

def set_inverter_limit(ahoi_ip, value):
    response = requests.post(
        f"http://{ahoi_ip}/api/ctrl",
        data={
            "data": json.dumps(
                {
                    "id": {InverterId},
                    "cmd": "limit_nonpersistent_absolute",
                    "val": value,
                }
            )
        },
    )
    response.raise_for_status()
    return response.json()["type"]



def main():
    while True:
        with logger.catch():
            grid_sum = sum(get_power(SHELLY_3EM_IP, i) for i in range(3))
            old_limit = get_power_shelly(SHELLY_IP)
            logger.info(
                "Bezug: {0:.1f} W, Produktion: {1:.1f} W, Verbrauch: {2:.1f} W",
                grid_sum,
                power,
                grid_sum + power,
            )
            if True:
                setpoint = grid_sum + old_limit - 5  # Neues Limit in Watt

                if setpoint > INVERTER_MAX_OUTPUT:
                    setpoint = INVERTER_MAX_OUTPUT
                    logger.info(
                        f"Setpoint auf Maximum: {INVERTER_MAX_OUTPUT} W"
                    )
                elif setpoint < INVERTER_MIN_OUTPUT:
                    setpoint = INVERTER_MIN_OUTPUT
                    logger.info(
                        f"Setpoint auf Minimum: {INVERTER_MIN_OUTPUT} W"
                    )
                else:
                    logger.info(
                        "Setpoint berechnet: {0:.1f} W + {1:.1f} W - 5 W ="
                        " {2:.1f} W",
                        grid_sum,
                        old_limit,
                        setpoint,
                    )

                if setpoint != old_limit:
                    logger.info(
                        "Setze Inverterlimit von {0:.1f} W auf {1:.1f} W... ",
                        old_limit,
                        setpoint,
                    )
                    type_ = set_inverter_limit(AHOI_IP, setpoint)
                    logger.info("Konfiguration gesendet ({0})", type_)

        time.sleep(5)

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13242
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@WZBFL: Das ist nicht der Code der tatsächlich gelaufen ist, denn der hier läuft gleich am Anfang in eine Ausnahme weil `power` nicht definiert ist.

Die Inverterseriennummer wird in ein `set` gesteckt und das ist nicht als JSON serialisierbar. Macht auch nicht wirklich Sinn.

`datetime`, `logging`, und `sys` werden importiert, aber nirgends verwendet. Ebenso werden `hass_token`, `check_address_reachable()` definiert und nirgends verwendet. Letzteres zieht nach sich, dass `subprocess` nicht verwendet wird.

`InverterId` ist falsch geschrieben. Das ist eine Konstante (KOMPLETT_GROSS) und keine Klasse (PascalCase).

`get_power()` und `get_power_shelly()` sind fast gleich, das sollten keine zwei Funktionen sein. Zumal auch `get_power()` ein Shelly-Gerät abfragt, der Namensunterschied also nicht wirklich aufklärt.

``if True:``‽

Code: Alles auswählen

#!/usr/bin/env python3
import json
import time

import requests
from loguru import logger
from requests.auth import HTTPBasicAuth


INVERTER_SERIAL_NUMBER = "000000000000"
INVERTER_MAX_OUTPUT = 200
INVERTER_MIN_OUTPUT = 10

SHELLY_IP = "192.168.1.62"  #  Shelly Plug S
SHELLY_3EM_IP = "192.168.1.14"  # Shelly Plug S
AHOI_IP = "192.168.1.106"  # Ahoy-DTU


def get_power(shelly_ip, meter_name, meter_index):
    response = requests.get(
        f"http://{shelly_ip}/{meter_name}/{meter_index}",
        headers={"Content-Type": "application/json"},
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["power"]


def set_inverter_limit(ahoi_ip, value):
    response = requests.post(
        f"http://{ahoi_ip}/api/ctrl",
        data={
            "data": json.dumps(
                {
                    "id": INVERTER_SERIAL_NUMBER,
                    "cmd": "limit_nonpersistent_absolute",
                    "val": value,
                }
            )
        },
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["type"]


def main():
    while True:
        with logger.catch():
            grid_sum = sum(
                get_power(SHELLY_3EM_IP, "emeter", i) for i in range(3)
            )
            old_limit = get_power(SHELLY_IP, "meter", 0)
            logger.info(
                "Bezug: {0:.1f} W, Produktion: {1:.1f} W, Verbrauch: {2:.1f} W",
                grid_sum,
                old_limit,
                grid_sum + old_limit,
            )
            setpoint = grid_sum + old_limit - 5  # Neues Limit in Watt.

            if setpoint > INVERTER_MAX_OUTPUT:
                setpoint = INVERTER_MAX_OUTPUT
                logger.info(f"Setpoint auf Maximum: {INVERTER_MAX_OUTPUT} W")
            elif setpoint < INVERTER_MIN_OUTPUT:
                setpoint = INVERTER_MIN_OUTPUT
                logger.info(f"Setpoint auf Minimum: {INVERTER_MIN_OUTPUT} W")
            else:
                logger.info(
                    "Setpoint berechnet: {0:.1f} W + {1:.1f} W - 5 W ="
                    " {2:.1f} W",
                    grid_sum,
                    old_limit,
                    setpoint,
                )

            if setpoint != old_limit:
                logger.info(
                    "Setze Inverterlimit von {0:.1f} W auf {1:.1f} W... ",
                    old_limit,
                    setpoint,
                )
                type_ = set_inverter_limit(AHOI_IP, setpoint)
                logger.info("Konfiguration gesendet ({0})", type_)

        time.sleep(5)


if __name__ == "__main__":
    main()
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

__blackjack__ hat geschrieben: Dienstag 18. Juli 2023, 09:41 @WZBFL: Das ist nicht der Code der tatsächlich gelaufen ist, denn der hier läuft gleich am Anfang in eine Ausnahme weil `power` nicht definiert ist.

Die Inverterseriennummer wird in ein `set` gesteckt und das ist nicht als JSON serialisierbar. Macht auch nicht wirklich Sinn.

- ich habe keine Idee was damit gemeint ist -> das wird vermutlich auch das verständnisproblem sein...

`datetime`, `logging`, und `sys` werden importiert, aber nirgends verwendet. Ebenso werden `hass_token`, `check_address_reachable()` definiert und nirgends verwendet. Letzteres zieht nach sich, dass `subprocess` nicht verwendet wird.

- joa, ich hab halt vorher noch mehr gemacht und wollte erstmal reduzieren und lauffähig werden. ist aus dem vorherigem Code rauskopiert mit deinen Code schnippseln

`InverterId` ist falsch geschrieben. Das ist eine Konstante (KOMPLETT_GROSS) und keine Klasse (PascalCase).

- auch das war mir erstmal wurscht. Ist aber korrigiert

`get_power()` und `get_power_shelly()` sind fast gleich, das sollten keine zwei Funktionen sein. Zumal auch `get_power()` ein Shelly-Gerät abfragt, der Namensunterschied also nicht wirklich aufklärt.

- Danke für den Hinweis, ich hab mir schon sowas gedacht, bin mit python aber noch nicht so warm. Danke!

``if True:``‽

- wenn ich reachable vom inverter nehme, speise ich die halbe zeit nicht ein, da das Ahoi DIng oft einfach gar nicht verfügbar ist oder nichts zurückmeldet. Hier funktioniert Fire&forget


Code: Alles auswählen

#!/usr/bin/env python3
import json
import time

import requests
from loguru import logger
from requests.auth import HTTPBasicAuth


INVERTER_SERIAL_NUMBER = "000000000000"
INVERTER_MAX_OUTPUT = 200
INVERTER_MIN_OUTPUT = 10

SHELLY_IP = "192.168.1.62"  #  Shelly Plug S
SHELLY_3EM_IP = "192.168.1.14"  # Shelly Plug S
AHOI_IP = "192.168.1.106"  # Ahoy-DTU


def get_power(shelly_ip, meter_name, meter_index):
    response = requests.get(
        f"http://{shelly_ip}/{meter_name}/{meter_index}",
        headers={"Content-Type": "application/json"},
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["power"]


def set_inverter_limit(ahoi_ip, value):
    response = requests.post(
        f"http://{ahoi_ip}/api/ctrl",
        data={
            "data": json.dumps(
                {
                    "id": INVERTER_SERIAL_NUMBER,
                    "cmd": "limit_nonpersistent_absolute",
                    "val": value,
                }
            )
        },
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["type"]


def main():
    while True:
        with logger.catch():
            grid_sum = sum(
                get_power(SHELLY_3EM_IP, "emeter", i) for i in range(3)
            )
            old_limit = get_power(SHELLY_IP, "meter", 0)
            logger.info(
                "Bezug: {0:.1f} W, Produktion: {1:.1f} W, Verbrauch: {2:.1f} W",
                grid_sum,
                old_limit,
                grid_sum + old_limit,
            )
            setpoint = grid_sum + old_limit - 5  # Neues Limit in Watt.

            if setpoint > INVERTER_MAX_OUTPUT:
                setpoint = INVERTER_MAX_OUTPUT
                logger.info(f"Setpoint auf Maximum: {INVERTER_MAX_OUTPUT} W")
            elif setpoint < INVERTER_MIN_OUTPUT:
                setpoint = INVERTER_MIN_OUTPUT
                logger.info(f"Setpoint auf Minimum: {INVERTER_MIN_OUTPUT} W")
            else:
                logger.info(
                    "Setpoint berechnet: {0:.1f} W + {1:.1f} W - 5 W ="
                    " {2:.1f} W",
                    grid_sum,
                    old_limit,
                    setpoint,
                )

            if setpoint != old_limit:
                logger.info(
                    "Setze Inverterlimit von {0:.1f} W auf {1:.1f} W... ",
                    old_limit,
                    setpoint,
                )
                type_ = set_inverter_limit(AHOI_IP, setpoint)
                logger.info("Konfiguration gesendet ({0})", type_)

        time.sleep(5)


if __name__ == "__main__":
    main()
WZBFL
User
Beiträge: 13
Registriert: Montag 10. Juli 2023, 09:19

aktuell stürzt es noch immer ab mit.

Code: Alles auswählen

Jul 18 11:05:41 raspvpn python3[6610]: 2023-07-18 11:05:41.413 | INFO     | __main__:main:106 - Setze Inverterlimit von 154.0 W auf 200.0 W...
Jul 18 11:05:44 raspvpn python3[6610]: --- Logging error in Loguru Handler #0 ---
Jul 18 11:05:44 raspvpn python3[6610]: Record was: {'elapsed': datetime.timedelta(seconds=3, microseconds=754115), 'exception': (type=<class 'requests.exceptions.ConnectionError'>, value=ConnectionError(ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))), traceback=<traceback object at 0x75d2fbe8>), 'extra': {}, 'file': (name='nulleinspeisung.py', path='/home/martin/nulleinspeisung.py'), 'function': 'main', 'level': (name='ERROR', no=40, icon='❌'), 'line': 109, 'message': "An error has been caught in function 'main', process 'MainProcess' (6610), thread 'MainThread' (1996188480):", 'module': 'nulleinspeisung', 'name': '__main__', 'process': (id=6610, name='MainProcess'), 'thread': (id=1996188480, name='MainThread'), 'time': datetime(2023, 7, 18, 11, 5, 44, 874562, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200), 'CEST'))}
J
habe deine änderungen aber übernommen:

Code: Alles auswählen

def set_inverter_limit(ahoi_ip, value):
    response = requests.post(
        f"http://{ahoi_ip}/api/ctrl",
        data={
            "data": json.dumps(
                {
                    "id": INVERTER_ID,
                    "cmd": "limit_nonpersistent_absolute",
                    "val": value,
                },
                )
             },
        timeout=None,
    )
    response.raise_for_status()
    return response.json()["type"]
im alten COde war noch ein Header der übratragen wird. Daran liegt es nicht oder?

Code: Alles auswählen

                 url = f"http://{ahoi_ip}/api/ctrl"
                   data = f'{{"id": {InverterId}, "cmd": "limit_nonpersistent_absolute", "val": {setpoint}}}'
                   headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
                   requests.post(url, data=data, headers=headers)
damit geht es zumindest.
Antworten