Custom Firmware zum Auslesen eines Growatt MOD 10KTL3-XH (BP) mit APX Speicher

02.02.2024 Embedded
 

Einleitung

Im August letzten Jahres habe ich mich dazu entschlossen, eine Solaranlage auf unserem Haus zu installieren. Aus Kostengründen erfolgte die Planung durch mich, die Installation der Module durch einen Dachdecker und der Anschluss ans Netz durch einen Elektriker. Da ich wenig Vorwissen hatte, habe ich mich für den Wechselrichter MOD 10KTL3-XH (BP) entschieden. Für die BP-Reihe hatte ich mich entschieden, da diese die Möglichkeit eines Notstromanschlusses unterstützt.

Zu diesem Zeitpunkt wusste ich nicht, dass es schwierig sein würde, den Wechselrichter zu überwachen und in das Smart Home einzubinden. Es gibt ein Portal von Growatt, das die Wechselrichterdaten anzeigt. Allerdings aktualisieren sich diese Daten nur etwa alle 5-10 Minuten (Stand Ende 2023). Selbst die Abfragen dieser Daten mit Home Assistant waren nur schwer möglich. Zwar gibt es eine Home Assistant Integration, jedoch sperrte Growatt nach kurzer Zeit meine IP-Adresse aufgrund der regelmäßigen Abfragen.

Kurze Zeit später stieß ich im Photovoltaikforum auf eine Custom Firmware des Nutzers "otti". Diese unterstützte zum damaligen Zeitpunkt drei verschiedene Abfragevarianten für Growatt-Wechselrichter (1.20, 1.24 und 3.05). Nach der Installation auf meinem ShineWifi-X Datenlogger musste ich feststellen, dass die angezeigten Werte des Batteriespeichers falsch waren. Auch nach einer ausführlichen Suche im Internet wurde ich nicht fündig. Ich entschied mich also dazu, selbst die korrekten Daten abzufragen.

Erster Versuch: Growatt-Support fragen

Oft ist die Lösung einfacher als erwartet. Anstatt am Anfang viel Aufwand in das Thema zu stecken, entschied ich mich, den Growatt-Support zu kontaktieren und nach einer Dokumentation der Register für die BP-Serie zu fragen. Dieser meldete sich etwa eine Woche später mit einer kurzen Antwort, dass das angefragte Dokument nur für Großkunden verfügbar sei.

Allerdings versuchte ich weiterhin, an die Informationen zu gelangen, und fragte nach den Speicherregistern für den Batteriespeicher. Knapp einen Monat später hatte ich immer noch keine Antwort erhalten.

Da sich der Growatt-Support als nicht hilfreich erwiesen hatte, benötigte ich eine Alternative.

Zweiter Versuch: Daten loggen und vergleichen

Ich überlegte mir, wie man noch an die Daten kommen könnte. Über die Custom Firmware gibt es die Möglichkeit, einzelne Register des Wechselrichters abzufragen.

Modbus Abfrageformular
Eine Abfrage des Registers 0 erzeugt z. B. folgende Ausgabe:
Read 16b Input register 0 with value 1


Nun musste ich nur noch ein Script schreiben, welches mir alle Register ausliest und in eine Datenbank schreibt:
prepare("INSERT INTO register_data (register, time, value) VALUES (?, NOW(), ?)");
    return $stmt->execute([$register, $value]);
}

function checkRegister(int $register): string {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "http://growatt-modstick-host/postCommunicationModbus_p");
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, "reg=$register&val=&type=16b&operation=R®isterType=I");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $data = curl_exec($ch);

    curl_close($ch);
    return $data;
}

function checkRegisterWithRetry(int $register): int {
    for($i = 5; $i > 0; $i--) {
        $data = checkRegister($register);
        if(!empty($data) && !stristr($data, 'not connected?')) {
            $parts = explode(' with value ', $data);
            return intval($parts[1]);
        }
    }

    return -1;
}

while(true) {
    for($i = 0; $i < 5000; $i++) {
        // load value
        echo "Checking $i: ";
        $value = checkRegisterWithRetry($i);
        echo "$value\n";

        // save in database
        insertRegister($i, $value);
    }
}
Ein erster Scan hat gezeigt, dass sich die meisten Vorgänge im Bereich von 0-5000 ereignen, daher habe ich das Skript so geschrieben, dass es alle Register in diesem Bereich in einer Dauerschleife abfragt.

Nachdem das Skript für ein paar Tage Daten gesammelt hatte, habe ich sie in einer Tabelle mit entsprechenden Grafiken dargestellt, um Ereignisse beim (Ent-)Laden des Akkus zu visualisieren.

Solartabelle

Ich vermutete bereits, dass in dieser Tabelle nicht die Lade-/Entladeleistung zu sehen war, da die Gesamtleistung (ein- und ausgehend) als 32 Bit in der Custom Firmware abgefragt wurde. Allerdings hoffte ich, den Akkustand in Prozent auslesen zu können. Ich erwartete einen Graphen nach folgendem Schema.

Entwurf der Speicherkurve

