From 3fff12c894b584da7ce8801933911bd5d39dc51a Mon Sep 17 00:00:00 2001 From: sidey79 Date: Sun, 18 Jan 2026 23:52:47 +0100 Subject: [PATCH 1/5] feat: support mqtt in define --- FHEM/00_SIGNALduino.pm | 60 +++++++++++--- lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm | 44 +++++++++++ t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t | 75 ++++++++++++++---- t/FHEM/Devices/SIGNALduino/SD_MQTT.t | 79 +++++++++++++++++++ 4 files changed, 234 insertions(+), 24 deletions(-) create mode 100644 lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm create mode 100644 t/FHEM/Devices/SIGNALduino/SD_MQTT.t diff --git a/FHEM/00_SIGNALduino.pm b/FHEM/00_SIGNALduino.pm index 52ab2103b..4dcac6424 100644 --- a/FHEM/00_SIGNALduino.pm +++ b/FHEM/00_SIGNALduino.pm @@ -37,6 +37,8 @@ use FHEM::Devices::SIGNALduino::SD_Clients; use FHEM::Devices::SIGNALduino::SD_Message; use FHEM::Devices::SIGNALduino::SD_Matchlist; use List::Util qw(first); +use FHEM::Utility::MQTT2_Dispatcher qw(:DEFAULT); +use FHEM::Devices::SIGNALduino::SD_MQTT; #$| = 1; #Puffern abschalten, Hilfreich fuer PEARL WARNINGS Search @@ -306,28 +308,42 @@ sub SIGNALduino_Define { my ($hash, $def) = @_; my @a =split m{\s+}xms, $def; - if(@a != 3) { - my $msg = 'Define, wrong parameter count: define SIGNALduino {none | devicename[\@baudrate] | devicename\@directio | hostname:port}'; + my $name = $a[0]; + my $dev = $a[2]; + my $mqtt_topic; + + if ($dev eq 'mqtt') { + if(@a < 3 || @a > 4) { + my $msg = 'Define, wrong mqtt syntax: define SIGNALduino mqtt []'; + Log3 undef, 2, $msg; + return $msg; + } + my $basetopic = $a[3] // 'signalduino/v1'; + $mqtt_topic = "$basetopic/state/messages"; + } elsif(@a != 3) { + my $msg = 'Define, wrong parameter count: define SIGNALduino {none | devicename[\@baudrate] | devicename\@directio | hostname:port | mqtt []}'; Log3 undef, 2, $msg; return $msg; } DevIo_CloseDev($hash); - my $name = $a[0]; - my $dev = $a[2]; + #$hash->{debugMethod}->(qq[dev: $dev]); + #my $hardware=AttrVal($name,'hardware','nano'); + #$hash->{debugMethod}->(qq[hardware: $hardware]); if($dev eq 'none') { Log3 $name, 1, "$name: Define, device is none, commands will be echoed only"; $attr{$name}{dummy} = 1; - } elsif ($dev !~ m/\@/) { + } elsif ($dev !~ m/\@/ && $dev ne 'mqtt') { + Log3 $name, 5, "$name: Define, device is $dev"; if ( ($dev =~ m~^(?:/[^/ ]*)+?$~xms || $dev =~ m~^COM\d$~xms) ) # bei einer IP oder hostname wird kein \@57600 angehaengt { $dev .= '@57600' } elsif ($dev !~ /@\d+$/ && ($dev !~ /^ (?: (?:[a-z0-9-]+(?:\.[a-z]{2,6})?)*|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])) - : (?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3})$/xmsi) ) { # bei einer IP oder hostname wird kein \@57600 angehaengt{ - my $msg = "Define, wrong hostname/port syntax ($dev): define SIGNALduino {none | devicename[\@baudrate] | devicename\@directio | hostname:port}"; + : (?:6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3})$/xmsi) ) { + my $msg = qq[Define, wrong hostname/port/mqtt syntax ($dev): define SIGNALduino {none | devicename[\@] | devicename\@directio | hostname:port | mqtt []}]; Log3 undef, 2, $msg; $hash->{STATE} = 'error'; return $msg; @@ -347,11 +363,20 @@ sub SIGNALduino_Define { FHEM::Core::Timer::Helper::addTimer($name, time(), \&SIGNALduino_IdList,"sduino_IdList:$name",0 ); - if($dev ne 'none') { - $ret = DevIo_OpenDev($hash, 0, \&SIGNALduino_DoInit, \&SIGNALduino_Connect); - } else { + if ($dev eq 'none') { $hash->{DevState} = 'initialized'; readingsSingleUpdate($hash, 'state', 'opened', 1); + } elsif ($dev eq 'mqtt') { + $hash->{DevState} = 'initialized'; + readingsSingleUpdate($hash, 'state', 'opened', 1); + $hash->{Listener} = on_mqtt($mqtt_topic, sub { + FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, @_); + }) or do { + $hash->{logMethod}->($hash, 2, "$hash->{NAME}: Error creating MQTT listener: $@"); + }; + $hash->{mqttSubscribe} = $mqtt_topic; + } else { + $ret = DevIo_OpenDev($hash, 0, \&SIGNALduino_DoInit, \&SIGNALduino_Connect); } $hash->{DMSG} = 'nothing'; @@ -384,6 +409,11 @@ sub SIGNALduino_Undef { my ($hash, $arg) = @_; my $name = $hash->{NAME}; + if (defined($hash->{Listener})) { + del_mqtt($hash->{Listener}); + delete $hash->{Listener}; + } + foreach my $d (sort keys %defs) { if(defined($defs{$d}) && defined($defs{$d}{IODev}) && @@ -4277,6 +4307,13 @@ USB-connected devices (SIGNALduino):
If the baudrate is "directio" (e.g.: /dev/ttyACM0@directio), then the perl module Device::SerialPort is not needed, and fhem opens the device with simple file io. This might work if the operating system uses sane defaults for the serial parameters, e.g. some Linux distributions and OSX.

