Marstek Venus E Modulentwicklung

Begonnen von chri0815, 01 April 2026, 20:39:11

Vorheriges Thema - Nächstes Thema

chri0815

Hallo Zusammen
ich habe mir vor kurzem einen Marstek Venus E angeschafft und bin damit auch ganz zufrieden. Nun habe ich entdeckt, dass es da eine Local API Schnittstelle mittels udp gibt. Ich habe mit ChatGpt und der API Beschreibung von Marstek auch schon erste erfolge und kann nun Daten vom Marstek empfangen. Nur ist mein Perl mehr als schlecht und ich benötige Hilfe bei der Implementierung der Set Befehle. Hätte jemand Lust mich dabei zu unterstützen?

rudolfkoenig

Ich empfehle eine erste Version des Moduls mit ChatGpt zu erstellen, laut anderen Beitraegen hier im Forum kommt dabei eine funktionsfaehige Version nach ein paar Runden raus.
Ich kann dann gerne das Ergebnis anschauen, und helfen, verbliebene Probleme zu beseitigen.

Moli

Guten Morgen, ich habe einen Venus E Gen 2 und einen B2500, mit Python und FHEM habe ich einen Shelly Simulator für den Venus und mit ESP32 C3 Mini (BT) lese ich den B2500.
Den Venus E lese ich mit Adapter und MQTT aus, da ich gerne einen 2 haben möchte, wäre mir aber die Local API lieber.

Habt ihr etwas hinbekommen, wo ich ansetzten kann?
Der Port 30000 ist offen, aber ich bekomme keine Daten.

Falls mir jemand seinen Ansatz im FHEM geben könnte, wäre ich dankbar, vielleicht zickt ja auch der Venus.

Gruß

Dracolein

Raspberry Pi 4 mit FHEM; FTUI Dashboard auf Asus 15,6" VT168H Touchscreen; ZigBee mit ConBee2 USB-Stick; div. Shelly 2.5; integr. Gaszähler mit ESP8266 & ESPEasy;

Moli

Moin. soweit ich weiß, geht die cloud app nicht mehr wenn man mqtt freischalten lässt.

Würde ich gerne vermeiden.

Gruß

chri0815

#5
Hallo Zusammen,
lang hat es gedauert aber ich habe jetzt eine erste Modulversion für den Marstek Venus E. Wer Verbesserungen hat, kann das gern einfügen. Ich habe aus der API Doku nur die Get Befehle übernommen. Zum Konfigurieren die IP Adresse und Den Port 30000 angeben. Das Intervall bei den attr definieren, aber nicht zu klein setzen, eher 10s oder höher.
Nicht vergessen im Marstek die Local API Schnitstelle einschalten!!!
Grüße

######################################################################
# 99_Marstek.pm
# FHEM Modul für Marstek Geräte (Venus C/E/D)
# Liest SOC, Bat-Power, PV-Power, Lade-/Entladeleistung
# Unterstützt Startwert für SOC (soc_start) und Polling
######################################################################

package main;

use strict;
use warnings;
use IO::Socket::INET;
use JSON::PP;
use Time::HiRes qw(gettimeofday);

##########################
# Modul-Initialisierung
##########################
sub Marstek_Initialize {
    my ($hash) = @_;
    $hash->{DefFn}    = "Marstek_Define";
    $hash->{UndefFn}  = "Marstek_Undef";
    $hash->{AttrFn}   = "Marstek_Attr";
    $hash->{GetFn}   = "Marstek_Get";
    $hash->{NotifyFn} = "Marstek_Notify";
    $hash->{AttrList} = "interval";
}

##########################
# Define
##########################
sub Marstek_Define {
    my ($hash, $def) = @_;
    my @args = split(" ", $def);

    return "Usage: define <name> Marstek <IP> <PORT>" unless @args == 4;

    $hash->{IP}       = $args[2];
    $hash->{PORT}     = $args[3];
    $hash->{INTERVAL} = 60;

    InternalTimer(gettimeofday()+1, sub { Marstek_Poll($hash) }, 0);
    return undef;
}

##########################
# Undef
##########################
sub Marstek_Undef {
    my ($hash) = @_;
    RemoveInternalTimer($hash);
    return undef;
}