Da die Tabelle jedoch über 5000 Einträge verfügte, war die Anzeige kaum möglich. Daher filterte ich nach folgenden Kriterien:
  • Liegt der Minimal- und Maximalwert weniger als 5 auseinander, wird ausgeblendet.
  • Der Maximalwert ist unter 150.
Damit schrumpfte die Tabelle auf wenige Register, in denen ich schnell fündig wurde.

Speicherkurve

Dieser Verlauf war in mehreren Registern zu sehen, in denen die Zahlen leicht voneinander abwichen. Nach einem Vergleich mit der Anzeige auf dem Speicher konnte ich feststellen, dass die Register 3171 und 4014 den Speicherstand anzeigten.

Erkennen von Lade- und Entladeleistung

Nachdem ich mit dieser Methode erfolgreich den Speicherstand auslesen konnte, wollte ich nun die Lade- und Entladeleistung ermitteln. Dazu musste ich Daten zu 32-Bit-Registern sammeln. Die Werte speicherte ich in eine separate Tabelle der Datenbank und passte den POST-Body entsprechend an. Jetzt musste nur noch die Sonne genug scheinen, um den Akku zu laden.

Ende Januar bzw. Anfang Februar 2024 gab es ein paar sonnige Tage, an denen ich Daten sammeln konnte. Nun musste ich nur noch nach Änderungen in den Graphen suchen, die mit den Zeitpunkten korrelierten, an denen der Akkustand stieg oder fiel. Anhand der vorhandenen Protokolle vermutete ich, dass die relevanten Register irgendwo in der Nähe von 3171 oder 4014 liegen mussten. Zuerst mussten die Filter wieder angepasst werden.

  • Liegt der Minimal- und Maximalwert weniger als 5 auseinander, wird ausgeblendet.
  • Der Maximalwert ist unter 50000, was der maximalen Lade- oder Entladeleistung des Speichers entspricht (Faktor 10, wegen einer Nachkommastelle).
Der Graph zeigte bei Register 4023 einen Anstieg, sobald die Sonne schien, und bei 4021 einen Rückgang. Als ich den Fön einschaltete, war es genau umgekehrt. Das bedeutet:
  • 4021 zeigt die Entladeleistung.
  • 4023 zeigt die Ladeleistung.
Die Graphen sehen folgendermaßen aus.

Speicherkurve

Nun musste das Ganze nur noch in das Projekt von otti eingebunden und eine neue Firmware gebaut werden. Die komplette Datei kann auf Github angesehen werden.

Einbindung in Home Assistant

Um alle für mich relevanten Werte in Home Assistant anzuzeigen, musste die configuration.yml um folgendes erweitert werden:
mqtt:
    - sensor:
      - name: "Growatt Status"
        unique_id: "growatt_status"
        state_topic: "energy/solar"
        value_template: "{{ value_json.InverterStatus }}"
        
      - name: "Growatt InputPower"
        unique_id: "growatt_input_power"
        unit_of_measurement: "W"
        state_class: "measurement"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.InputPower }}"
        
      - name: "Growatt OutputPower"
        unique_id: "growatt_output_power"
        unit_of_measurement: "W"
        state_class: "measurement"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.OutputPower }}"
        
      - name: "Growatt PV1Voltage"
        unique_id: "growatt_pv1_voltage"
        unit_of_measurement: "V"
        state_class: "measurement"
        device_class: "voltage"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV1Voltage }}"
        
      - name: "Growatt PV1InputCurrent"
        unique_id: "growatt_pv1_input_current"
        unit_of_measurement: "A"
        state_class: "measurement"
        device_class: "current"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV1InputCurrent }}"
        
      - name: "Growatt PV1InputPower"
        unique_id: "growatt_pv1_input_power"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV1InputPower }}"
        
      - name: "Growatt PV2Voltage"
        unique_id: "growatt_pv2_voltage"
        unit_of_measurement: "V"
        state_class: "measurement"
        device_class: "voltage"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV2Voltage }}"
        
      - name: "Growatt PV2InputCurrent"
        unique_id: "growatt_pv2_input_current"
        unit_of_measurement: "A"
        state_class: "measurement"
        device_class: "current"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV2InputCurrent }}"
        
      - name: "Growatt PV2InputPower"
        unique_id: "growatt_pv2_input_power"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV2InputPower }}"
       
      - name: "Growatt GridFrequency"
        unique_id: "growatt_GridFrequency"
        unit_of_measurement: "Hz"
        state_class: "measurement"
        state_topic: "energy/solar"
        device_class: "frequency"
        value_template: "{{ value_json.GridFrequency }}"
        
      - name: "Growatt L1ThreePhaseGridVoltage"
        unique_id: "growatt_l1_three_phase_grid_voltage"
        unit_of_measurement: "V"
        state_class: "measurement"
        device_class: "voltage"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L1ThreePhaseGridVoltage }}"
        
      - name: "Growatt L1ThreePhaseGridOutputCurrent"
        unique_id: "growatt_l1_three_phase_grid_output_current"
        unit_of_measurement: "A"
        state_class: "measurement"
        device_class: "current"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L1ThreePhaseGridOutputCurrent }}"
        
      - name: "Growatt L1ThreePhaseGridOutputPower"
        unique_id: "growatt_l1_three_phase_grid_output_power"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L1ThreePhaseGridOutputPower }}"
        
      - name: "Growatt L2ThreePhaseGridVoltage"
        unique_id: "growatt_l2_three_phase_grid_voltage"
        unit_of_measurement: "V"
        state_class: "measurement"
        device_class: "voltage"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L2ThreePhaseGridVoltage }}"
        
      - name: "Growatt L2ThreePhaseGridOutputCurrent"
        unique_id: "growatt_l2_three_phase_grid_output_current"
        unit_of_measurement: "A"
        state_class: "measurement"
        device_class: "current"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L2ThreePhaseGridOutputCurrent }}"
        
      - name: "Growatt L2ThreePhaseGridOutputPower"
        unique_id: "growatt_l2_three_phase_grid_output_power"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L2ThreePhaseGridOutputPower }}"
        
      - name: "Growatt L3ThreePhaseGridVoltage"
        unique_id: "growatt_l3_three_phase_grid_voltage"
        unit_of_measurement: "V"
        state_class: "measurement"
        device_class: "voltage"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L3ThreePhaseGridVoltage }}"
        
      - name: "Growatt L3ThreePhaseGridOutputCurrent"
        unique_id: "growatt_l3_three_phase_grid_output_current"
        unit_of_measurement: "A"
        state_class: "measurement"
        device_class: "current"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L3ThreePhaseGridOutputCurrent }}"
        
      - name: "Growatt L3ThreePhaseGridOutputPower"
        unique_id: "growatt_l3_three_phase_grid_output_power"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.L3ThreePhaseGridOutputPower }}"
        
      - name: "Growatt TodayGenerateEnergy"
        unique_id: "growatt_today_generate_energy"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.TodayGenerateEnergy }}"
        
      - state_topic: "energy/solar"
        unique_id: "growatt_total_generate_energy"
        name: "Growatt TotalGenerateEnergy"
        unit_of_measurement: "kWh"
        value_template: "{{ float(value_json.TotalGenerateEnergy) | round(1) }}"
        device_class: energy
        state_class: total_increasing
        json_attributes_topic: "energy/solar"
        payload_available: "5"
        availability_mode: latest
        availability_topic: "energy/solar"
        availability_template: "{{ value_json.InverterStatus }}"
        
      - name: "Growatt PV1EnergyToday"
        unique_id: "growatt_pv1_energy_today"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV1EnergyToday }}"
        
      - name: "Growatt PV1EnergyTotal"
        unique_id: "growatt_pv1_energy_total"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV1EnergyTotal }}"
        
      - name: "Growatt PV2EnergyToday"
        unique_id: "growatt_pv2_energy_today"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV2EnergyToday }}"
        
      - name: "Growatt PV2EnergyTotal"
        unique_id: "growatt_pv2_energy_today"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PV2EnergyToday }}"
        
      - name: "Growatt PVEnergyTotal"
        unique_id: "growatt_pv_energy_today"
        unit_of_measurement: "kWh"
        state_class: "total_increasing"
        device_class: "energy"
        state_topic: "energy/solar"
        value_template: "{{ value_json.PVEnergyTotal }}"
        
      - name: "Growatt InverterTemperature"
        unique_id: "growatt_inverter_temperature"
        unit_of_measurement: "C"
        state_class: "measurement"
        device_class: "temperature"
        state_topic: "energy/solar"
        value_template: "{{ value_json.InverterTemperature }}"
        
      - name: "Growatt BatteryPercentage"
        unique_id: "growatt_battery_percentage"
        unit_of_measurement: "%"
        state_class: "measurement"
        device_class: "battery"
        state_topic: "energy/solar"
        value_template: "{{ value_json.BatteryPercentage }}"      

    - name: "Growatt Charging"
        unique_id: "growatt_battery_charging"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.BatteryCharge }}"
        
    - name: "Growatt Discharging"
        unique_id: "growatt_battery_discharging"
        unit_of_measurement: "W"
        device_class: "power"
        state_topic: "energy/solar"
        value_template: "{{ value_json.BatteryDischarge }}"
Nach dem Laden der neuen Konfiguration lassen sich diese zum Dashboard hinzufügen.

Ausblick

Trotz fehlender Unterstützung seitens des Growatt-Supports ist es mir gelungen, die gewünschten Werte abzurufen. Leider konnte ich diese Werte nur finden, weil ich wusste, wonach ich suchen musste und gezielt danach gesucht habe. Weitere Werte kann ich leider nicht ohne konkreten Anhaltspunkt herausfinden.

Falls Sie Ideen haben, welche anderen Werte abgefragt werden könnten, können Sie sich gerne melden. Neben dem Speicher interessiert mich auch die Abfrage der Notstromfunktion. Da ich diese jedoch nicht besitze, könnte das vielleicht jemand anderes übernehmen.

Vielleicht konnte ich mit dieser Methode anderen helfen und sie dazu ermutigen, selbst weitere Werte abzurufen und zu teilen.