diff --git a/fhem/FHEM/10_KNX.pm b/fhem/FHEM/10_KNX.pm index 530e9b686..332e68712 100644 --- a/fhem/FHEM/10_KNX.pm +++ b/fhem/FHEM/10_KNX.pm @@ -187,7 +187,14 @@ # correct dpt1.009, dpt1.024, dpt1.100 # add dpt18 "learn" - still experimental # cmd-ref update +# MH 20241114 dpt5, dpt7 change to float (rounding) again... +# (on|off)-for-timer now supports > 86400 seconds and fractions of a sec (up to 9.9 secs) +# new cmd "on-till" - works like set extensions cmd - end time must be gt current time (same day)! +# dpt18 "learn" now implemented for dpt18.001, experimental dpt18.099 removed +# PBP: replace unless w. if not..., fix postfix if +# cmd-ref update # +# postponed new attr readingRegex - allow manipulating reading-names on the fly # todo-4/2024 remove support for oldsyntax cmd's: raw,value,string,rgb @@ -204,11 +211,11 @@ use GPUtils qw(GP_Import); # Package Helper Fn # these ones are NOT used! (constants,Policy::Modules::RequireFilenameMatchesPackage,NamingConventions::Capitalization) # these ones are NOT used! (ControlStructures::ProhibitCascadingIfElse) # these ones are NOT used! (RegularExpressions::RequireDotMatchAnything,RegularExpressions::RequireLineBoundaryMatching) +# these ones are NOT used! (ControlStructures::ProhibitPostfixControls) ### the following percritic items will be ignored global ### ## no critic (NamingConventions::Capitalization) ## no critic (Policy::CodeLayout::ProhibitParensWithBuiltins) ## no critic (ValuesAndExpressions::RequireNumberSeparators,ValuesAndExpressions::ProhibitMagicNumbers) -## no critic (ControlStructures::ProhibitPostfixControls) ## no critic (Documentation::RequirePodSections) ### import FHEM functions / global vars @@ -219,8 +226,7 @@ BEGIN { qw(readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsBeginUpdate readingsEndUpdate Log3 - AttrVal InternalVal ReadingsVal ReadingsNum - addToDevAttrList + AttrVal AttrNum InternalVal ReadingsVal ReadingsNum AssignIoPort IOWrite CommandDefMod CommandModify CommandDelete CommandAttr CommandDeleteAttr CommandDeleteReading defs modules attr cmds @@ -235,6 +241,7 @@ BEGIN { AnalyzePerlCommand AnalyzeCommandChain EvalSpecials fhemTimeLocal) ); +# addToDevAttrList # removed 11/2024 } #string constants @@ -252,12 +259,11 @@ my $SVNID = '$Id$'; ## no critic (Policy::ValuesAndExpressions::RequireInt #regex patterns #pattern for group-adress -my $PAT_GAD = '(?:3[01]|([012])?[0-9])\/(?:[0-7])\/(?:2[0-4][0-9]|25[0-5]|([01])?[0-9]{1,2})'; # 0-31/0-7/0-255 +my $PAT_GAD = '(3[01]|[012][\d]|[\d])\/([0-7])\/(2[0-4][\d]|25[0-5]|(?:[01])?[\d]{1,2})'; # 0-31/0-7/0-255 #pattern for group-adress in hex-format my $PAT_GAD_HEX = '[01][0-9a-f][0-7][0-9a-f]{2}'; # max is 1F7FF -> 31/7/255 5 digits #pattern for group-no my $PAT_GNO = qr/^g[1-9][\d]?$/xms; -#my $PAT_GNO = 'g[1-9][0-9]?'; #pattern for GAD-Options my $PAT_GAD_OPTIONS = 'get|set|listenonly'; #pattern for GAD-suffixes @@ -338,8 +344,8 @@ my %dpttypes = ( 'dpt5.001' => {CODE=>'dpt5', UNIT=>q{%}, PATTERN=>qr/[+]?\d{1,3}/xms, FACTOR=>100/255, MIN=>0, MAX=>100}, 'dpt5.003' => {CODE=>'dpt5', UNIT=>q{°}, PATTERN=>qr/[+]?\d{1,3}/xms, FACTOR=>360/255, MIN=>0, MAX=>360}, 'dpt5.004' => {CODE=>'dpt5', UNIT=>q{%}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, - 'dpt5.005' => {CODE=>'dpt5', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, # Decimal Factor - 'dpt5.006' => {CODE=>'dpt5', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, # Tariff + 'dpt5.005' => {CODE=>'dpt5', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, # Decimal Factor + 'dpt5.006' => {CODE=>'dpt5', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, # Tariff 'dpt5.010' => {CODE=>'dpt5', UNIT=>q{p}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>255}, # counter pulses # 1-Octet signed value @@ -535,15 +541,14 @@ my %dpttypes = ( 'dpt16.001' => {CODE=>'dpt16', UNIT=>q{}, PATTERN=>qr/(?:[[:ascii:]]|[\xC2-\xF4][\x80-\xBF]{1,3}){1,}/ixms, MIN=>undef, MAX=>undef, SETLIST=>'multiple,>CLR<'}, # Scene, 0-63 - 'dpt17' => {CODE=>'dpt17', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>63, + 'dpt17' => {CODE=>'dpt17', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,2}/xms, MIN=>0, MAX=>63, DEC=>\&dec_dpt18,ENC=>\&enc_dpt18,}, - 'dpt17.001' => {CODE=>'dpt17', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, MIN=>0, MAX=>63}, + 'dpt17.001' => {CODE=>'dpt17', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,2}/xms, MIN=>0, MAX=>63}, # Scene, 1-64 - 'dpt18' => {CODE=>'dpt18', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, OFFSET=>1, MIN=>1, MAX=>64, + 'dpt18' => {CODE=>'dpt18', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,2}/xms, OFFSET=>1, MIN=>1, MAX=>64, DEC=>\&dec_dpt18,ENC=>\&enc_dpt18,}, - 'dpt18.001' => {CODE=>'dpt18', UNIT=>q{}, PATTERN=>qr/[+]?\d{1,3}/xms, OFFSET=>1, MIN=>1, MAX=>64}, - 'dpt18.099' => {CODE=>'dpt18', UNIT=>q{}, PATTERN=>qr/(activate|learn)?[,]?\d{1,3}/xms, OFFSET=>1, MIN=>1, MAX=>64, SETLIST=>'widgetList,3,select,activate,learn,1,textField'}, + 'dpt18.001' => {CODE=>'dpt18', UNIT=>q{}, PATTERN=>qr/(activate|learn)?[,]?[+]?\d{1,2}/xms, OFFSET=>1, MIN=>1, MAX=>64, SETLIST=>'widgetList,3,select,activate,learn,1,textField'}, #date and time 'dpt19' => {CODE=>'dpt19', UNIT=>q{}, PATTERN=>qr/($PAT_DATE$PAT_DTSEP$PAT_TIME|now)/ixms, MIN=>undef, MAX=>undef, @@ -607,14 +612,15 @@ sub Initialize { $hash->{DbLog_splitFn} = \&KNX_DbLog_split; $hash->{AttrList} = 'IODev ' . #define IO-Device to communicate with. Deprecated at definition line. - 'disable:1 ' . #device disabled - 'showtime:0,1 ' . #show event-time instead of value in device overview - 'stateRegex:textField-long ' . #modifies state value - 'stateCmd:textField-long ' . #modify state value - 'putCmd:textField-long ' . #enable FHEM to answer KNX read telegrams - 'format ' . #supplies post-string - 'KNX_toggle:textField ' . #toggle source : - "$readingFnAttributes "; #standard attributes + 'disable:1 ' . #device disabled + 'showtime:0,1 ' . #show event-time instead of value in device overview + 'stateRegex:textField-long ' . #modifies state value + 'stateCmd:textField-long ' . #modify state value +#not yet 'readingRegex:textField-long ' . #modify reading name + 'putCmd:textField-long ' . #enable FHEM to answer KNX read telegrams + 'format ' . #supplies post-string + 'KNX_toggle:textField ' . #toggle source : + "$readingFnAttributes "; #standard attributes $hash->{noAutocreatedFilelog} = 1; # autocreate devices create no FileLog $hash->{AutoCreate} = {'KNX_.*' => { ATTR => 'disable:1'} }; #autocreate devices are disabled by default @@ -637,10 +643,10 @@ sub KNX_Define { KNX_Log ($name, 5, join (q{ }, @a)); #too less arguments or no valid 1st gad - if (int(@a) < 3 || $a[2] !~ m/^(?:$PAT_GAD|$PAT_GAD_HEX)/ixms) { + if (int(@a) < 3 || $a[2] !~ m/^(?:$PAT_GAD|$PAT_GAD_HEX)[:](?:dpt|$MODELERR)/ixms) { # at least the first gad must be valid return (qq{KNX_define: wrong syntax or wrong group-format (0-31/0-7/0-255)\n} . - qq{ "define $name KNX } . - q{[]"}); + qq{ "define $name KNX [:set|get|listenonly][:nosuffix] } . + q{[[:set|get|listenonly][:nosuffix]]"}); } $hash->{'.DEFLINE'} = join(q{ },@a[2 .. $#a]); # temp store defs for define2... @@ -665,12 +671,10 @@ sub KNX_Define2 { AssignIoPort($hash); # AssignIoPort will take device from $attr{$name}{IODev} if defined - #reset - $hash->{GADDETAILS} = {}; - $hash->{GADTABLE} = {}; - - #delete all defptr entries for this device (defmod & copy problem) + #reset: delete all defptr entries for this device (defmod & copy problem) KNX_delete_defptr($hash); # verify with: {PrintHash($modules{KNX}->{defptr},3) } + $hash->{GADDETAILS} = {}; + $hash->{GADTABLE} = {}; #create groups and models, iterate through all possible args my $gadNo = 1; # index to GAD's @@ -678,8 +682,7 @@ sub KNX_Define2 { my $setString = q{}; # placeholder for setstring foreach my $gaddef (@a) { - my $gadCode = undef; - my $gadOption = undef; + my $gadOption = undef; my $gadNoSuffix = undef; my $gadName = 'g' . $gadNo; # old syntax @@ -687,26 +690,24 @@ sub KNX_Define2 { KNX_Log ($name, 5, qq{gadNr= $gadNo def-string= $gaddef}); my ($gad, $gadModel, @gadArgs) = split(/:/xms, $gaddef); - if (! defined($gad) || $gad !~ m/^(?:$PAT_GAD|$PAT_GAD_HEX)$/ixms) { + my $gadCode = KNX_name2gadCode($gad); + if (! defined($gadCode)) { push(@logarr,qq{GAD not defined or wrong format for group-number $gadNo, specify as 0-31/0-7/0-255}); next; } - $gad = KNX_hex2Name($gad) if ($gad =~ m/^$PAT_GAD_HEX$/ixms); - $gadCode = KNX_name2Hex ($gad); #convert it vice-versa, just to be sure - - if(! defined($gadModel)) { + if (! defined($gadModel)) { push(@logarr,qq{no model defined for group-number $gadNo}); next; } if ($gadModel eq $MODELERR) { #within autocreate no model is supplied - throw warning KNX_Log ($name, 3, q{autocreate device will be disabled, correct def with valid dpt and enable device}); - $attr{$name}->{disable} = 1 if (AttrVal($name,'disable',0) != 1); + if (AttrNum($name,'disable',0) != 1) {$attr{$name}->{disable} = 1;} } - elsif (!defined($dpttypes{$gadModel})) { #check model-type + elsif (! defined($dpttypes{$gadModel})) { #check model-type push(@logarr,qq{invalid dpt $gadModel for group-number $gadNo, consult commandref - avaliable DPT}); - $attr{$name}->{disable} = 1 if (AttrVal($name,'disable',0) != 1); + if (AttrNum($name,'disable',0) != 1) {$attr{$name}->{disable} = 1;} next; } elsif ($gadNo == 1) { # gadModel ok - use first gad as mdl reference for fheminfo @@ -714,9 +715,9 @@ sub KNX_Define2 { } if (scalar(@gadArgs)) { - $gadNoSuffix = lc(pop(@gadArgs)) if ($gadArgs[-1] =~ /$PAT_GAD_SUFFIX/ixms); - $gadOption = lc(pop(@gadArgs)) if (@gadArgs && $gadArgs[-1] =~ /^($PAT_GAD_OPTIONS)$/ixms); - $gadName = shift(@gadArgs) // q{g} . $gadNo; # use first verb + if ($gadArgs[-1] =~ /$PAT_GAD_SUFFIX/ixms) {$gadNoSuffix = lc(pop(@gadArgs));} + if (@gadArgs && $gadArgs[-1] =~ /^($PAT_GAD_OPTIONS)$/ixms) {$gadOption = lc(pop(@gadArgs));} + $gadName = shift(@gadArgs) // q{g} . $gadNo; # use first verb } if (scalar(@gadArgs)) { @@ -737,7 +738,7 @@ sub KNX_Define2 { #cache suffixes my ($suffixGet, $suffixSet) = qw(-get -set); - ($suffixGet, $suffixSet) = (q{},q{}) if (defined($gadNoSuffix)); + if (defined($gadNoSuffix)) {($suffixGet, $suffixSet) = (q{},q{});} # new syntax readingNames my $rdNameGet = $gadName . $suffixGet; @@ -747,8 +748,10 @@ sub KNX_Define2 { $rdNameSet = 'setG' . $gadNo; } - KNX_Log ($name, 5, qq{found GAD: $gad NAME: $gadName NO: $gadNo HEX: $gadCode DPT: $gadModel} . - qq{ OPTION: $gadOption}) if (defined ($gadOption)); + if (defined ($gadOption)) { + KNX_Log ($name, 5, qq{found GAD: $gad NAME: $gadName NO: $gadNo HEX: $gadCode DPT: $gadModel} . + qq{ OPTION: $gadOption}); + } #determine dpt-details my $dptDetails = $dpttypes{$gadModel}; @@ -776,7 +779,7 @@ sub KNX_Define2 { #create setlist for setFn / getlist will be created during get-cmd! if ((! defined($gadOption)) || $gadOption eq 'set') { #no set-command for get or listenonly - $setString .= 'on:noArg off:noArg ' if (($gadNo == 1) && ($gadModel =~ /^(dpt1|dpt1.001)$/xms)); + if (($gadNo == 1) && ($gadModel =~ /^(dpt1|dpt1.001)$/xms)) {$setString .= 'on:noArg off:noArg ';} if (! defined($dptDetails->{SETLIST}) || $dptDetails->{SETLIST} ne 'noset') { $setString .= $gadName . $setlist . q{ }; } @@ -786,10 +789,10 @@ sub KNX_Define2 { } # /foreach KNX_Log ($name, 5, qq{setString= $setString}); - $hash->{'.SETSTRING'} = $setString =~ s/[\s]$//ixrms; # save setstring + $hash->{'.SETSTRING'} = $setString =~ s/\s+$//rxms; # save setstring if (scalar(@logarr)) { my $logtxt = join('',@logarr); - FW_directNotify("FILTER=$name",'#FHEMWEB:' . $FW_wname, qq{FW_okDialog('$logtxt')}, q{}) if (defined($FW_wname)); + if (defined($FW_wname)) {FW_directNotify("FILTER=$name",'#FHEMWEB:' . $FW_wname, qq{FW_okDialog('$logtxt')}, q{});} foreach (@logarr) { KNX_Log ($name, 2, $_); } @@ -830,10 +833,10 @@ sub KNX_Get { last if (! defined($key)); my $option = $hash->{GADDETAILS}->{$key}->{OPTION}; next if (defined($option) && $option =~ /(?:set|listenonly)/xms); - $getter .= q{ } . $key . ':noArg'; + $getter .= $key . ':noArg '; } - $getter =~ s/^\s+//gixms; #trim leading blank - $getter = q{} if (IsDisabled($name) == 1); + $getter =~ s/\s+$//xms; #trim trailing blank + if (IsDisabled($name) == 1) {$getter = q{};} return qq{unknown argument $gadName choose one of $getter}; } @@ -876,7 +879,7 @@ sub KNX_Set { #FHEM asks with a "?" at startup or any reload of the device-detail-view - if dev is disabled: no SET/GET pulldown ! if ($targetGadName eq q{?}) { my $setter = exists($hash->{'.SETSTRING'})?$hash->{'.SETSTRING'}:q{}; - $setter = q{} if (IsDisabled($name) == 1); + if (IsDisabled($name) == 1) {$setter = q{};} return qq{unknown argument $targetGadName choose one of $setter}; } @@ -919,7 +922,7 @@ sub KNX_Set { #Text neads special treatment - additional args may be blanked words - truncate to 14 char elsif ($model =~ m/^dpt16(?:[.][\d]{3})?$/xms) { - $value .= q{ } . join (q{ }, @arg) if (scalar (@arg) > 0); + if (scalar(@arg) > 0) {$value .= q{ } . join (q{ }, @arg);} } my $transval = KNX_encodeByDpt($hash, $value, $targetGadName); #process set command @@ -930,7 +933,7 @@ sub KNX_Set { KNX_Log ($name, 5, qq{cmd= $cmd , value= $value , translated= $transval}); # decode again for values that have been changed in encode process - $value = KNX_decodeByDpt($hash, $transval, $targetGadName) if ($model =~ m/^dpt(?:3|10|11|16|18|19)(?:[.][\d]{3})?$/xms); + if ($model =~ m/^dpt(?:3|10|11|16|18|19)(?:[.][\d]{3})?$/xms) {$value = KNX_decodeByDpt($hash, $transval, $targetGadName);} #apply post processing for state and set all readings KNX_SetReadings($hash, $targetGadName, $value, undef, undef); @@ -970,39 +973,35 @@ sub KNX_Set_oldsyntax { # all of the following cmd's need at least 1 Argument (or more) return (undef, $targetGadName, $cmd) if ($na <= 0); # pass thru -for-timer,-until,blink cmds... - return (undef, $targetGadName, $cmd, @arg) if ($cmd =~ m/(?:-until|-till-overnight|-for-timer|$BLINK)$/ixms); + return (undef, $targetGadName, $cmd, @arg) if ($cmd =~ m/(?:-till|-until|-till-overnight|-for-timer|$BLINK)$/ixms); my $code = $hash->{GADDETAILS}->{$targetGadName}->{MODEL}; - my $value = $arg[0]; + my $value = shift(@arg); + my $rtxt = qq{"set $name $cmd $value }; # log text - if ($code =~ /^dpt16(?:[.]\d+)?$/xms) { #special case txt - $cmd = shift(@arg) if ($cmd =~ m/$STRING/xms); # cmd contains first word - return (undef, $targetGadName, $cmd, @arg); + if ($cmd =~ m/$STRING/ixms) { ## no critic (ControlStructures::ProhibitCascadingIfElse) + return $rtxt . q{ ..." only allowed for dpt16} if ($code !~ m/^dpt16/ixms); } - - my $rtxt = qq{set $name $cmd }; - - if ($cmd =~ m/$RAW/ixms) { + elsif ($cmd =~ m/$RAW/ixms) { #check for 1-16 hex-digits - return $rtxt . $value . q{ has wrong syntax. Use hex-format only.} if ($value !~ m/[\da-f]{1,16}/ixms); + return $rtxt . q{" has wrong syntax. Use hex-format only.} if ($value !~ m/[\da-f]{1,16}/ixms); } elsif ($cmd =~ m/$VALUE/ixms) { - return $rtxt . q{not allowed for dpt1, dpt16 and dpt232} if ($code =~ m/(dpt1|dpt16|dpt232)(?:[.]\d+)?$/ixms); + return $rtxt . q{" not allowed for dpt1, dpt16 and dpt232} if ($code =~ m/(dpt1|dpt16|dpt232)(?:[.]\d+)?$/ixms); $value =~ s/,/\./gxms; } - #set RGB elsif ($cmd =~ m/$RGB/ixms) { - return $rtxt . q{only allowed for dpt232} if ($code !~ m/dpt232/ixms); + return $rtxt . q{" only allowed for dpt232} if ($code !~ m/dpt232/ixms); #check for 6 hex-digits - return $rtxt . $value . q{ has wrong syntax. Use 6 hex-digits only.} if ($value !~ m/[\da-f]{6}/ixms); + return $rtxt . q{" has wrong syntax. Use 6 hex-digits only.} if ($value !~ m/[\da-f]{6}/ixms); } else { - KNX_Log ($name, 2, q{invalid cmd: "} . $rtxt . q{" issued - ignored}); - return q{invalid cmd: "} . $rtxt . join(q{ },@arg). q{" issued - ignored}; + KNX_Log ($name, 2, q{invalid cmd: } . $rtxt . join(q{ },@arg) . q{" issued - ignored}); + return q{invalid cmd: } . $rtxt . join(q{ },@arg) . q{" issued - ignored}; } - KNX_Log ($name, 3, q{This cmd will be deprecated by 1/2024: "} . $rtxt . join(q{ },@arg) . - qq{" - use: "set $name $targetGadName $value"}); + KNX_Log ($name, 3, q{This cmd will be deprecated by 1/2024: } . $rtxt . join(q{ },@arg) . + qq{" - use: "set $name $targetGadName $value } . join(q{ },@arg) . q{"} ); return (undef, $targetGadName, $value, @arg); } @@ -1017,7 +1016,7 @@ sub KNX_Set_dpt1 { my $groupCode = $hash->{GADDETAILS}->{$targetGadName}->{CODE}; #delete any running timers - if ($hash->{".TIMER_$groupCode"}) { + if ($hash->{".TIMER_$groupCode"} || IsDevice($name . "_TIMER_$groupCode")) { CommandDelete(undef, $name . "_TIMER_$groupCode"); delete $hash->{".TIMER_$groupCode"}; } @@ -1029,31 +1028,43 @@ sub KNX_Set_dpt1 { return (undef,$value) if ($cmd =~ m/(?:$PAT_DPT1_PAT)$/ixms); # shortcut #set on-for-timer / off-for-timer - my $hms_til = undef; + my $endTS = undef; if ($cmd =~ m/(?:(on|off)-for-timer)$/ixms) { #get duration - return qq{KNX_Set_dpt1 ($name): parameter must be numeric} unless Scalar::Util::looks_like_number($arg[0]); - return qq{KNX_Set_dpt1 ($name): subsecond parameter not supported} if ($arg[0] < 1); - - my $myTS = Time::HiRes::time() + $arg[0]; - $hms_til = (split(q{ },::FmtDateTime($myTS)))[1]; # fhem.pl + return qq{KNX_Set_dpt1 ($name): parameter must be numeric} if (! Scalar::Util::looks_like_number($arg[0])); + # subsecond timer duration - set called with only $hash as param + if ($arg[0] < 10) { # allow fraction secs when dur < 10sec + my $param = {hash=>$hash, gadName=>$targetGadName, cmd=>$tvalue}; + InternalTimer(Time::HiRes::time() + $arg[0],\&doKNX_Set,$param); + return (undef,$value); + } else { + $endTS = Time::HiRes::time() + $arg[0]; # allow more than one day ( > 86400) !!! + } } - #set on-until / off-until - elsif ($cmd =~ m/(?:(on|off)-(until|till-overnight))$/ixms) { - #get off-time + #set (on|off)-until / -till + elsif ($cmd =~ m/(?:(on|off)-(until|till-overnight|till))$/ixms) { + #get end-time my ($err, $hr, $min, $sec, $fn) = GetTimeSpec($arg[0]); # fhem.pl - return qq{KNX_Set_dpt1 ($name): Error trying to parse timespec for $arg[0] : $err} if (defined($err)); + return qq{KNX_Set_dpt1 ($name): $err} if (defined($err)); + return qq{KNX_Set_dpt1 ($name): Error trying to parse timespec $arg[0] } if ($hr >= 24); - #do like (on|off)-till-overnight in at cmd ! - $hms_til = sprintf('%02d:%02d:%02d', $hr, $min, $sec); + my ($secs,$mins,$hours,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time()); # default now + $endTS = fhemTimeLocal($sec, $min, $hr, $mday, $mon, $year); # today end-time spec + + if ($cmd =~ /-till$/xms) { # prevent overnight times + return qq{KNX_Set_dpt1 ($name): will not execute as end-Time is earlier than now} if (time() >= $endTS); + } } - if (defined($hms_til)) { # process both timer variants + # process both timer variants + if (defined($endTS)) { + my $hms_til = ::FmtDateTime($endTS) =~ s/[\s]/T/rxms; # fhem.pl alternativ use full datespec $hash->{".TIMER_$groupCode"} = $hms_til; #create local marker #place at-command for switching on / off - CommandDefMod(undef, '-temporary ' . $name . qq{_TIMER_$groupCode at $hms_til set $name $targetGadName $tvalue}); +# CommandDefMod(undef, '-temporary ' . $name . qq{_TIMER_$groupCode at $hms_til set $name $targetGadName $tvalue}); + CommandDefMod(undef, '-silent ' . $name . qq{_TIMER_$groupCode at $hms_til set $name $targetGadName $tvalue}); return (undef,$value); } -### + return KNX_set_dpt1_sp($hash, $targetGadName, $cmd, @arg); } @@ -1070,7 +1081,7 @@ sub KNX_set_dpt1_sp { my ($tDev, $togglereading) = split(qr/:/xms,AttrVal($name,'KNX_toggle',$name)); if (defined($togglereading)) { # prio1: use Attr. KNX_toggle: format: : - $tDev = $name if ($tDev eq '$self'); ## no critic (Policy::ValuesAndExpressions::RequireInterpolationOfMetachars) + if ($tDev eq '$self') {$tDev = $name;} ## no critic (Policy::ValuesAndExpressions::RequireInterpolationOfMetachars) $toggleOldVal = ReadingsVal($tDev, $togglereading, undef); # switch off in case of non existent reading } else { @@ -1085,16 +1096,18 @@ sub KNX_set_dpt1_sp { #toggle if ($cmd =~ m/$TOGGLE/ixms) { - KNX_Log ($name, 3, qq{current value for "set $name $targetGadName TOGGLE" is not "on" or "off" - } . - qq{$targetGadName will be switched off}) if ($toggleOldVal !~ /^(?:on|off)/ixms); + if ($toggleOldVal !~ /^(?:on|off)/ixms) { + KNX_Log ($name, 3, qq{current value for "set $name $targetGadName TOGGLE" is not "on" or "off" - } . + qq{$targetGadName will be switched off}); + } } #blink - implemented with timer & toggle elsif ($cmd =~ m/$BLINK/ixms) { my $count = ($arg[0])?$arg[0] * 2 -1:1; - $count = 1 if ($count < 1); + if ($count < 1) {$count = 1;} my $dur = ($arg[1])?$arg[1]:1; - $dur = 1 if ($dur < 1); + if ($dur < 1) {$dur = 1;} my $duration = sprintf('%02d:%02d:%02d', $dur/3600, ($dur%3600)/60, $dur%60); CommandDefMod(undef, '-temporary ' . $name . "_TIMERBLINK_$groupCode at +*{" . $count ."}$duration set $name $targetGadName toggle"); @@ -1127,9 +1140,9 @@ sub KNX_Attr { if ($aName eq 'KNX_toggle') { # validate device/reading my ($srcDev,$srcReading) = split(qr/:/xms,$aVal); # format: : - $srcDev = $name if ($srcDev eq '$self'); ## no critic (Policy::ValuesAndExpressions::RequireInterpolationOfMetachars) + if ($srcDev eq '$self') {$srcDev = $name;} ## no critic (Policy::ValuesAndExpressions::RequireInterpolationOfMetachars) return 'no valid device for attr: KNX_toggle' if (!IsDevice($srcDev)); - $value = ReadingsVal($srcDev,$srcReading,undef) if (defined($srcReading)); #test for value + if (defined($srcReading)) {$value = ReadingsVal($srcDev,$srcReading,undef);} #test for value return 'no valid device/reading value for attr: KNX_toggle' if (!defined($value) ); } @@ -1139,11 +1152,13 @@ sub KNX_Attr { return qq{syntax check failed for $aName: \n $err} if($err); } - elsif ($aName eq 'stateRegex') { # test for syntax errors + elsif ($aName =~ /(stateRegex|readingRegex)/xms) { # test for syntax errors +# elsif ($aName eq 'stateRegex') { # test for syntax errors my @aValS = split(/\s+|\//xms,$aVal); foreach (@aValS) { next if ($_ eq q{} ); - my $err = main::CheckRegexp($_,'Attr stateRegex'); # fhem.pl + my $err = main::CheckRegexp($_,'Attr ' . $aName); # fhem.pl +# my $err = main::CheckRegexp($_,'Attr stateRegex'); # fhem.pl return qq{syntax check failed for $aName: \n $err} if (defined($err)); } } @@ -1182,11 +1197,12 @@ sub KNX_DbLog_split { $reading = shift(@strings); $reading =~ s/[:]$//xms; } - $strings[0] = q{} if (! defined($strings[0])); + if (! defined($strings[0])) {$strings[0] = q{};} #numeric value? and last value non numeric? - assume unit - except for dpt16 my $devhash = $defs{$device}; foreach my $key (keys %{$devhash->{GADDETAILS}}) { + next if ($key ne $reading); next if ($devhash->{GADDETAILS}->{$key}->{MODEL} !~ /^dpt16/xms); $dpt16flag = 1; last; @@ -1196,7 +1212,7 @@ sub KNX_DbLog_split { } my $value = join(q{ },@strings); - $unit = q{} if (!defined($unit)); + if (!defined($unit)) {$unit = q{};} KNX_Log ($device, 5, qq{EVENT= $event READING= $reading VALUE= $value UNIT= $unit}); return ($reading, $value, $unit); @@ -1236,7 +1252,8 @@ sub KNX_Parse { my $deviceName = $deviceHash->{NAME}; my $gadName = $deviceHash->{GADTABLE}->{$gadCode}; - push(@foundMsgs, $deviceName); # save to list even if dev is disabled + if (IsDevice($deviceName)) { push(@foundMsgs, $deviceName);} # save to list even if dev is disabled - dev must exist! +# push(@foundMsgs, $deviceName); # save to list even if dev is disabled if (IsDisabled($deviceName) == 1) { $deviceHash->{RAWMSG} = qq{gadName=$gadName cmd=$cmd, hexvalue=$val}; # for debugging @@ -1357,8 +1374,7 @@ sub KNX_autoCreate { # check if any autocreate device has ignoretype "KNX..." set my @acList = devspec2array('TYPE=autocreate'); foreach my $acdev (@acList) { - next unless $acdev; - next if (! $defs{$acdev}); + next if (! defined($defs{$acdev})); my $igntypes = AttrVal($acdev,'ignoreTypes',q{}); return q{} if($newDevName =~ /$igntypes/xms); } @@ -1393,6 +1409,9 @@ sub KNX_SetReadings { $rdName = $hash->{GADDETAILS}->{$gadName}->{RDNAMEGET}; } +### test remapping of readings +# $rdName = KNX_readingRegex($hash,$rdName); # modify reading name + #execute stateRegex my $state = KNX_replaceByRegex ($hash, $rdName, $value); @@ -1403,7 +1422,6 @@ sub KNX_SetReadings { if (defined($state)) { #execute state-command if defined #must be placed after first reading, because it may have a reference - my $deviceName = $name; #hack for being backward compatible - serve $name and $devname my $cmdAttr = AttrVal($name, 'stateCmd', undef); if ((defined($cmdAttr)) && ($cmdAttr ne q{})) { @@ -1422,6 +1440,18 @@ sub KNX_SetReadings { return; } +### prepare args for KNX_set +# called from InternalTimer - on/off-for-timer +sub doKNX_Set { + my @param = @_; + my ($hash, $gadName, $cmd) = ($param[0]->{hash}, $param[0]->{gadName}, $param[0]->{cmd}); + + my $name = $hash->{NAME}; + my @arg = (); + $arg[0] = $cmd; + return KNX_Set($hash, $name, $gadName, @arg); +} + ### check for valid IODev # called from define & Attr # returns undef on success , error msg on failure @@ -1435,7 +1465,7 @@ sub KNX_chkIODev { my @IOList = devspec2array('TYPE=(TUL|KNXTUL|KNXIO|FHEM2FHEM)'); my @IOList2 = (); # holds all non disabled io-devs foreach my $iodev (@IOList) { - next unless $iodev; + next if (! defined($defs{$iodev})); next if ((IsDisabled($iodev) == 1) || IsDummy($iodev)); # IO - device is disabled or dummy my $iohash = $defs{$iodev}; next if ($iohash->{TYPE} eq 'KNXIO' && exists($iohash->{model}) && $iohash->{model} eq 'X'); # exclude dummy dev @@ -1464,18 +1494,19 @@ sub KNX_delete_defptr { my $name = $hash->{NAME}; for my $gad (sort keys %{$modules{KNX}->{defptr}}) { # get all gad for all KNX devices - my @olddefList = (); - @olddefList = @{$modules{KNX}->{defptr}->{$gad}} if (defined ($modules{KNX}->{defptr}->{$gad})); # get list of devices with this gad - my @newdefList = (); - foreach my $devHash (@olddefList) { - push (@newdefList, $devHash) if ($devHash != $hash); # remove previous definition for this device, but keep them for other devices! + next if (! defined ($modules{KNX}->{defptr}->{$gad})); + my @defList = @{$modules{KNX}->{defptr}->{$gad}}; # get list of device-hashes with this gad + my $i = 0; + foreach (@defList) { + if ($defList[$i] == $hash) {splice(@defList, $i, 1); last;} + $i++; } - #restore list if we have at least one entry left, or delete key! - if (scalar(@newdefList) == 0) { - delete $modules{KNX}->{defptr}->{$gad}; + # restore list if we have at least one entry left, or delete key + if (scalar(@defList) > 0) { + @{$modules{KNX}->{defptr}->{$gad}} = @defList; } else { - @{$modules{KNX}->{defptr}->{$gad}} = @newdefList; + delete $modules{KNX}->{defptr}->{$gad}; } } return; @@ -1496,14 +1527,16 @@ sub KNX_hex2Name { } ### convert GAD from readable version to 5 digit hex -sub KNX_name2Hex { - my $v = shift; - my $r = $v; +# calling parameter: gadvalue from def-stmt +# return: 5 hex-digit gadcode, undef on invalid call param. +sub KNX_name2gadCode { + my $gad = shift // return; - if($v =~ /^(\d{1,2})\/(\d{1,2})\/(\d{1,3})$/xms) { - $r = sprintf('%02x%01x%02x',$1,$2,$3); + if ($gad =~ m/^$PAT_GAD_HEX$/ixms) {$gad = KNX_hex2Name($gad);} # called with hex-option + if ($gad =~ m/^$PAT_GAD$/xms) { + return sprintf('%02x%01x%02x',$1,$2,$3); } - return $r; + return; } ### replace state-values by Attr stateRegex @@ -1550,10 +1583,45 @@ sub KNX_replaceByRegex { } last; } - KNX_Log ($name, 5, qq{replaced $rdName value from: $input to $retVal}) if ($input ne $retVal); + if ($input ne $retVal) {KNX_Log ($name, 5, qq{replaced $rdName value from: $input to $retVal});} return ($retVal eq 'undefined')?undef:$retVal; } +### replace reading Name by regex +### called from KNX_SetReadings +### input: hash, readingname +### return: new readingname +sub KNX_readingRegex { + my $hash = shift; + my $rName = shift; + + my $name = $hash->{NAME}; + my $regAttr = AttrVal($name, 'readingRegex', undef); + return $rName if (! defined($regAttr)); + + #get array of given attributes + my @reg = split(/\s\//xms, $regAttr); + my $newrName = $rName; # default if no match + + #loop over all regex + foreach my $regex (@reg) { + #trim leading and trailing slashes + $regex =~ s/^\/|\/$//gixms; + # get pairs + my @regPair = split(/\//xms, $regex); +#KNX_Log ($name, 1, qq{r0=$regPair[0] r1=$regPair[1] rName=$rName}); + next if ((! defined($regPair[0])) || ($rName !~ /$regPair[0]/xms) ); # no match + if ($regPair[0] =~ /[(].*[)]/xms) { # we have a capture group + my $g1 = $rName =~ s/$regPair[0]/$1/rxms; # extract 1st capture group + $regPair[1] =~ s/\$1/$g1/xms; + } + $newrName =~ s/$regPair[0]/$regPair[1]/xms; + + KNX_Log ($name, 4, qq{replaced $rName with $newrName}); + } + return $newrName; +} + ### limit numeric values. Valid directions: encode, decode # called from: _encodeByDpt, _decodeByDpt # returns: limited value @@ -1570,19 +1638,21 @@ sub KNX_limit { #get correction details my $factor = 1; # defaults my $offset = 0; - $factor = $dpttypes{$model}->{FACTOR} if exists($dpttypes{$model}->{FACTOR}); - $offset = $dpttypes{$model}->{OFFSET} if exists($dpttypes{$model}->{OFFSET}); + if (exists($dpttypes{$model}->{FACTOR})) {$factor = $dpttypes{$model}->{FACTOR};} + if (exists($dpttypes{$model}->{OFFSET})) {$offset = $dpttypes{$model}->{OFFSET};} #get limits my $min = $dpttypes{$model}->{MIN}; my $max = $dpttypes{$model}->{MAX}; - return $value if (! Scalar::Util::looks_like_number ($min)); # allow 0/1 for dpt1.002+ + return $value if (! Scalar::Util::looks_like_number($min)); # allow 0/1 for dpt1.002+ #determine direction of scaling, do only if defined if ($direction =~ m/^encode/ixms) { #limitValue - $retVal = $min if (defined ($min) && ($value < $min)); - $retVal = $max if (defined ($max) && ($value > $max)); - KNX_Log ($name, 3, qq{cmd value modified by limits... Input= $value Output= $retVal Model= $model}) if ($value != $retVal);; + if (defined ($min) && ($value < $min)) {$retVal = $min;} + if (defined ($max) && ($value > $max)) {$retVal = $max;} + if ($value != $retVal) { + KNX_Log ($name, 3, qq{cmd value modified by limits... Input= $value Output= $retVal Model= $model}); + } #correct value $retVal /= $factor; $retVal -= $offset; @@ -1592,8 +1662,8 @@ sub KNX_limit { $retVal += $offset; $retVal *= $factor; #limitValue - $retVal = $min if (defined ($min) && ($retVal < $min)); - $retVal = $max if (defined ($max) && ($retVal > $max)); + if (defined ($min) && ($retVal < $min)) {$retVal = $min;} + if (defined ($max) && ($retVal > $max)) {$retVal = $max;} } KNX_Log ($name, 5, qq{DIR= $direction INPUT= $value OUTPUT= $retVal}); @@ -1639,7 +1709,7 @@ sub KNX_encodeByDpt { $value = (split(/\s+/xms,$value))[0]; # strip off unit } # compatibility with widgetoverride :time - $value .= ':00' if ($code eq 'dpt10' && $value =~ /^[\d]{2}:[\d]{2}$/xms); + if ($code eq 'dpt10' && $value =~ /^[\d]{2}:[\d]{2}$/xms) {$value .= ':00';} # support dpt18 learn my $arg1 = undef; @@ -1661,7 +1731,7 @@ sub KNX_encodeByDpt { if (ref($dpttypes{$code}->{ENC}) eq 'CODE') { my $hexval = $dpttypes{$code}->{ENC}->($lvalue, $model); - $hexval = sprintf('00%.2x',hex($hexval) + 0x80) if ($code eq 'dpt18' && $arg1 =~ /^learn/xms); + if ($code eq 'dpt18' && $arg1 =~ /^learn/xms) {$hexval = sprintf('00%.2x',hex($hexval) + 0x80);} KNX_Log ($name, 5, qq{gadName= $gadName model= $model } . qq{in-Value= $value out-Value= $lvalue out-ValueHex= $hexval}); return $hexval; @@ -1704,8 +1774,8 @@ sub enc_dpt1 { #Binary value my $value = shift; my $model = shift; my $numval = 0; #default - $numval = 1 if ($value =~ m/(1|on)$/ixms); - $numval = 1 if ($value eq $dpttypes{$model}->{MAX}); # dpt1.011 problem + if ($value =~ m/(1|on)$/ixms) {$numval = 1;} + if ($value eq $dpttypes{$model}->{MAX}) {$numval = 1;} # dpt1.011 problem return sprintf('%.2x',$numval); } @@ -1713,7 +1783,7 @@ sub enc_dpt2 { #Step value (two-bit) my $value = shift; my $dpt2list = {off => 0, on => 1, forceoff => 2, forceon =>3}; my $numval = $dpt2list->{lc($value)}; - $numval = $value if ($value =~ m/^0?[0-3]$/ixms); # JoeALLb request + if ($value =~ m/^0?[0-3]$/xms) {$numval = $value;} # JoeALLb request return sprintf('%.2x',$numval); } @@ -1727,7 +1797,7 @@ sub enc_dpt3 { #Step value (four-bit) $numval++; last if ($value >= $key); } - $numval = ($numval | 0x08) if ($sign == 1); # positive + if ($sign == 1) {$numval = ($numval | 0x08);} # positive return sprintf('%.2x',$numval); } @@ -1735,7 +1805,7 @@ sub enc_dpt4 { #single ascii or iso-8859-1 char my $value = shift; my $model = shift; my $numval = Encode::encode('iso-8859-1', Encode::decode('utf8', $value)); #always convert to latin-1 - $numval =~ s/[\x80-\xff]/?/gxms if ($model ne 'dpt4.002'); #replace values >= 0x80 if ascii + if ($model ne 'dpt4.002') {$numval =~ s/[\x80-\xff]/?/gxms;} #replace values >= 0x80 if ascii #convert to hex-string my $dat = unpack('H*', $numval); return sprintf('00%s',$dat); @@ -1787,7 +1857,7 @@ sub enc_dpt10 { #Time of Day $year += 1900; $mon++; # calculate offset for weekday - $wday = 7 if ($wday == 0); + if ($wday == 0) {$wday = 7;} $hours += 32 * $wday; $numval = $secs + ($mins << 8) + ($hours << 16); @@ -1804,13 +1874,13 @@ sub enc_dpt11 { #Date year range is 1990-2089 {0 => 2000 , 89 => 2089, 90 => 199 $year+=1900; $mon++; # calculate offset for weekday - $wday = 7 if ($wday == 0); + if ($wday == 0) {$wday = 7;} $numval = ($year - 2000) + ($mon << 8) + ($mday << 16); } else { my ($dd, $mm, $yyyy) = split (/[.]/xms, $value); $yyyy -= 2000; - $yyyy += 100 if ($yyyy < 0); + if ($yyyy < 0) {$yyyy += 100;} $numval = ($yyyy) + ($mm << 8) + ($dd << 16); } return sprintf('00%.6x',$numval); @@ -1835,12 +1905,12 @@ sub enc_dpt16 { #14-Octet String my $value = shift; my $model = shift; my $numval = Encode::encode('iso-8859-1', Encode::decode('utf8', $value)); #always convert to latin-1 - $numval =~ s/[\x80-\xff]/?/gxms if ($model ne 'dpt16.001'); #replace values >= 0x80 if ascii + if ($model ne 'dpt16.001') {$numval =~ s/[\x80-\xff]/?/gxms;} #replace values >= 0x80 if ascii $numval = substr($numval,0,14); # limit to 14 char #convert to hex-string my $dat = unpack('H*', $numval); - $dat = '00' if ($value =~ /^$PAT_DPT16_CLR/ixms); # send all zero string if "clear line string" + if ($value =~ /^$PAT_DPT16_CLR/ixms) {$dat = '00';} # send all zero string if "clear line string" #format for 14-byte-length and replace trailing blanks with zeros my $hexval = sprintf('00%-28s',$dat); $hexval =~ s/\s/0/gxms; @@ -1861,11 +1931,11 @@ sub enc_dpt19 { #DateTime $ts = fhemTimeLocal($6, $5, $4, $1, $2-1, $3 - 1900); } my ($secs,$mins,$hours,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($ts); - $wday = 7 if ($wday == 0); # calculate offset for weekday + if ($wday == 0) {$wday = 7;} # calculate offset for weekday $hours += ($wday << 5); # add day of week my $status1 = 0x40; # Fault=0, WD = 1, NWD = 0 (WD Field valid), NY = 0, ND = 0, NDOW= 0,NT=0, SUTI = 0 - $status1 = $status1 & 0xBF if ($wday >= 6); # Saturday & Sunday is non working day - $status1 += 1 if ($isdst == 1); + if ($wday >= 6) {$status1 = $status1 & 0xBF;} # Saturday & Sunday is non working day + if ($isdst == 1) {$status1 += 1;} my $status0 = 0x00; # CLQ=0 $mon++; return sprintf('00%02x%02x%02x%02x%02x%02x%02x%02x',$year,$mon,$mday,$hours,$mins,$secs,$status1,$status0); @@ -1875,7 +1945,7 @@ sub enc_dpt20 { # HVAC 1Byte my $value = shift; my $dpt20list = {auto => 0, comfort => 1, standby => 2, economy => 3, protection => 4,}; my $numval = $dpt20list->{lc($value)}; - $numval = 5 if (! defined($numval)); + if (! defined($numval)) {$numval = 5;} return sprintf('00%.2x',$numval); } @@ -1902,7 +1972,7 @@ sub dec_dpt1 { #Binary value my $model = shift; $numval = ($numval & 0x01); my $state = $dpttypes{$model}->{MIN}; # default - $state = $dpttypes{$model}->{MAX} if ($numval == 1); + if ($numval == 1) {$state = $dpttypes{$model}->{MAX};} return $state; } @@ -1911,7 +1981,7 @@ sub dec_dpt2 { #Step value (two-bit) my $model = shift; my $state = ($numval & 0x03); my @dpt2txt = qw(off on forceOff forceOn); - $state = $dpt2txt[$state] if ($model ne 'dpt2.000'); # JoeALLb request; + if ($model ne 'dpt2.000') {$state = $dpt2txt[$state];} # JoeALLb request; return $state; } @@ -1922,7 +1992,7 @@ sub dec_dpt3 { #Step value (four-bit) my $stepcode = 0; if ($step > 0) { $stepcode = int(100 / (2**($step-1))); - $stepcode *= -1 if ($dir == 0); + if ($dir == 0) {$stepcode *= -1;} } return sprintf ('%d', $stepcode); } @@ -1932,7 +2002,7 @@ sub dec_dpt5 { #1-Octet unsigned value my $model = shift; my $hash = shift; my $state = KNX_limit ($hash, $numval, $model, 'DECODE'); - return sprintf ('%d', $state); + return sprintf ('%.0f', $state); } sub dec_dpt6 { #1-Octet signed value @@ -1951,7 +2021,7 @@ sub dec_dpt7 { #2-Octet unsigned Value my $state = KNX_limit ($hash, $numval, $model, 'DECODE'); return sprintf ('%.2f', $state) if ($model eq 'dpt7.003'); return sprintf ('%.1f', $state) if ($model eq 'dpt7.004'); - return sprintf ('%d', $state); + return sprintf ('%.0f', $state); } sub dec_dpt8 { #2-Octet signed Value @@ -1970,10 +2040,10 @@ sub dec_dpt9 { #2-Octet Float value my $model = shift; my $hash = shift; my $sign = 1; - $sign = -1 if(($numval & 0x8000) > 0); + if(($numval & 0x8000) > 0) {$sign = -1;} my $exp = ($numval & 0x7800) >> 11; my $mant = ($numval & 0x07FF); - $mant = -(~($mant-1) & 0x07FF) if($sign == -1); + if($sign == -1) {$mant = -(~($mant-1) & 0x07FF);} $numval = (1 << $exp) * 0.01 * $mant; my $state = KNX_limit ($hash, $numval, $model, 'DECODE'); return $state; @@ -1996,8 +2066,7 @@ sub dec_dpt11 { #Date my $month = ($numval & 0x0F00) >> 8; my $year = ($numval & 0x7F); #translate year (21st cent if <90 / else 20th century) - $year += 1900 if($year >= 90); - $year += 2000 if($year < 90); + $year += ($year >= 90)?1900:2000; return sprintf('%02d.%02d.%04d',$day,$month,$year); } @@ -2045,8 +2114,8 @@ sub dec_dpt16 { #14-Octet String or dpt4: single Char string $value =~ s/\s*$//gxms; # strip trailing blanks my $state = pack('H*',$value); #convert from latin-1 - $state = Encode::encode ('utf8', Encode::decode('iso-8859-1',$state)) if ($model =~ m/^dpt(?:16.001|4.002)$/xms); - $state = q{} if ($state =~ m/^[\x00]+$/ixms); # case all zeros received + if ($model =~ m/^dpt(?:16.001|4.002)$/xms) {$state = Encode::encode ('utf8', Encode::decode('iso-8859-1',$state));} + if ($state =~ m/^[\x00]+$/ixms) {$state = q{};} # case all zeros received $state =~ s/[\x00-\x1F]+//gxms; # remove non printable chars return $state; } @@ -2062,6 +2131,7 @@ sub dec_dpt18 { #1-Octet scene - also used for dpt17 sub dec_dpt19 { #DateTime my $numval = substr(shift,-16); # strip off 1st byte + if (length($numval) < 16) { $numval = q{460101000000};} # 1970-01-01 00:00:00 my $time = hex (substr ($numval, 6, 6)); my $date = hex (substr ($numval, 0, 6)); my $secs = ($time & 0x3F); @@ -2080,7 +2150,7 @@ sub dec_dpt20 { #HVAC my $numval = hex (shift); $numval = ($numval & 0x07); # mask any non-std bits! my @dpt20_102txt = qw(Auto Comfort Standby Economy Protection reserved); - $numval = 5 if ($numval > 4); # dpt20.102 + if ($numval > 4) {$numval = 5;} # dpt20.102 return $dpt20_102txt[$numval]; } @@ -2088,8 +2158,8 @@ sub dec_dpt22 { #HVAC dpt22.101 only my $numval = hex (shift); return 'error' if (($numval & 0x01) == 1); my $state = (($numval & 0x0100) == 0)?'heating':'cooling'; - $state .= '_Frostalarm' if (($numval & 0x2000) != 0); - $state .= '_Overtempalarm' if (($numval & 0x4000) != 0); + if (($numval & 0x2000) != 0) {$state .= '_Frostalarm';} + if (($numval & 0x4000) != 0) {$state .= '_Overtempalarm';} return $state; } @@ -2115,7 +2185,7 @@ sub dec_dpt232 { #RGB-Code sub dec_dpt251 { #RGBW-Code my $numval = substr(shift,0,-4); # strip off controls - $numval = substr($numval,2,8) if (length($numval) == 10); # from set cmd + if (length($numval) == 10) {$numval = substr($numval,2,8);} # from set cmd return sprintf ('%.8x',hex($numval)); } @@ -2153,7 +2223,7 @@ sub KNX_Log { my $logtxt = shift; my $name = ( ref($dev) eq 'HASH' ) ? $dev->{NAME} : $dev; - my $dloglvl = AttrVal($name,'verbose',undef) // AttrVal('global','verbose',3); + my $dloglvl = AttrNum($name,'verbose',undef) // AttrNum('global','verbose',3); return if ($loglvl > $dloglvl); # shortcut performance my $sub = (caller(1))[3] // 'main'; @@ -2188,8 +2258,7 @@ sub main::KNX_scan { my $iohash; foreach my $knxdef (@devlist) { - next unless $knxdef; - next if($knxdef eq $devs && !$defs{$knxdef}); + next if (! defined($defs{$knxdef})); my $devhash = $defs{$knxdef}; next if ((! defined($devhash)) || ($devhash->{TYPE} ne 'KNX')); @@ -2207,10 +2276,10 @@ sub main::KNX_scan { $k++; push (@{$iohash->{Helper}->{knxscan}}, qq{$knxdef $key}); } - $j++ if ($k > $k0); + if ($k > $k0) {$j++;} } Log3 (undef, 3, qq{KNX_scan: $i devices selected (regex= $devs) / $j devices with get / $k "gets" executing...}); - doKNX_scan($iohash) if ($k > 0); + if ($k > 0) {doKNX_scan($iohash);} return $k; } @@ -2318,6 +2387,13 @@ This module provides a basic set of operations (on, off, toggle, on-until, on-fo interpretation/encoding of messages from/to devices manufactured by different vendors. These datatypes are used to interpret the status of a device, so the readings in FHEM will show the correct value and optional unit.