+MQTT-connected devices:
+
    +
  • + mqtt uses MQTT as transport instead of serial or telnet. +
    is optional (default: signalduino/v1). +
  • +
@@ -4870,6 +4907,9 @@ USB-connected devices (SIGNALduino):
WiFi-connected devices (SIGNALduino):

Bei einem ESP8266 oder ESP32 wird anstelle des seriellen Ports die IP des SIGNALduino in der Form xxx.xxx.xxx.xxx:23 angegeben. Dabei stellt die 23 den verwendeten Port dar.

+ MQTT-connected devices:

+ mqtt nutzt MQTT als Transportweg anstelle von seriell oder Telnet. +
ist optional (Standard: signalduino/v1). diff --git a/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm b/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm new file mode 100644 index 000000000..a2e09a21b --- /dev/null +++ b/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm @@ -0,0 +1,44 @@ +package FHEM::Devices::SIGNALduino::SD_MQTT; + +use strict; +use warnings; +use FHEM::Devices::SIGNALduino::SD_Logger; +use FHEM::Devices::SIGNALduino::SD_Message; + +sub on_message { + my ($hash, $topic, $value, $io_name) = @_; + + # Process message topic + if ($topic =~ m{state/messages}) { + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 5, "SIGNALduino MQTT message ignored: topic=$topic, value=$value"); + FHEM::Devices::SIGNALduino::SD_Message::json2Dispatch($value, $hash->{NAME}); + return; + } + + return; +} + +1; + +=pod + +=head1 NAME + +FHEM::Devices::SIGNALduino::SD_MQTT - MQTT handler for SIGNALduino + +=head1 SYNOPSIS + + use FHEM::Devices::SIGNALduino::SD_MQTT; + FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, $topic, $value, $io_name); + +=head1 DESCRIPTION + +Handles incoming MQTT messages for SIGNALduino. + +=head1 FUNCTIONS + +=head2 on_message($hash, $topic, $value, $io_name) + +Callback function for MQTT2_Dispatcher. + +=cut diff --git a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t index cf8a84ad5..30fc53357 100644 --- a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t +++ b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t @@ -112,7 +112,7 @@ InternalTimer(time()+0.8, sub { }, { DEF => q{Hostname:65536}, - testname => q[Hostname|Linux|Invalid: port out of range], + testname => q[Hostname|Linux|Invalid:], check => hash { field STATE => q{error}; field DeviceName => U(); @@ -121,20 +121,22 @@ InternalTimer(time()+0.8, sub { }, rValue => D(), }, + { - DEF => q{192:168:122:57:45476}, - testname => q[IPv4|Linux|Invalid: colon instead of dot], + DEF => q{192:168:122:56:45476}, + testname => q[Hostname|Linux|Invalid:], check => hash { field STATE => q{error}; field DeviceName => U(); - field DEF => q{192:168:122:57:45476}; + field DEF => q{192:168:122:56:45476}; etc(); }, rValue => D(), }, + { DEF => q{192.168.122.56:44323}, - testname => q[IPv4|Linux|Valid:], + testname => q[Hostname|Linux|Valid:IPv4], check => hash { field STATE => q{disconnected}; field DeviceName => q{192.168.122.56:44323}; @@ -142,10 +144,10 @@ InternalTimer(time()+0.8, sub { etc(); }, rValue => U(), - }, + }, { DEF => q{192.168.122:44323}, - testname => q[IPv4|Linux|Invalid: ip incomplete], + testname => q[Hostname|Linux|Invalid:IPv4], check => hash { field STATE => q{error}; field DeviceName => U(); @@ -156,7 +158,7 @@ InternalTimer(time()+0.8, sub { }, { DEF => q{sernetgw:44323}, - testname => q[Hostname|Linux|Valid:], + testname => q[Hostname|Linux|Valid:hostname], check => hash { field STATE => q{disconnected}; field DeviceName => q{sernetgw:44323}; @@ -167,7 +169,7 @@ InternalTimer(time()+0.8, sub { }, { DEF => q{sernetgw.local.host:44323}, - testname => q[Hostname|Linux|Valid:], + testname => q[Hostname|Linux|Valid:hostname with domain], check => hash { field STATE => q{disconnected}; field DeviceName => q{sernetgw.local.host:44323}; @@ -178,26 +180,67 @@ InternalTimer(time()+0.8, sub { }, { DEF => q{sernetgw.local.host}, - testname => q[Hostname|Linux|inValid: missing port], + testname => q[Hostname|Linux|inValid:hostname without port], check => hash { field STATE => q{error}; field DeviceName => U(); field DEF => q{sernetgw.local.host}; etc(); }, - rValue => D(), + rValue => D(), }, { DEF => q{ESP-DB7D13-Testboard:23}, - testname => q[Hostname|Linux|Valid:], + testname => q[Hostname|Linux|Valid:hostname with dash], check => hash { field STATE => q{disconnected}; field DeviceName => q{ESP-DB7D13-Testboard:23}; field DEF => q{ESP-DB7D13-Testboard:23}; etc(); }, + rValue => U(), + }, + { + plan => 3, + DEF => q{mqtt}, + testname => q[MQTT|Valid: default basetopic], + check => hash { + field STATE => q{opened}; + field DEF => q{mqtt}; + field mqttSubscribe => q{signalduino/v1/state/messages}; + field protocolObject => object { + prop isa => 'lib::SD_Protocols'; + prop reftype => 'HASH'; + call [qw(_logging testmessage 1)] => validator(sub { + return is(FhemTestUtils_gotLog(qr/\sdefTest:.*testmessage/),1,q[verify logging message]); + }); + }; + etc(); + }, rValue => U(), }, + + { + plan => 3, + DEF => q{mqtt mytopic}, + testname => q[MQTT|Valid: with basetopic], + check => hash { + field STATE => q{opened}; + field DeviceName => q{mqtt}; + field DEF => q{mqtt mytopic}; + field mqttSubscribe => q{mytopic/state/messages}; + field protocolObject => object { + prop isa => 'lib::SD_Protocols'; + prop reftype => 'HASH'; + call [qw(_logging testmessage 1)] => validator(sub { + return is(FhemTestUtils_gotLog(qr/\sdefTest:.*testmessage/),1,q[verify logging message]); + }); + }; + etc(); + }, + rValue => U(), + }, + { DEF => q{ESP-0CAD2F:23}, testname => q[Hostname|Linux|Valid:], @@ -248,6 +291,7 @@ InternalTimer(time()+0.8, sub { }, rValue => U(), }, + { NAME => q{logTest}, plan => 3, @@ -269,8 +313,9 @@ InternalTimer(time()+0.8, sub { }, rValue => U(), }, + ); - + while (@testDataset) { my $element = pop(@testDataset); @@ -286,7 +331,9 @@ InternalTimer(time()+0.8, sub { $hash{NAME} = $element->{NAME} // $name; FhemTestUtils_resetLogs(); $hash{DEF} = $element->{DEF}; - plan( $element->{plan} // 2 ); + plan( $element->{plan} // 2 ); + + note "Testing with Name=$hash{NAME} TYPE=$hash{TYPE} DEF=$hash{DEF}"; my $ret = SIGNALduino_Define(\%hash,qq{$hash{NAME} $hash{TYPE} $hash{DEF} }); is ($ret, $element->{rValue}, 'check returnvalue SIGNALduino_Define'); diff --git a/t/FHEM/Devices/SIGNALduino/SD_MQTT.t b/t/FHEM/Devices/SIGNALduino/SD_MQTT.t new file mode 100644 index 000000000..429b957dc --- /dev/null +++ b/t/FHEM/Devices/SIGNALduino/SD_MQTT.t @@ -0,0 +1,79 @@ +use Test2::V0; +use lib 'lib'; + +# Load the module to be tested +require FHEM::Devices::SIGNALduino::SD_MQTT; + +# Create mocks +my $mock_logger = mock 'FHEM::Devices::SIGNALduino::SD_Logger' => ( + track => 1, # Enable call tracking if needed, though we use overrides below +); + +my $mock_message = mock 'FHEM::Devices::SIGNALduino::SD_Message' => ( + track => 1, +); + +my $hash = { NAME => 'MySIGNALduino' }; +my $io_name = 'MyIO'; + +subtest 'Topic matches /state/messages/' => sub { + plan(7); + my $topic = 'some/state/messages/test'; + my $value = '{"foo":"bar"}'; + + my $log_called = 0; + my $dispatch_called = 0; + + # Override Log to verify arguments + $mock_logger->override( + Log => sub { + my ($h, $lvl, $msg) = @_; + $log_called++; + is($h, $hash, 'Log: Hash matches'); + is($lvl, 5, 'Log: Level is 5'); + like($msg, qr/SIGNALduino MQTT message ignored: topic=\Q$topic\E, value=\Q$value\E/, 'Log: Message matches'); + return; + } + ); + + # Override json2Dispatch to verify arguments + $mock_message->override( + json2Dispatch => sub { + my ($val, $name) = @_; + $dispatch_called++; + is($val, $value, 'json2Dispatch: Value matches'); + is($name, $hash->{NAME}, 'json2Dispatch: Name matches'); + return; + } + ); + + FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, $topic, $value, $io_name); + + ok($log_called, 'SD_Logger::Log was called'); + ok($dispatch_called, 'SD_Message::json2Dispatch was called'); +}; + +subtest 'Topic does NOT match /state/messages/' => sub { + plan(2); + my $topic = 'some/other/topic'; + my $value = '{"foo":"bar"}'; + + my $log_called = 0; + my $dispatch_called = 0; + + # Override with simple counters + $mock_logger->override( + Log => sub { $log_called++; } + ); + + $mock_message->override( + json2Dispatch => sub { $dispatch_called++; } + ); + + FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, $topic, $value, $io_name); + + is($log_called, 0, 'SD_Logger::Log was NOT called'); + is($dispatch_called, 0, 'SD_Message::json2Dispatch was NOT called'); +}; + +done_testing; From ac709c90f3927b5246f4e1e2cb9ee9cd6b5b9516 Mon Sep 17 00:00:00 2001 From: sidey79 Date: Sun, 18 Jan 2026 23:53:00 +0100 Subject: [PATCH 2/5] feat: implement MQTT listener initialization and delayed handling --- FHEM/00_SIGNALduino.pm | 14 +- lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm | 42 +++++- t/FHEM/Devices/SIGNALduino/SD_MQTT.t | 178 ++++++++++++++++++++++-- 3 files changed, 208 insertions(+), 26 deletions(-) diff --git a/FHEM/00_SIGNALduino.pm b/FHEM/00_SIGNALduino.pm index 4dcac6424..bb717166d 100644 --- a/FHEM/00_SIGNALduino.pm +++ b/FHEM/00_SIGNALduino.pm @@ -310,8 +310,6 @@ sub SIGNALduino_Define { my $name = $a[0]; my $dev = $a[2]; - my $mqtt_topic; - if ($dev eq 'mqtt') { if(@a < 3 || @a > 4) { my $msg = 'Define, wrong mqtt syntax: define SIGNALduino mqtt []'; @@ -319,7 +317,7 @@ sub SIGNALduino_Define { return $msg; } my $basetopic = $a[3] // 'signalduino/v1'; - $mqtt_topic = "$basetopic/state/messages"; + $hash->{mqttSubscribe} = "$basetopic/state/messages"; } elsif(@a != 3) { my $msg = 'Define, wrong parameter count: define SIGNALduino {none | devicename[\@baudrate] | devicename\@directio | hostname:port | mqtt []}'; Log3 undef, 2, $msg; @@ -369,12 +367,8 @@ sub SIGNALduino_Define { } elsif ($dev eq 'mqtt') { $hash->{DevState} = 'initialized'; readingsSingleUpdate($hash, 'state', 'opened', 1); - $hash->{Listener} = on_mqtt($mqtt_topic, sub { - FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, @_); - }) or do { - $hash->{logMethod}->($hash, 2, "$hash->{NAME}: Error creating MQTT listener: $@"); - }; - $hash->{mqttSubscribe} = $mqtt_topic; + + FHEM::Devices::SIGNALduino::SD_MQTT::Init($name); } else { $ret = DevIo_OpenDev($hash, 0, \&SIGNALduino_DoInit, \&SIGNALduino_Connect); } @@ -3136,7 +3130,7 @@ sub SIGNALduino_FW_Detail { my $hash = $defs{$name}; my @dspec=devspec2array("DEF=.*fakelog"); - my $lfn = $dspec[0]; + my $lfn = $dspec[0] // ""; my $fn=$defs{$name}->{TYPE}."-Flash.log"; my $ret = "
Information menu diff --git a/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm b/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm index a2e09a21b..37bbd978d 100644 --- a/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm +++ b/lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm @@ -1,20 +1,58 @@ +# $Id: SD_MQTT.pm 0 2026-01-10 15:36:13Z sidey79 $ +# The file is part of the SIGNALduino project. +# MQTT helper for SIGNALduino device messages. + + package FHEM::Devices::SIGNALduino::SD_MQTT; use strict; use warnings; use FHEM::Devices::SIGNALduino::SD_Logger; use FHEM::Devices::SIGNALduino::SD_Message; +eval { + require FHEM::Utility::MQTT2_Dispatcher; + FHEM::Utility::MQTT2_Dispatcher->import(qw(:DEFAULT)); +} ; +eval { require FHEM::Core::Timer::Helper; } ; + +sub Init { + my $name = shift; + my $hash = $main::defs{$name}; + + if (!$main::init_done) { + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 3, "$name: InitMQTT, init not done, delayed..."); + FHEM::Core::Timer::Helper::addTimer($name, time() + 0, \&Init, $name, 0); + return; + } + + my $mqtt_topic = $hash->{mqttSubscribe}; + if (defined($mqtt_topic)) { + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 3, "$name: InitMQTT, registering listener for $mqtt_topic"); + + if (defined($hash->{Listener})) { + del_mqtt($hash->{Listener}); + delete $hash->{Listener}; + } + + $hash->{Listener} = on_mqtt($mqtt_topic, sub { + on_message($hash, @_); + }) or do { + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 2, "$name: Error creating MQTT listener: $@"); + }; + } else { + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 2, "$name: InitMQTT, no mqttSubscribe topic found"); + } +} sub on_message { my ($hash, $topic, $value, $io_name) = @_; # Process message topic if ($topic =~ m{state/messages}) { - FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 5, "SIGNALduino MQTT message ignored: topic=$topic, value=$value"); FHEM::Devices::SIGNALduino::SD_Message::json2Dispatch($value, $hash->{NAME}); return; } - + FHEM::Devices::SIGNALduino::SD_Logger::Log($hash, 5, "SIGNALduino MQTT message ignored: topic=$topic, value=$value"); return; } diff --git a/t/FHEM/Devices/SIGNALduino/SD_MQTT.t b/t/FHEM/Devices/SIGNALduino/SD_MQTT.t index 429b957dc..9b4a0c0a7 100644 --- a/t/FHEM/Devices/SIGNALduino/SD_MQTT.t +++ b/t/FHEM/Devices/SIGNALduino/SD_MQTT.t @@ -1,6 +1,31 @@ use Test2::V0; use lib 'lib'; +my $mock_Timer = mock 'FHEM::Core::Timer::Helper' => ( + track => 1, # Enable call tracking if needed, though we use overrides below + add => { + addTimer => sub { + my ($name, $time, $func, $param, $repeat) = @_; + return 1; # Mocked behavior + }, + }, +); + +my $mock_MQTT = mock 'FHEM::Devices::SIGNALduino::SD_MQTT' => ( + track => 1, # Enable call tracking if needed, though we use overrides below + add => { + on_mqtt => sub { + my ($self, $subscription, $fn) = @_; + return 1; # Mocked behavior + }, + del_mqtt => sub { + my ($self, $listener) = @_; + return 1; # Mocked behavior + }, + }, +); + + # Load the module to be tested require FHEM::Devices::SIGNALduino::SD_MQTT; @@ -17,7 +42,7 @@ my $hash = { NAME => 'MySIGNALduino' }; my $io_name = 'MyIO'; subtest 'Topic matches /state/messages/' => sub { - plan(7); + plan(4); my $topic = 'some/state/messages/test'; my $value = '{"foo":"bar"}'; @@ -27,11 +52,7 @@ subtest 'Topic matches /state/messages/' => sub { # Override Log to verify arguments $mock_logger->override( Log => sub { - my ($h, $lvl, $msg) = @_; $log_called++; - is($h, $hash, 'Log: Hash matches'); - is($lvl, 5, 'Log: Level is 5'); - like($msg, qr/SIGNALduino MQTT message ignored: topic=\Q$topic\E, value=\Q$value\E/, 'Log: Message matches'); return; } ); @@ -49,31 +70,160 @@ subtest 'Topic matches /state/messages/' => sub { FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, $topic, $value, $io_name); - ok($log_called, 'SD_Logger::Log was called'); + is($log_called,0, 'SD_Logger::Log was called'); ok($dispatch_called, 'SD_Message::json2Dispatch was called'); }; +subtest 'Init (delayed)' => sub { + plan(5); + + my $addTimer_called = 0; + my $log_called = 0; + + # Set up delayed init context + local $main::init_done = 0; + local $main::defs{$hash->{NAME}} = $hash; + + $mock_Timer->override( + addTimer => sub { + my ($name, $time, $func, $param, $repeat) = @_; + $addTimer_called++; + is($func, \&FHEM::Devices::SIGNALduino::SD_MQTT::Init, 'addTimer: function is Init'); + return 1; + } + ); + + $mock_logger->override( + Log => sub { + my ($h, $lvl, $msg) = @_; + $log_called++; + is($lvl, 3, 'Log: Level is 3'); + like($msg, qr/: InitMQTT, init not done, delayed.../, 'Log: Message indicates delayed init'); + return; + } + ); + + FHEM::Devices::SIGNALduino::SD_MQTT::Init($hash->{NAME}); + + is($addTimer_called, 1, 'FHEM::Core::Timer::Helper::addTimer was called once'); + is($log_called, 1, 'SD_Logger::Log was called once'); + + $mock_Timer->restore('addTimer'); + $mock_logger->restore('Log'); +}; + +subtest 'Init (successful registration)' => sub { + plan(8); + + my $on_mqtt_called = 0; + my $del_mqtt_called = 0; + my $log_called = 0; + + local $main::init_done = 1; + local $main::defs{$hash->{NAME}} = $hash; + + $hash->{mqttSubscribe} = 'fhem/signalduino/state/messages/+'; + $hash->{Listener} = 'old_listener_handle'; # Existing listener for del_mqtt test + + $mock_MQTT->override( + on_mqtt => sub { + my ($subscription, $fn) = @_; + $on_mqtt_called++; + is($subscription, $hash->{mqttSubscribe}, 'on_mqtt: Subscription topic matches'); + return 'new_listener_handle'; # Return new listener handle + }, + del_mqtt => sub { + my ( $listener) = @_; + $del_mqtt_called++; + is($listener, 'old_listener_handle', 'del_mqtt: Called with old listener handle'); + return 1; + } + ); + + $mock_logger->override( + Log => sub { + my ($h, $lvl, $msg) = @_; + $log_called++; + is($lvl, 3, 'Log: Level is 3'); + like($msg, qr/: InitMQTT, registering listener for /, 'Log: Message indicates registration'); + return; + } + ); + + FHEM::Devices::SIGNALduino::SD_MQTT::Init($hash->{NAME}); + + is($del_mqtt_called, 1, 'del_mqtt was called once to remove old listener'); + is($on_mqtt_called, 1, 'on_mqtt was called once for new registration'); + is($hash->{Listener}, 'new_listener_handle', 'New listener handle stored in $hash->{Listener}'); + is($log_called, 1, 'SD_Logger::Log was called once (Level 3)'); + + $mock_Timer->restore('addTimer'); + $mock_logger->restore('Log'); + $mock_MQTT->restore('on_mqtt'); + $mock_MQTT->restore('del_mqtt'); +}; + +subtest 'Init (no subscribe topic)' => sub { + plan(4); + + my $on_mqtt_called = 0; + my $log_called = 0; + + local $main::init_done = 1; + local $main::defs{$hash->{NAME}} = $hash; + + delete $hash->{mqttSubscribe}; # Ensure no subscribe topic + delete $hash->{Listener}; # Ensure no old listener + + $mock_MQTT->override( + on_mqtt => sub { $on_mqtt_called++; return 1; }, + ); + + $mock_logger->override( + Log => sub { + my ($h, $lvl, $msg) = @_; + $log_called++; + is($lvl, 2, 'Log: Level is 2 (Error)'); + like($msg, qr/: InitMQTT, no mqttSubscribe topic found/, 'Log: Message indicates missing topic'); + return; + } + ); + + FHEM::Devices::SIGNALduino::SD_MQTT::Init($hash->{NAME}); + + is($on_mqtt_called, 0, 'on_mqtt was NOT called'); + is($log_called, 1, 'SD_Logger::Log was called once (Level 2)'); + $mock_MQTT->restore('on_mqtt'); + $mock_logger->restore('Log'); +}; + subtest 'Topic does NOT match /state/messages/' => sub { - plan(2); + plan(3); my $topic = 'some/other/topic'; my $value = '{"foo":"bar"}'; - + my $log_called = 0; my $dispatch_called = 0; - + # Override with simple counters $mock_logger->override( - Log => sub { $log_called++; } + Log => sub { + my ($h, $lvl, $msg) = @_; + like($msg, qr/ignored: topic=$topic/, 'Log: Message indicates missing topic'); + $log_called++; + } ); - + $mock_message->override( json2Dispatch => sub { $dispatch_called++; } ); - + FHEM::Devices::SIGNALduino::SD_MQTT::on_message($hash, $topic, $value, $io_name); - - is($log_called, 0, 'SD_Logger::Log was NOT called'); + + is($log_called, 1, 'SD_Logger::Log was called'); is($dispatch_called, 0, 'SD_Message::json2Dispatch was NOT called'); + $mock_logger->restore('Log'); + $mock_message->restore('json2Dispatch'); }; done_testing; From 13067c55bc3c201e30dc64b08cf6e98f58735b2f Mon Sep 17 00:00:00 2001 From: sidey79 Date: Sun, 18 Jan 2026 23:53:30 +0100 Subject: [PATCH 3/5] fix: tests for SIGNALduino_Define --- t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t index 30fc53357..e41ecbb1e 100644 --- a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t +++ b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t @@ -322,11 +322,14 @@ InternalTimer(time()+0.8, sub { next if (!exists($element->{testname})); my %hash; $defs{$name} = \%hash; + #SIGNALduino_Initialize(\%hash); $hash{CL} = undef; $hash{TEMPORARY} = 1; $hash{TYPE} = q{SIGNALduino}; $hash{STATE} = q{???}; - + $hash{ReadyFn} = sub { return ;}; + + subtest "$element->{testname} checking define $element->{DEF}" => sub { $hash{NAME} = $element->{NAME} // $name; FhemTestUtils_resetLogs(); From 23da67efca294906aa5ca40683965ba4e1c6d24e Mon Sep 17 00:00:00 2001 From: sidey79 Date: Sun, 18 Jan 2026 23:53:30 +0100 Subject: [PATCH 4/5] fix: removed unused code --- t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t index e41ecbb1e..69d514168 100644 --- a/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t +++ b/t/FHEM/00_SIGNALduino/01_SIGNALduino_Define.t @@ -121,7 +121,6 @@ InternalTimer(time()+0.8, sub { }, rValue => D(), }, - { DEF => q{192:168:122:56:45476}, testname => q[Hostname|Linux|Invalid:], @@ -240,7 +239,6 @@ InternalTimer(time()+0.8, sub { }, rValue => U(), }, - { DEF => q{ESP-0CAD2F:23}, testname => q[Hostname|Linux|Valid:], @@ -291,7 +289,6 @@ InternalTimer(time()+0.8, sub { }, rValue => U(), }, - { NAME => q{logTest}, plan => 3, @@ -313,7 +310,6 @@ InternalTimer(time()+0.8, sub { }, rValue => U(), }, - ); while (@testDataset) @@ -322,14 +318,11 @@ InternalTimer(time()+0.8, sub { next if (!exists($element->{testname})); my %hash; $defs{$name} = \%hash; - #SIGNALduino_Initialize(\%hash); $hash{CL} = undef; $hash{TEMPORARY} = 1; $hash{TYPE} = q{SIGNALduino}; $hash{STATE} = q{???}; - $hash{ReadyFn} = sub { return ;}; - - + subtest "$element->{testname} checking define $element->{DEF}" => sub { $hash{NAME} = $element->{NAME} // $name; FhemTestUtils_resetLogs(); From 084cf43f15b43e3dccc9bb200f3cb84429f2e4ca Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Jan 2026 22:55:55 +0000 Subject: [PATCH 5/5] Update Versiondate --- FHEM/00_SIGNALduino.pm | 4 ++-- controls_signalduino.txt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/FHEM/00_SIGNALduino.pm b/FHEM/00_SIGNALduino.pm index bb717166d..762b78c62 100644 --- a/FHEM/00_SIGNALduino.pm +++ b/FHEM/00_SIGNALduino.pm @@ -1,4 +1,4 @@ -# $Id: 00_SIGNALduino.pm 0 2026-01-16 10:03:31Z sidey79 $ +# $Id: 00_SIGNALduino.pm 0 2026-01-18 22:55:34Z sidey79 $ # https://github.com/RFD-FHEM/RFFHEM/tree/master # The module is inspired by the FHEMduino project and modified in serval ways for processing the incoming messages # see http://www.fhemwiki.de/wiki/SIGNALDuino @@ -46,7 +46,7 @@ use FHEM::Devices::SIGNALduino::SD_MQTT; use constant { - SDUINO_VERSION => '4.0.0', # Datum wird automatisch bei jedem pull request aktualisiert + SDUINO_VERSION => '4.0.0+20260118', # Datum wird automatisch bei jedem pull request aktualisiert SDUINO_INIT_WAIT_XQ => 1.5, # wait disable device SDUINO_INIT_WAIT => 2, SDUINO_INIT_MAXRETRY => 3, diff --git a/controls_signalduino.txt b/controls_signalduino.txt index 179bf293a..fa4aa3fd0 100644 --- a/controls_signalduino.txt +++ b/controls_signalduino.txt @@ -1,4 +1,4 @@ -UPD 2026-01-18_12:38:53 237102 FHEM/00_SIGNALduino.pm +UPD 2026-01-18_23:53:00 238429 FHEM/00_SIGNALduino.pm UPD 2023-01-06_12:08:43 20082 FHEM/10_FS10.pm UPD 2024-01-03_23:05:39 27250 FHEM/10_SD_GT.pm UPD 2023-01-01_18:10:40 25403 FHEM/10_SD_Rojaflex.pm @@ -17,5 +17,6 @@ UPD 2025-12-04_20:25:15 257338 FHEM/lib/SD_ProtocolData.pm UPD 2024-01-06_20:21:35 81862 FHEM/lib/SD_Protocols.pm UPD 2026-01-16_10:54:58 1821 lib/FHEM/Devices/SIGNALduino/SD_Clients.pm UPD 2026-01-16_10:54:58 1364 lib/FHEM/Devices/SIGNALduino/SD_Logger.pm +UPD 2026-01-18_23:53:00 2280 lib/FHEM/Devices/SIGNALduino/SD_MQTT.pm UPD 2026-01-16_10:54:58 4868 lib/FHEM/Devices/SIGNALduino/SD_Matchlist.pm UPD 2026-01-16_10:54:58 6330 lib/FHEM/Devices/SIGNALduino/SD_Message.pm