Zitat von: Marko1976 am 03 April 2026, 20:58:31Leider bewirkt auch das herauslöschen der Leerzeichen eine extreme Änderung der Regex - das hatte ich selbst schon mal ausprobiert und völlig andere Ergebnisse bzw. Regex bekommen als mit.
Zitat von: RalfRog am 04 April 2026, 00:25:45Die Frage ist dann doch schon ob das so Sinn macht, da die kleinste Änderung alles durcheinander bringt.Nein, eben nicht.
Zitat von: Marko1976 am 03 April 2026, 20:58:31Die Teamnamen und das Serien-Ergebniss habe ich bereits rausbekommen. Wo ich aktuell dran bin sind die Einzelergebnisse
attr eis reading01Name name
attr eis reading01RegOpt g
attr eis reading01Regex <div class="col-5 text-center">\v +<div class="imgcontainer">\v +<img class="img-fluid poteamlogo" alt.*images.*" \/>\v.*\v +\b(.*)\v.*\v.*\v.*Serie
attr eis reading02Name standing
attr eis reading02RegOpt g
attr eis reading02Regex <h3>(.*)<\/h3>
attr eis reading03Name einzel
attr eis reading03RegOpt g
attr eis reading03Regex <div class="col text-center gameentry">\v.*teamshorts">(\w+:\w+).*\v.*\v +(\d:\d)\v
einzel-1 SWW:KEC 2026-04-04 22:08:51
einzel-2 0:1 2026-04-04 22:08:51
einzel-3 KEC:SWW 2026-04-04 22:08:51
einzel-4 4:2 2026-04-04 22:08:51
einzel-5 SWW:KEC 2026-04-04 22:08:51
einzel-6 2:4 2026-04-04 22:08:51
einzel-7 KEC:SWW 2026-04-04 22:08:51
einzel-8 2:1 2026-04-04 22:08:51
einzel-9 MAN:BHV 2026-04-04 22:08:51
einzel-10 5:2 2026-04-04 22:08:51
einzel-11 BHV:MAN 2026-04-04 22:08:51
einzel-12 4:5 2026-04-04 22:08:51
einzel-13 MAN:BHV 2026-04-04 22:08:51
einzel-14 5:1 2026-04-04 22:08:51
einzel-15 BHV:MAN 2026-04-04 22:08:51
einzel-16 6:1 2026-04-04 22:08:51
einzel-17 MAN:BHV 2026-04-04 22:08:51
einzel-18 4:3 2026-04-04 22:08:51
einzel-19 STR:EBB 2026-04-04 22:08:51
einzel-20 5:1 2026-04-04 22:08:51
einzel-21 EBB:STR 2026-04-04 22:08:51
einzel-22 2:1 2026-04-04 22:08:51
einzel-23 STR:EBB 2026-04-04 22:08:51
einzel-24 2:4 2026-04-04 22:08:51
einzel-25 EBB:S 2026-04-04 22:08:51
einzel-26 2:1 2026-04-04 22:08:51
einzel-27 STR:EBB 2026-04-04 22:08:51
einzel-28 2:1 2026-04-04 22:08:51
einzel-29 RBM:ING 2026-04-04 22:08:51
einzel-30 5:6 2026-04-04 22:08:51
einzel-31 ING:RBM 2026-04-04 22:08:51
einzel-32 1:6 2026-04-04 22:08:51
einzel-33 RBM:ING 2026-04-04 22:08:51
einzel-34 5:2 2026-04-04 22:08:51
einzel-35 ING:RBM 2026-04-04 22:08:51
einzel-36 7:2 2026-04-04 22:08:51
einzel-37 RBM:ING 2026-04-04 22:08:51
einzel-38 6:3 2026-04-04 22:08:51
einzel-39 BHV:NIT 2026-04-04 22:08:51
einzel-40 4:0 2026-04-04 22:08:51
einzel-41 NIT:BHV 2026-04-04 22:08:51
einzel-42 2:3 2026-04-04 22:08:51
einzel-43 WOB:SWW 2026-04-04 22:08:51
einzel-44 3:1 2026-04-04 22:08:51
einzel-45 SWW:WOB 2026-04-04 22:08:51
einzel-46 5:1 2026-04-04 22:08:51
einzel-47 WOB:SWW 2026-04-04 22:08:51
einzel-48 2:3 2026-04-04 22:08:51
name-1 Kölner Haie 2026-04-04 22:08:51
name-2 Adler Mannheim 2026-04-04 22:08:51
name-3 Kölner Haie 2026-04-04 22:08:51
name-4 Adler Mannheim 2026-04-04 22:08:51
name-5 Straubing Tigers 2026-04-04 22:08:51
name-6 EHC Red Bull München 2026-04-04 22:08:51
name-7 Pinguins Bremerhaven 2026-04-04 22:08:51
name-8 Grizzlys Wolfsburg 2026-04-04 22:08:51
standing-1 0:0 2026-04-04 22:08:51
standing-2 0:0 2026-04-04 22:08:51
standing-3 4:0 2026-04-04 22:08:51
standing-4 4:1 2026-04-04 22:08:51
standing-5 2:3 2026-04-04 22:08:51
standing-6 3:2 2026-04-04 22:08:51
standing-7 2:0 2026-04-04 22:08:51
standing-8 1:2 2026-04-04 22:08:51
Zitat von: Marko1976 am 03 April 2026, 20:58:31...wird erst hinzugefügt wenn die entsprechenden Mannschaften feststehen. Das bedeutet aber leider, dass der Inhalt der jetzt im ersten Reading landet (zur Zeit Kölner Haie, weil 1. Halbfinalbegegnung) stände dann plötzlich im 3. gefundenen Reading...Die Frage ist dann doch schon ob das so Sinn macht, da die kleinste Änderung alles durcheinander bringt.
stty -F /dev/... -a u.a. eine Baudrate von 115200 anzeigen. Wenn der Stick über 5.42.1 oder 5.42.2 initialisiert wird, dann zeigt der stty-Befehl eine Baudrate von 4800 oder 9600 an, was ja falsch ist. Entscheidend ist: an der Config habe ich nichts geändert, mit 5.42.0 läuft alles, ab 5.42.1 nicht mehr. Also muss es einen Commit in Perl geben, der Auswirkungen auf das DUOFERN-Modul hat bzw. es könnte auch an DevIO liegen, oder?##############################################################################
# $Id: 98_KebaP30.pm $
#
# 98_KebaP30.pm
#
# FHEM module to control a Keba KeContact P30 wallbox via Modbus TCP
#
# Prerequisites:
# - FHEM ModbusTCP / ModbusAttr support (98_Modbus.pm, 98_ModbusAttr.pm)
# - Keba P30 with Modbus TCP enabled (DIP switch DSW1.3 = ON)
# - Network connectivity to the wallbox on TCP port 502
#
# Usage in FHEM:
# define myKeba KebaP30 <IP-Address> [<Interval>]
# Example: define myKeba KebaP30 192.168.1.50 30
#
# set myKeba chargingCurrent 10000 (in mA, e.g. 10000 = 10A)
# set myKeba enable 1 (1=enable, 0=disable)
# set myKeba unlock 0 (0=unlock plug)
# set myKeba setEnergy 5000 (in 0.1 Wh)
# set myKeba failsafeCurrent 6000 (in mA)
# set myKeba failsafeTimeout 60 (in seconds)
# set myKeba failsafePersist 0 (0 or 1)
#
##############################################################################
package main;
use strict;
use warnings;
use IO::Socket::INET;
use DevIo;
# Modbus Function Codes
use constant FC_READ_HOLDING => 0x03;
use constant FC_WRITE_SINGLE => 0x06;
# Keba P30 Modbus Unit ID (must be 255)
use constant KEBA_UNIT_ID => 255;
# Modbus TCP Port
use constant KEBA_PORT => 502;
# Default polling interval in seconds
use constant DEFAULT_INTERVAL => 30;
# ============================================================================
# Register-Definitionen (Keba P30 Modbus TCP Programmers Guide V1.04)
# ============================================================================
# Lesbare Register (Holding Registers, FC3, UINT32 = 2 Words)
my %readRegisters = (
'chargingState' => { addr => 1000, len => 2, type => 'uint32', unit => '', desc => 'Ladezustand (0=Startup,1=NotReady,2=Ready,3=Charging,4=Error,5=Suspended)' },
'cableState' => { addr => 1004, len => 2, type => 'uint32', unit => '', desc => 'Kabelzustand' },
'errorCode' => { addr => 1006, len => 2, type => 'uint32', unit => '', desc => 'Fehlercode' },
'currentL1' => { addr => 1008, len => 2, type => 'uint32', unit => 'mA', desc => 'Ladestrom Phase 1', factor => 1 },
'currentL2' => { addr => 1010, len => 2, type => 'uint32', unit => 'mA', desc => 'Ladestrom Phase 2', factor => 1 },
'currentL3' => { addr => 1012, len => 2, type => 'uint32', unit => 'mA', desc => 'Ladestrom Phase 3', factor => 1 },
'serialNumber' => { addr => 1014, len => 2, type => 'uint32', unit => '', desc => 'Seriennummer' },
'productType' => { addr => 1016, len => 2, type => 'uint32', unit => '', desc => 'Produkttyp und Features' },
'firmwareVersion' => { addr => 1018, len => 2, type => 'uint32', unit => '', desc => 'Firmware-Version' },
'activePower' => { addr => 1020, len => 2, type => 'uint32', unit => 'mW', desc => 'Aktive Leistung', factor => 1 },
'totalEnergy' => { addr => 1036, len => 2, type => 'uint32', unit => '0.1Wh', desc => 'Gesamtenergie', factor => 0.1 },
'voltageL1' => { addr => 1040, len => 2, type => 'uint32', unit => 'V', desc => 'Spannung Phase 1', factor => 1 },
'voltageL2' => { addr => 1042, len => 2, type => 'uint32', unit => 'V', desc => 'Spannung Phase 2', factor => 1 },
'voltageL3' => { addr => 1044, len => 2, type => 'uint32', unit => 'V', desc => 'Spannung Phase 3', factor => 1 },
'powerFactor' => { addr => 1046, len => 2, type => 'uint32', unit => '0.1%', desc => 'Leistungsfaktor (cos phi)', factor => 0.1 },
'maxChargingCurrent'=> { addr => 1100, len => 2, type => 'uint32', unit => 'mA', desc => 'Max. Ladestrom der Station', factor => 1 },
'maxSupportedCurr' => { addr => 1110, len => 2, type => 'uint32', unit => 'mA', desc => 'Max. unterstuetzter Ladestrom', factor => 1 },
'rfidCard' => { addr => 1500, len => 2, type => 'uint32', unit => '', desc => 'RFID Karten UID' },
'sessionEnergy' => { addr => 1502, len => 2, type => 'uint32', unit => '0.1Wh', desc => 'Energie aktuelle Session', factor => 0.1 },
);
# Schreibbare Register (FC6, UINT16 = 1 Word)
my %writeRegisters = (
'chargingCurrent' => { addr => 5004, len => 1, type => 'uint16', unit => 'mA', desc => 'Ladestrom setzen (0 oder 6000-63000 mA)', min => 0, max => 63000 },
'setEnergy' => { addr => 5010, len => 1, type => 'uint16', unit => '0.1Wh', desc => 'Energielimit setzen (0=kein Limit)', min => 0, max => 65535 },
'unlock' => { addr => 5012, len => 1, type => 'uint16', unit => '', desc => 'Stecker entriegeln (0=unlock)', min => 0, max => 0 },
'enable' => { addr => 5014, len => 1, type => 'uint16', unit => '', desc => 'Station ein/ausschalten (0=aus, 1=ein)', min => 0, max => 1 },
'failsafeCurrent' => { addr => 5016, len => 1, type => 'uint16', unit => 'mA', desc => 'Failsafe Ladestrom (6000-32000 mA)', min => 6000, max => 32000 },
'failsafeTimeout' => { addr => 5018, len => 1, type => 'uint16', unit => 's', desc => 'Failsafe Timeout (0=deaktiviert, 10-600 s)', min => 0, max => 600 },
'failsafePersist' => { addr => 5020, len => 1, type => 'uint16', unit => '', desc => 'Failsafe persistent speichern (0 oder 1)', min => 0, max => 1 },
);
# ============================================================================
# FHEM Interface Functions
# ============================================================================
sub KebaP30_Initialize($) {
my ($hash) = @_;
$hash->{DefFn} = "KebaP30_Define";
$hash->{UndefFn} = "KebaP30_Undef";
$hash->{SetFn} = "KebaP30_Set";
$hash->{GetFn} = "KebaP30_Get";
$hash->{AttrFn} = "KebaP30_Attr";
$hash->{NotifyFn} = "KebaP30_Notify";
$hash->{AttrList} = "disable:0,1 "
. "interval "
. "timeout "
. $readingFnAttributes;
return undef;
}
# ----------------------------------------------------------------------------
# Define: define myKeba KebaP30 <IP> [<Interval>]
# ----------------------------------------------------------------------------
sub KebaP30_Define($$) {
my ($hash, $def) = @_;
my @args = split("[ \t]+", $def);
return "Usage: define <name> KebaP30 <IP-Address> [<Interval>]"
if (@args < 3 || @args > 4);
my $name = $args[0];
my $ip = $args[2];
my $interval = (@args > 3) ? int($args[3]) : DEFAULT_INTERVAL;
$hash->{HOST} = $ip;
$hash->{PORT} = KEBA_PORT;
$hash->{UNIT_ID} = KEBA_UNIT_ID;
$hash->{INTERVAL} = $interval;
$hash->{TRANSID} = 0;
$hash->{STATE} = "initialized";
# Readings initialisieren
readingsBeginUpdate($hash);
readingsBulkUpdate($hash, "state", "initialized");
readingsEndUpdate($hash, 1);
# Timer fuer zyklisches Polling starten
RemoveInternalTimer($hash);
InternalTimer(gettimeofday() + 5, "KebaP30_Poll", $hash, 0);
Log3 $name, 3, "KebaP30 ($name): defined with host=$ip interval=${interval}s";
return undef;
}
# ----------------------------------------------------------------------------
# Undef
# ----------------------------------------------------------------------------
sub KebaP30_Undef($$) {
my ($hash, $name) = @_;
RemoveInternalTimer($hash);
KebaP30_Disconnect($hash);
return undef;
}
# ----------------------------------------------------------------------------
# Notify (z.B. bei INITIALIZED)
# ----------------------------------------------------------------------------
sub KebaP30_Notify($$) {
my ($hash, $devHash) = @_;
my $name = $hash->{NAME};
return if (IsDisabled($name));
my $events = deviceEvents($devHash, 1);
return if (!$events);
foreach my $event (@{$events}) {
if ($event =~ /^INITIALIZED$/ || $event =~ /^REREADCFG$/) {
KebaP30_Poll($hash);
}
}
return undef;
}
# ----------------------------------------------------------------------------
# Attr
# ----------------------------------------------------------------------------
sub KebaP30_Attr(@) {
my ($cmd, $name, $attrName, $attrVal) = @_;
my $hash = $defs{$name};
if ($attrName eq "interval") {
if ($cmd eq "set") {
return "interval must be a positive integer" unless ($attrVal =~ /^\d+$/ && $attrVal > 0);
$hash->{INTERVAL} = $attrVal;
} else {
$hash->{INTERVAL} = DEFAULT_INTERVAL;
}
RemoveInternalTimer($hash);
InternalTimer(gettimeofday() + $hash->{INTERVAL}, "KebaP30_Poll", $hash, 0);
}
if ($attrName eq "disable") {
if ($cmd eq "set" && $attrVal) {
RemoveInternalTimer($hash);
readingsSingleUpdate($hash, "state", "disabled", 1);
} else {
readingsSingleUpdate($hash, "state", "initialized", 1);
InternalTimer(gettimeofday() + 2, "KebaP30_Poll", $hash, 0);
}
}
return undef;
}
# ============================================================================
# Set-Befehle
# ============================================================================
sub KebaP30_Set($@) {
my ($hash, @a) = @_;
my $name = shift @a;
my $cmd = shift @a;
my $val = shift @a;
# Hilfe / Liste der Set-Befehle
my @cmds = sort keys %writeRegisters;
return "Unknown argument $cmd, choose one of " . join(" ", @cmds)
if (!defined($writeRegisters{$cmd}));
return "$cmd requires a numeric value" unless (defined($val) && $val =~ /^\d+$/);
my $reg = $writeRegisters{$cmd};
# Wertbereich pruefen (spezielle Behandlung fuer chargingCurrent: 0 oder 6000-63000)
if ($cmd eq 'chargingCurrent') {
return "$cmd: value must be 0 or between 6000 and 63000 mA"
unless ($val == 0 || ($val >= 6000 && $val <= 63000));
} elsif ($cmd eq 'failsafeTimeout') {
return "$cmd: value must be 0 or between 10 and 600"
unless ($val == 0 || ($val >= 10 && $val <= 600));
} else {
return "$cmd: value must be between $reg->{min} and $reg->{max}"
if ($val < $reg->{min} || $val > $reg->{max});
}
# Modbus Write
my $result = KebaP30_ModbusWrite($hash, $reg->{addr}, $val);
if (defined($result)) {
readingsSingleUpdate($hash, "last_set_$cmd", $val, 1);
Log3 $name, 4, "KebaP30 ($name): set $cmd = $val";
return undef;
} else {
return "KebaP30 ($name): Error writing register $reg->{addr} for $cmd";
}
}
# ============================================================================
# Get-Befehle
# ============================================================================
sub KebaP30_Get($@) {
my ($hash, @a) = @_;
my $name = shift @a;
my $cmd = shift @a;
my @cmds = sort keys %readRegisters;
push @cmds, "update";
return "Unknown argument $cmd, choose one of " . join(" ", @cmds)
if (!grep { $_ eq $cmd } @cmds);
if ($cmd eq "update") {
KebaP30_Poll($hash);
return "Polling all registers...";
}
my $reg = $readRegisters{$cmd};
my $val = KebaP30_ModbusRead($hash, $reg->{addr}, $reg->{len});
if (defined($val)) {
my $factor = $reg->{factor} // 1;
my $display = ($factor != 1) ? sprintf("%.1f", $val * $factor) : $val;
my $unit = $reg->{unit} || '';
return "$cmd: $display $unit";
} else {
return "Error reading $cmd from register $reg->{addr}";
}
}
# ============================================================================
# Polling (Timer-basiert)
# ============================================================================
sub KebaP30_Poll($) {
my ($hash) = @_;
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
return if (IsDisabled($name));
Log3 $name, 5, "KebaP30 ($name): polling registers...";
readingsBeginUpdate($hash);
my $errorCount = 0;
foreach my $reading (sort keys %readRegisters) {
my $reg = $readRegisters{$reading};
my $val = KebaP30_ModbusRead($hash, $reg->{addr}, $reg->{len});
if (defined($val)) {
my $factor = $reg->{factor} // 1;
my $display = ($factor != 1) ? sprintf("%.1f", $val * $factor) : $val;
readingsBulkUpdate($hash, $reading, $display);
} else {
$errorCount++;
Log3 $name, 4, "KebaP30 ($name): Error reading $reading (register $reg->{addr})";
}
}
# Abgeleitete Readings
my $chargingState = ReadingsVal($name, "chargingState", "");
my %stateMap = (
0 => "startup",
1 => "not_ready",
2 => "ready",
3 => "charging",
4 => "error",
5 => "suspended",
);
my $stateText = $stateMap{$chargingState} // "unknown ($chargingState)";
readingsBulkUpdate($hash, "chargingStateText", $stateText);
# Leistung in kW
my $powerMw = ReadingsVal($name, "activePower", 0);
if ($powerMw =~ /^\d+$/) {
readingsBulkUpdate($hash, "activePower_kW", sprintf("%.3f", $powerMw / 1000000));
}
# Gesamtenergie in kWh
my $totalE = ReadingsVal($name, "totalEnergy", 0);
if ($totalE =~ /^[\d.]+$/) {
readingsBulkUpdate($hash, "totalEnergy_kWh", sprintf("%.3f", $totalE / 1000));
}
# Session-Energie in kWh
my $sessE = ReadingsVal($name, "sessionEnergy", 0);
if ($sessE =~ /^[\d.]+$/) {
readingsBulkUpdate($hash, "sessionEnergy_kWh", sprintf("%.3f", $sessE / 1000));
}
if ($errorCount == 0) {
readingsBulkUpdate($hash, "state", "connected");
} else {
readingsBulkUpdate($hash, "state", "polling_errors: $errorCount");
}
readingsEndUpdate($hash, 1);
# Naechsten Poll-Zyklus einplanen
my $interval = $hash->{INTERVAL} || DEFAULT_INTERVAL;
InternalTimer(gettimeofday() + $interval, "KebaP30_Poll", $hash, 0);
return undef;
}
# ============================================================================
# Modbus TCP Kommunikation
# ============================================================================
sub KebaP30_Connect($) {
my ($hash) = @_;
my $name = $hash->{NAME};
my $host = $hash->{HOST};
my $port = $hash->{PORT};
my $timeout = AttrVal($name, "timeout", 3);
my $sock = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => $port,
Proto => 'tcp',
Timeout => $timeout,
);
if (!$sock) {
Log3 $name, 2, "KebaP30 ($name): Cannot connect to $host:$port - $!";
return undef;
}
$sock->autoflush(1);
Log3 $name, 4, "KebaP30 ($name): Connected to $host:$port";
return $sock;
}
sub KebaP30_Disconnect($) {
my ($hash) = @_;
if ($hash->{SOCKET}) {
close($hash->{SOCKET});
delete $hash->{SOCKET};
}
}
# Naechste Transaction-ID
sub KebaP30_NextTransId($) {
my ($hash) = @_;
$hash->{TRANSID} = ($hash->{TRANSID} + 1) & 0xFFFF;
return $hash->{TRANSID};
}
# ----------------------------------------------------------------------------
# Modbus Read (FC3) - Liest ein UINT32 (2 Register) oder UINT16 (1 Register)
# ----------------------------------------------------------------------------
sub KebaP30_ModbusRead($$$) {
my ($hash, $addr, $len) = @_;
my $name = $hash->{NAME};
my $sock = KebaP30_Connect($hash);
return undef unless $sock;
my $transId = KebaP30_NextTransId($hash);
my $unitId = KEBA_UNIT_ID;
# Modbus TCP ADU: TransID(2) + ProtocolID(2) + Length(2) + UnitID(1) + FC(1) + Addr(2) + Qty(2)
my $request = pack("nnnCCnn",
$transId, # Transaction ID
0, # Protocol ID (Modbus)
6, # Length (UnitID + FC + Addr + Qty = 6 bytes)
$unitId, # Unit ID
FC_READ_HOLDING, # Function Code 3
$addr, # Starting Register Address
$len # Quantity of Registers
);
my $written = $sock->send($request);
if (!$written) {
Log3 $name, 2, "KebaP30 ($name): Send error for register $addr";
close($sock);
return undef;
}
# Antwort lesen (max. 256 Bytes, Timeout beachten)
my $timeout = AttrVal($name, "timeout", 3);
my $rin = '';
vec($rin, fileno($sock), 1) = 1;
my $nfound = select($rin, undef, undef, $timeout);
if ($nfound <= 0) {
Log3 $name, 2, "KebaP30 ($name): Timeout reading register $addr";
close($sock);
return undef;
}
my $response = '';
$sock->recv($response, 256);
close($sock);
if (length($response) < 9) {
Log3 $name, 2, "KebaP30 ($name): Short response for register $addr (" . length($response) . " bytes)";
return undef;
}
# Antwort parsen
my ($rTransId, $rProtoId, $rLen, $rUnitId, $rFC, $byteCount) = unpack("nnnCCC", $response);
# Fehlercode pruefen (FC + 0x80)
if ($rFC & 0x80) {
my $exCode = unpack("C", substr($response, 8, 1));
Log3 $name, 2, "KebaP30 ($name): Modbus exception $exCode for register $addr";
return undef;
}
# Datenbytes extrahieren
my $data = substr($response, 9);
if ($len == 2 && length($data) >= 4) {
# UINT32: Big-Endian, 2 Register = 4 Bytes
return unpack("N", $data);
} elsif ($len == 1 && length($data) >= 2) {
# UINT16: Big-Endian, 1 Register = 2 Bytes
return unpack("n", $data);
}
Log3 $name, 2, "KebaP30 ($name): Unexpected data length for register $addr";
return undef;
}
# ----------------------------------------------------------------------------
# Modbus Write Single Register (FC6) - Schreibt UINT16
# ----------------------------------------------------------------------------
sub KebaP30_ModbusWrite($$$) {
my ($hash, $addr, $value) = @_;
my $name = $hash->{NAME};
my $sock = KebaP30_Connect($hash);
return undef unless $sock;
my $transId = KebaP30_NextTransId($hash);
my $unitId = KEBA_UNIT_ID;
# Modbus TCP ADU: TransID(2) + ProtocolID(2) + Length(2) + UnitID(1) + FC(1) + Addr(2) + Value(2)
my $request = pack("nnnCCnn",
$transId, # Transaction ID
0, # Protocol ID
6, # Length
$unitId, # Unit ID
FC_WRITE_SINGLE, # Function Code 6
$addr, # Register Address
$value # Value
);
my $written = $sock->send($request);
if (!$written) {
Log3 $name, 2, "KebaP30 ($name): Send error writing register $addr";
close($sock);
return undef;
}
# Antwort lesen
my $timeout = AttrVal($name, "timeout", 3);
my $rin = '';
vec($rin, fileno($sock), 1) = 1;
my $nfound = select($rin, undef, undef, $timeout);
if ($nfound <= 0) {
Log3 $name, 2, "KebaP30 ($name): Timeout writing register $addr";
close($sock);
return undef;
}
my $response = '';
$sock->recv($response, 256);
close($sock);
if (length($response) < 12) {
Log3 $name, 2, "KebaP30 ($name): Short write response for register $addr";
return undef;
}
my ($rTransId, $rProtoId, $rLen, $rUnitId, $rFC) = unpack("nnnCC", $response);
if ($rFC & 0x80) {
my $exCode = unpack("C", substr($response, 8, 1));
Log3 $name, 2, "KebaP30 ($name): Modbus write exception $exCode for register $addr";
return undef;
}
Log3 $name, 4, "KebaP30 ($name): Wrote value $value to register $addr";
return 1;
}
1;
# ============================================================================
# Commandref-Dokumentation (fuer FHEM)
# ============================================================================
=pod
=item device
=item summary Keba KeContact P30 Wallbox via Modbus TCP
=item summary_DE Keba KeContact P30 Wallbox ueber Modbus TCP steuern
=begin html
<a name="KebaP30"></a>
<h3>KebaP30</h3>
<ul>
FHEM module to monitor and control a Keba KeContact P30 wallbox via Modbus TCP.<br>
The wallbox must have Modbus TCP enabled (DIP switch DSW1.3 = ON).<br><br>
<a name="KebaP30define"></a>
<b>Define</b>
<ul>
<code>define <name> KebaP30 <IP-Address> [<Interval>]</code><br><br>
Example: <code>define myKeba KebaP30 192.168.1.50 30</code><br>
Interval is the polling interval in seconds (default: 30).
</ul><br>
<a name="KebaP30set"></a>
<b>Set</b>
<ul>
<li><b>chargingCurrent</b> <mA> - Set charging current (0 or 6000-63000 mA)</li>
<li><b>enable</b> <0|1> - Enable (1) or disable (0) charging station</li>
<li><b>unlock</b> 0 - Unlock the charging plug</li>
<li><b>setEnergy</b> <0.1Wh> - Set energy limit (0 = no limit)</li>
<li><b>failsafeCurrent</b> <mA> - Failsafe current (6000-32000 mA)</li>
<li><b>failsafeTimeout</b> <s> - Failsafe timeout (0=off, 10-600 s)</li>
<li><b>failsafePersist</b> <0|1> - Persist failsafe settings</li>
</ul><br>
<a name="KebaP30get"></a>
<b>Get</b>
<ul>
<li><b>update</b> - Force immediate polling of all registers</li>
<li><b>chargingState</b> - Current charging state</li>
<li><b>cableState</b> - Cable connection state</li>
<li><b>errorCode</b> - Error code</li>
<li><b>currentL1/L2/L3</b> - Charging current per phase (mA)</li>
<li><b>voltageL1/L2/L3</b> - Voltage per phase (V)</li>
<li><b>activePower</b> - Active power (mW)</li>
<li><b>totalEnergy</b> - Total charged energy</li>
<li><b>sessionEnergy</b> - Energy of current session</li>
<li><b>powerFactor</b> - Power factor (cos phi)</li>
<li><b>serialNumber</b> - Serial number</li>
<li><b>firmwareVersion</b> - Firmware version</li>
<li>... and more (see register definitions)</li>
</ul><br>
<a name="KebaP30readings"></a>
<b>Readings</b>
<ul>
All readable registers are available as readings. Additional computed readings:
<li><b>chargingStateText</b> - Human-readable charging state</li>
<li><b>activePower_kW</b> - Active power in kW</li>
<li><b>totalEnergy_kWh</b> - Total energy in kWh</li>
<li><b>sessionEnergy_kWh</b> - Session energy in kWh</li>
</ul><br>
<a name="KebaP30attr"></a>
<b>Attributes</b>
<ul>
<li><b>interval</b> - Polling interval in seconds (default: 30)</li>
<li><b>timeout</b> - Connection timeout in seconds (default: 3)</li>
<li><b>disable</b> - Disable the device (0|1)</li>
</ul>
</ul>
=end html
=cut
ZitatInstallation & Nutzung:
Die Datei als 98_KebaP30.pm ins FHEM-Modulverzeichnis (üblicherweise /opt/fhem/FHEM/) legen, dann in FHEM:
reload 98_KebaP30.pm
define myKeba KebaP30 192.168.1.50 30
Einige Hinweise zur Keba P30:
Der TCP-Port 502 ist reserviert und die Unit ID muss auf 255 gesetzt sein Wallbox Center – das ist im Modul bereits korrekt konfiguriert.
Modbus TCP wird über den DIP-Schalter DSW1.3 auf der Platine aktiviert TapHome, danach muss die Wallbox neu gestartet werden.
Es ist nicht möglich, mehrere Register gleichzeitig zu lesen – die maximale Leselänge beträgt 2 Words Wallbox Center, weshalb das Modul jedes Register einzeln abfragt.
Das empfohlene Timing-Intervall zum Lesen beträgt mindestens 0,5 Sekunden Wallbox Center – das Modul hält dies durch sequenzielles Lesen automatisch ein.