+

The KNX-Bus protocol defines three basic commands, any KNX-device, including FHEM, has to support: +

    +
  1. write - send a msg to KNX-bus - FHEM implementation: set cmd
  2. +
  3. read  - send request for a msg to another KNX-device - FHEM implementation: get cmd
  4. +
  5. reply - send answer when receiving a read-cmd from KNX-Bus - FHEM implementation: putCmd attribute
  6. +
+

For each received message there will be a reading containing the received value and the sender address.
For every set, there will be a reading containing the sent value.
The reading <state> will be updated with the last sent or received value. 

@@ -2340,12 +2416,14 @@ and setG<number>.
The optional parameteter <gadName> may contain an alias for the GAD. The following gadNames are not allowed: state, on, off, on-for-timer, on-until, off-for-timer, off-until, toggle, raw, rgb, string, value, set, get, listenonly, nosuffix - because of conflict with cmds & parameters.
-If you supply <gadName> this name is used instead. The reading-names are <gadName>-get and <gadName>-set. -The synonyms <getName> and <setName> are used in this documentation.
+If you supply <gadName> this name is used instead as cmd prefix. The reading-names are <gadName>-get and <gadName>-set. + The synonyms <getName> and <setName> are used in this documentation.
If you add the option "nosuffix", <getName> and <setName> have the identical name - <gadName>. -Both sent and received bus messages will be stored in the same reading <gadName>
-If you want to restrict the GAD, you can use the options "get", "set", or "listenonly". The usage should be self-explanatory. - It is not possible to combine the options.

