Marstek Venus E Modulentwicklung

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

Vorheriges Thema - Nächstes Thema

Moli

Endlich hat Marstek geantwortet und mir ein Update zugewiesen, was ich auch sofort installiert habe.

Leider keine Änderung, Port offen, keine Daten.

Moli

So noch mal Danke für deine Arbeit, mit dem Gen 3 ging es sofort, ärgerlich.

Habe mal noch stateFormat hinzugefügt.

######################################################################
# 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" . $readingFnAttributes;
}

##########################
# 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;

Moli

Habe noch bisschen optimiert und Versionierung gemacht.

99_Marstek.pm