##########################
# Attribute
##########################
sub Marstek_Attr {
    my ($cmd, $name, $attrName, $attrValue) = @_;
    my $hash = $defs{$name};

    if ($attrName eq "interval" && $attrValue =~ /^\d+$/) {
        $hash->{INTERVAL} = $attrValue;
    }
    return undef;
}

##########################
# Notify
##########################
sub Marstek_Notify {
    my ($hash, $dev) = @_;
    return undef;
}

##########################
# Gemeinsame Request-Funktion
##########################
sub Marstek_DoRequest {
    my ($hash, $method, $units) = @_;
    my $success = 0;
    my $sock = IO::Socket::INET->new(
        PeerAddr => $hash->{IP},
        PeerPort => $hash->{PORT},
        Proto    => 'udp',
        Timeout  => 2
    );

    if ($sock) {
        my $req = {
            id     => 1,
            method => $method,
            params => { id => 0 }
        };

        my $json = encode_json($req);
        $sock->send($json);

        my $resp = "";

        eval {
            local $SIG{ALRM} = sub { die "Timeout\n" };
            alarm 2;
            $sock->recv($resp, 4096);
            alarm 0;
        };

        if ($@ && $@ ne "Timeout\n") {
            Log 3, "Marstek Poll recv error ($method): $@";
        }

        my $data;
        eval { $data = decode_json($resp) };

        if ($@) {
            Log 3, "Marstek JSON decode error ($method): $@";
        }

        if ($data && $data->{result}) {
            for my $key (keys %{$units}) {
                if (exists $data->{result}{$key}) {
                    readingsSingleUpdate(
                        $hash,
                        $key,
                        $data->{result}{$key} . " " . $units->{$key},
                        1
                    );
                    $success = 1;  # mark success if at least one reading updated
                }
            }
        }

        close($sock);
    }
    else {
        readingsSingleUpdate($hash, "state", "timeout", 1);
    }
    return $success;
}

##########################
# Polling
##########################
sub Marstek_Poll {
    my ($hash) = @_;
    return unless $hash;
    my $success = 0;
    eval {
        $success ||= Marstek_DoRequest($hash, "ES.GetStatus", {
            bat_soc       => "%",
            bat_cap       => "Wh",
            pv_power      => "W",
            ongrid_power  => "W",
            offgrid_power => "W",
            total_pv_energy           => "Wh",
            total_grid_output_energy  => "Wh",
            total_grid_input_energy   => "Wh",
            total_load_energy         => "Wh",
        });

        $success ||= Marstek_DoRequest($hash, "Bat.GetStatus", {
            id              => "",
            soc             => "%",
            charge_flag     => "",
            dischrg_flag    => "",
            bat_temp        => "C",
            bat_capacity    => "Wh",
            rated_capacity  => "Wh",
        });

        $success ||= Marstek_DoRequest($hash, "Wifi.GetStatus", {
            id        => "",
            wifi_mac  => "",
            ssid      => "",
            rssi      => "",
            sta_ip    => "",
        });

        $success ||= Marstek_DoRequest($hash, "ES.GetMode", {
            id             => "",
            mode           => "",
            ct_state       => "",
            total_power    => "W",
            input_energy   => "Wh",
            output_energy  => "Wh",
        });
    };

    if ($@) {
        Log 3, "Marstek Poll error: $@";
    }

    # State korrekt setzen
    readingsSingleUpdate($hash, "state", $success ? "ok" : "timeout", 1);


    InternalTimer(
        gettimeofday() + ($hash->{INTERVAL} // 60),
        sub { Marstek_Poll($hash) },
        0
    );
}
##########################
# Get
##########################
sub Marstek_Get {
    my ($hash, $name, $opt, @args) = @_;

    return "\"get $name\" needs at least one argument" unless(defined($opt));

    if ($opt eq "update") {
        Marstek_Poll($hash);
        return "Update triggered";
    }else {
    # IMPORTANT! This defines the list of possible commands
    my $list = "update:noArg";
        return "Unknown argument $opt, choose one of $list";
    }
  return -1;
   }
1;