+ Both sent and received bus messages will be stored in the same reading <gadName>
+If you want to restrict the GAD, use the options "get", "set", or "listenonly". The usage is described in Set/Get-cmd chapter. + It is not possible to combine the options.
+ +

Specifying an IO-Device in define is deprecated! Use attribute IODev instead, but only if absolutely required!

If enabled, the module autocreate is creating a new definition for each not already defined group-address. @@ -2363,16 +2441,16 @@ Examples: attr lamp1 webCmd on:off attr lamp1 devStateIcon on:li_wht_on:off off:li_wht_off:on - define lamp2 KNX 0/7/12:dpt1:steuern:set 0/7/13:dpt1.001:status:listenonly + define lamp2 KNX 0/7/12:dpt1:switch:set 0/7/13:dpt1.001:status:listenonly - define lamp3 KNX 0070D:dpt1.001 + define lamp3 KNX 0070E:dpt1.001 # equivalent to 0/7/14:dpt1.001

  • Set

    set <deviceName> [<gadName>] on|off|toggle
    set <deviceName> <gadName> blink <nr of blinks> <duration seconds>
    - set <deviceName> <gadName> on-for-timer|off-for-timer <duration seconds> # seconds < 1 are NOT supported!
    + set <deviceName> <gadName> on-for-timer|off-for-timer <duration seconds>
    set <deviceName> <gadName> on-until|off-until <timespec (HH:MM[:SS])>

    Set sends the given value to the bus.
    If <gadName> is omitted, the first listed GAD of the device definition is used. @@ -2392,13 +2470,16 @@ Examples: Examples: set lamp2 on # gadName omitted set lamp2 off # gadName omitted - set lamp2 steuern on - set lamp2 steuern off - set lamp2 steuern on-for-timer 10 # seconds - minimum time is 1 sec ! - set lamp2 steuern on-until 13:15:00 # if timestamp is earlier than "now", device will be switched off tomorrow ! + set lamp2 switch on + set lamp2 switch off + set lamp2 switch on-for-timer 10 # seconds - seconds > 86400 are supported, fractions of seconds if seconds < 10 + set lamp2 switch on-until 13:15:00 # if timestamp is earlier than "now", device will be switched off tomorrow ! + set lamp2 switch on-till 13:15:00 # if timestamp is earlier than "now", cmd will be rejected (same function as in SetExtensions) set lamp2 status on # will be refused - status has option "listenonly" set - set lamp3 g1 off-until 13:15:00 + set lamp3 g1 off-for-timer 86460 # switch off until tomorrow, same time + 60 seconds + set lamp3 g1 off-until 13:15:00 + set lamp3 g1 off-till 13:15:00 # if timestamp is earlier than "now", cmd will be rejected (same function as in SetExtensions) set lamp3 g1 toogle # lamp3 change state set lamp3 g1 blink 2 4 # lamp3 on for 4 seconds, off for 4 seconds, 2 repeats @@ -2458,40 +2539,69 @@ Examples:

  • Special attributes
      +
    • stateRegex
      + This function modifies the value of reading state. + It is executed directly after replacing the reading-names and processing "format" Attr, but before stateCmd.
      You can pass mutiple pairs of regex-patterns and strings to replace, separated by a space. A regex-pair is always in the format /<readingName>[:<value>]/[2nd part]/. - The first part of the regex must exactly match the readingname, and optional (separated by a colon) the readingValue. + The first part is not handled as regex, have to be exactly equal the readingname, and optional (separated by a colon) the readingValue. If first part match, the matching part will be replaced by the 2nd part of the regex. If the 2nd part is empty, the value is ignored and reading state will not get updated. The substitution is done every time a reading is updated. You can use this function for converting, adding units, - having more fun with icons, ...
      - This function has only an impact on the content of reading state. - It is executed directly after replacing the reading-names and processing "format" Attr, but before stateCmd. -
    • + having more fun with icons, ... +
      +Examples:
      +   attr <device> stateRegex /position:0/aus/ # on update of reading "position" and value=0, reading state value will be "aus"
      +   attr <device> stateRegex /position/pos-/  # on update of reading "position" reading state value will be prepended with "pos-".
      +   attr <device> stateRegex /desired-pos//   # reading state will NOT get updated when reading "desired-pos" is updated.
      +
      +
    • stateCmd
      - You can supply a perl-command for modifying state. This command is executed directly before updating the reading - - so after renaming, format and stateRegex. - Please supply a valid perl command like using the attribute stateFormat.
      - Unlike stateFormat the stateCmd modifies the content of the reading state, + Unlike stateFormat the stateCmd modifies the value of the reading state, not only the hash-content for visualization.
      + You can supply a perl-command for modifying the value of reading state. This command is executed directly before updating the reading + - so after renaming, format and stateRegex.
      You can access the device-hash ( e.g: $hash{IODev} ) in yr. perl-cmd. In addition the variables - "$name", "$gadName" and "$state" are avaliable. A return value must be set and overrides reading state. -
    • + "$name", "$gadName" and "$state" are avaliable. A return value must be set and overrides reading state value. + More examples - see Wiki +
      +Examples:
      +   attr <device> stateCmd {return sprintf("%.1f",ReadingsNum($name,'g1',0)*3.6);} # multiply reading value 'g1' and set value into state-reading
      +   attr <device> stateCmd {my99Utilssub($name,$gadName,$state);} # call subroutine (99_myUtils.pm ?) and use return value as state value
      +   attr <device> stateCmd {return 123 if ($gadName eq 'position' && $state eq 'off'); return $state;} # on change of GadName position and value = off ..., else no change  
      +
      +
    • putCmd
      Every time a KNX-value is requested from the bus to FHEM, the content of putCmd is evaluated before the answer is sent. You can use a perl-command for modifying content. This command is executed directly before sending the data. A copy is stored in the reading <putName>.
      - Each device only knows one putCmd, so you have to take care about the different GAD's in the perl string.
      + Each device only knows one putCmd, so you have to take care about the different GAD's/readings in the perl string.
      Like in stateCmd you can access the device hash ("$hash") in yr. perl-cmd. In addition the variables "$name", "$gadName" and "$state" are avaliable. On entry, "$state" contains the current value of reading "state". - The return-value will be sent to KNX-bus. The value has to be in the allowed range and format for the corresponding dpt, - else the send is rejected. The reading "state" will NOT get updated! + The return-value will be sent to KNX-bus unless the the return value is undefined. The value has to be in the + allowed range and format for the corresponding dpt, else the send is rejected. The reading "state" will NOT get updated.
       Examples:
      -   attr <device> putCmd {return $state if($gadName eq 'status'); return;}  #returns value of reading state on request from bus for gadName "status".
      -   attr <device> putCmd {return ReadingsVal('dummydev','state','error') if(...); return;}          #returns value of device "dummydev" reading "state".
      -   attr <device> putCmd {return (split(/[\s]/xms,TimeNow()))[1] if ($gadName eq 'time'); return;}  #returns system timestamp (dpt10 format) ...
      +   attr <device> putCmd {return $state if($gadName eq 'status'); return;} #returns value of reading state on request from bus for gadName "status".
      +   attr <device> putCmd {return ReadingsVal('dummydev','state','error') if(...); return;}         #returns value of device "dummydev" reading "state".
      +   attr <device> putCmd {return (split(/[\s]/xms,TimeNow()))[1] if ($gadName eq 'time'); return;} #returns system timestamp (dpt10 format) ...
       
    • format
      @@ -2505,9 +2615,9 @@ Examples:
      As an aid for debugging, an additional INTERNAL: "RAWMSG" will show any message received from bus while the device is disabled.
    • KNX_toggle
      - Lookup current value before issuing set <device> <gadName> toggle cmd.
      - FHEM has to retrieve a current value to make the toggle-cmd acting correctly. This attribute can be used to - define the source of the current value.
      + As there is no direct support in KNX devices for toggle cmd, we have to + lookup the current value before issuing set <device> <gadName> toggle cmd.
      + This attribute is used to define the source of the current value.
      Format is: <devicename>:<readingname>. If you want to use a reading from own device, you can use "$self" as devicename. Be aware that only on and off are supported as valid values when defining <device>:<readingname>.
      @@ -2540,7 +2650,7 @@ Examples:

      The following dpt are implemented and have to be assigned within the device definition. The values right to the dpt define the valid range of Set-command values and Get-command return values and units.

        -
      1. dpt1 off, on, toggle
      2. +
      3. dpt1 off, on, toggle - see Note 1
      4. dpt1.000 0, 1
      5. dpt1.001 off, on, toggle
      6. dpt1.002 false, true
      7. @@ -2688,7 +2798,7 @@ Examples:
      8. dpt14.036 -1.4e-45..+1.7e+38 W (heat flow rate)
      9. dpt14.037 -1.4e-45..+1.7e+38 J (heat quantity)
      10. dpt14.038 -1.4e-45..+1.7e+38 Ω (impedance)
      11. -
      12. dpt14.039 -1.4e-45..+1.7e+38 m (lenght)
      13. +
      14. dpt14.039 -1.4e-45..+1.7e+38 m (length)
      15. dpt14.040 -1.4e-45..+1.7e+38 J (light quantity)
      16. dpt14.041 -1.4e-45..+1.7e+38 cd/m² (luminance)
      17. dpt14.042 -1.4e-45..+1.7e+38 lm (luminous flux)
      18. @@ -2734,8 +2844,9 @@ Examples:
      19. dpt16 14 char ASCII string
      20. dpt16.000 14 char ASCII string
      21. dpt16.001 14 char ISO-8859-1 string (Latin1)
      22. -
      23. dpt17.001 Scene Nr: 0..63
      24. -
      25. dpt18.001 Scene Nr: 1..64. - only "activation" works..
      26. +
      27. dpt17.001 0..63 (scene nr)
      28. +
      29. dpt18 1..64 (szene nr)
      30. +
      31. dpt18.001 [(activate|learn),]1..64
      32. dpt19 DD.MM.YYYY_HH:MM:SS (Date&Time combined)
      33. dpt19.001 DD.MM.YYYY_HH:MM:SS (Date&Time combined)
      34. dpt20.102 HVAC mode
      35. @@ -2746,10 +2857,12 @@ Examples:
      36. dpt232 RRGGBB RGB-Value (hex)
      37. dpt251 RRGGBBWW RGBW-Value (hex)
      38. dpt251.600 RRGGBBWW RGBW-Value (hex)
      39. -
      40. dptRAW testing/debugging - see Note
      41. +
      42. dptRAW testing/debugging - see Note 2

      -Note: dptRAW is for testing/debugging only! You can send/receive hex strings of unlimited length (assuming the KNX-HW supports it). +Note 1: A toggle cmd is translated to on/off, as there is no direct support for toggle in KNX-devices. See also Attr KNX_toggle. +
      +Note 2: dptRAW is for testing/debugging only! You can send/receive hex strings of unlimited length (assuming the KNX-HW supports it). No conversion, limit-, plausibility-check is done, the hex values are sent unmodified to the KNX bus.
        syntax: set <device> <gadName> <hex-string>
           Examples of valid / invalid hex-strings: