############################################## # $Id$ package main; use strict; use warnings; use POSIX; sub CommandIF($$); sub GetBlockIf ($$); sub CmdIf($); sub ReplaceReadingsIf($); sub ReplaceAllReadingsIf($$); sub ParseCommandsIf($); sub EvalAllIf($); sub InternalIf($$$); sub ReadingValIf($$$); ##################################### sub IF_Initialize($$) { my %lhash = ( Fn=>"CommandIF", Hlp=>"() () ELSE (), executes FHEM commands depending on the condition"); $cmds{IF} = \%lhash; } sub GetBlockIf ($$) { my ($cmd,$match) = @_; my $count=0; my $first_pos=0; my $last_pos=0; my $err=""; while($cmd =~ /$match/g) { if (substr($cmd,pos($cmd)-1,1) eq substr($match,2,1)) { $count++; $first_pos=pos($cmd) if ($count == 1); } elsif (substr($cmd,pos($cmd)-1,1) eq substr($match,4,1)) { $count--; } if ($count < 0) { $err="right bracket without left bracket"; return ("",substr($cmd,pos($cmd)-1),$err,""); } if ($count == 0) { $last_pos=pos($cmd); last; } } if ($count > 0) { $err="no right bracket"; return ("",substr($cmd,$first_pos-1),$err); } if ($first_pos) { return (substr($cmd,0,$first_pos-1),substr($cmd,$first_pos,$last_pos-$first_pos-1),"",substr($cmd,$last_pos)); } else { return ($cmd,"","",""); } } sub InternalIf($$$) { my ($name,$internal,$regExp)=@_; my $r=""; my $element; $r=$defs{$name}{$internal}; if ($regExp) { $element = ($r =~ /$regExp/) ? $1 : ""; } else { $element=$r; } return($element); } sub ReadingValIf($$$) { my ($name,$reading,$regExp)=@_; my $r=""; my $element; $r=$defs{$name}{READINGS}{$reading}{VAL}; if ($regExp) { $element = ($r =~ /$regExp/) ? $1 : ""; } else { $element=$r; } return($element); } sub ReplaceReadingIf($) { my ($element) = @_; my $beginning; my $tailBlock; my $err; my $regExp=""; my ($name,$reading,$format)=split(":",$element); my $internal=""; if ($name) { return ($name,"unknown Device") if(!$defs{$name}); if ($reading) { if (substr($reading,0,1) eq "\&") { $internal = substr($reading,1); return ($name.":".$internal,"unknown internal") if(!$defs{$name}{$internal}); } else { return ($name.":".$reading,"unknown reading") if(!$defs{$name}{READINGS}{$reading}); } if ($format) { if ($format eq "d") { $regExp = '(-?\d+(\.\d+)?)'; } elsif (substr($format,0,1) eq '[') { ($beginning,$regExp,$err,$tailBlock)=GetBlockIf($format,'[\[\]]'); return ($regExp,$err) if ($err); return ($regExp,"no round brackets in regular expression") if ($regExp !~ /.*\(.*\)/); } else { return($format,"unknown expression format"); } } if ($internal) { return("InternalIf('$name','$internal','$regExp')",""); } else { return("ReadingValIf('$name','$reading','$regExp')",""); } } else { return("InternalIf('$name','STATE','$regExp')",""); } } } sub ReplaceAllReadingsIf($$) { my ($tailBlock,$evalFlag)= @_; my $block=""; my $beginning; my $err; my $cmd=""; my $ret=""; while ($tailBlock ne "") { ($beginning,$block,$err,$tailBlock)=GetBlockIf($tailBlock,'[\[\]]'); return ($block,$err) if ($err); if ($block ne "") { if ($block =~ /:/ or ($block =~ /[a-z]/i and $block =~ /^[a-z0-9._]*$/i)) { ($block,$err)=ReplaceReadingIf($block); return ($block,$err) if ($err); if ($evalFlag) { my $ret = eval $block; return($block." ",$@) if ($@); # return($eval,"no reading value") if (!$ret); $ret =~ s/'/\\'/g; $block=$ret; } } else { $block="[".$block."]"; } } $cmd.=$beginning.$block; } return ($cmd,""); } sub EvalAllIf($) { my ($tailBlock)= @_; my $eval=""; my $beginning; my $err; my $cmd=""; my $ret=""; while ($tailBlock ne "") { ($beginning,$eval,$err,$tailBlock)=GetBlockIf($tailBlock,'[\{\}]'); return ($eval,$err) if ($err); if ($eval) { if (substr($eval,0,1) eq "(") { my $ret = eval $eval; return($eval." ",$@) if ($@); $eval=$ret; } else { $eval="{".$eval."}"; } } $cmd.=$beginning.$eval; } return ($cmd,""); } sub ParseCommandsIf($) { my($tailBlock) = @_; my $currentBlock=""; my $beginning=""; my $err=""; my $parsedCmd=""; my $pos=0; $tailBlock =~ s/;/;;/g; my $sleep; while ($tailBlock ne "") { if ($tailBlock=~ /^\s*\{/) { # perl block ($beginning,$currentBlock,$err,$tailBlock)=GetBlockIf($tailBlock,'[\{\}]'); return ($currentBlock,$err) if ($err); $parsedCmd.=$currentBlock; } if ($tailBlock =~ /^\s*IF/) { ($beginning,$currentBlock,$err,$tailBlock)=GetBlockIf($tailBlock,'[\(\)]'); #condition return ($currentBlock,$err) if ($err); $parsedCmd.="fhem('".$beginning."(".$currentBlock.")"; ($beginning,$currentBlock,$err,$tailBlock)=GetBlockIf($tailBlock,'[\(\)]'); #if case return ($currentBlock,$err) if ($err); $currentBlock =~ s/'/\\'/g; $currentBlock =~ s/;/;;/g; $parsedCmd.=$beginning."(".$currentBlock.")"; if ($tailBlock =~ /^\s*ELSE/) { ($beginning,$currentBlock,$err,$tailBlock)=GetBlockIf($tailBlock,'[\(\)]'); #else case return ($currentBlock,$err) if ($err); $currentBlock =~ s/'/\\'/g; $currentBlock =~ s/;/;;/g; $parsedCmd.=$beginning."(".$currentBlock.")"; } $parsedCmd.="')"; } else { #replace Readings if no IF command if ($tailBlock =~ /^\s*\(/) { # remove bracket ($beginning,$currentBlock,$err,$tailBlock)=GetBlockIf($tailBlock,'[\(\)]'); return ($currentBlock,$err) if ($err); $tailBlock=substr($tailBlock,pos($tailBlock)) if ($tailBlock =~ /^\s*,/g); } elsif ($tailBlock =~ /,/g) { $pos=pos($tailBlock)-1; $currentBlock=substr($tailBlock,0,$pos); $tailBlock=substr($tailBlock,$pos+1); } else { $currentBlock=$tailBlock; $tailBlock=""; } if ($currentBlock =~ /[^\s]/g) { $currentBlock =~ s/'/\\'/g; ($currentBlock,$err)=ReplaceAllReadingsIf($currentBlock,1); return ($currentBlock,$err) if ($err); ($currentBlock,$err)=EvalAllIf($currentBlock); $currentBlock =~ s/;/;;/g; return ($currentBlock,$err) if ($err); if ($sleep) { $parsedCmd.=$currentBlock; if ($tailBlock) { $parsedCmd.=";;" } else { $parsedCmd.="')" } } elsif ($currentBlock =~ /^\s*sleep/) { $sleep=1; $parsedCmd.="fhem('".$currentBlock.";;"; $parsedCmd.="')" if !($tailBlock); } else { $parsedCmd.="fhem('".$currentBlock."')"; $parsedCmd.=";;" if ($tailBlock); } } else { $parsedCmd.=";;" if ($tailBlock); } } } return($parsedCmd,""); } sub CmdIf($) { my($cmd) = @_; my $cond=""; my $err=""; my $if_cmd=""; my $else_cmd=""; my $tail; my $tailBlock; my $eval=""; my $beginning; $cmd =~ s/\n//g; return($cmd, "no left bracket") if ($cmd !~ /^ *\(/); ($beginning,$cond,$err,$tail)=GetBlockIf($cmd,'[\(\)]'); return ($cond,$err) if ($err); ($cond,$err)=ReplaceAllReadingsIf($cond,0); return ($cond,$err) if ($err); return ($cmd,"no condition") if ($cond eq ""); if ($tail =~ /^\s*\(/) { ($beginning,$if_cmd,$err,$tail)=GetBlockIf($tail,'[\(\)]'); return ($if_cmd,$err) if ($err); ($if_cmd,$err)=ParseCommandsIf($if_cmd); return ($if_cmd,$err) if ($err); return ($cmd,"no commands") if ($if_cmd eq ""); } else { return($tail, "no left bracket"); } return ($if_cmd,$err) if ($err); if (length($tail)) { $tail =~ /^\s*ELSE/g; if (pos($tail)) { $tail=substr($tail,pos($tail)); if (!length($tail)) { return ($tail,"no else block"); } } else { return ($tail,"expected ELSE"); } if ($tail =~ /^\s*\(/) { ($beginning,$else_cmd,$err,$tail)=GetBlockIf($tail,'[\(\)]'); return ($else_cmd,$err) if ($err); ($else_cmd,$err)=ParseCommandsIf($else_cmd); return ($else_cmd,$err) if ($err); } else { return($tail, "no left bracket"); } return ($else_cmd,$err) if ($err); } my $perl_cmd="{if(".$cond.")"; $perl_cmd .="{".$if_cmd."}"; $perl_cmd .= "else{".$else_cmd."}" if ($else_cmd); $perl_cmd.="}"; return($perl_cmd,""); } sub CommandIF($$) { my ($cl, $param) = @_; return "Usage: IF () () ELSE ()\n" if (!$param); my $ret; #print ("vor IF:$param\n"); my ($cmd,$err)=CmdIf($param); #print ("nach IF:$cmd\n"); if ($err ne "") { $ret="IF: $err: $cmd"; } else { $ret = AnalyzeCommandChain(undef,$cmd); use strict "refs"; } return $ret; } 1; =pod =item summary FHEM IF-command =item summary_DE FHEM IF-Befehl =begin html

IF

    IF (<condition>) (<FHEM commands1>) ELSE (<FHEM commands2>)

    Executes <FHEM commands1> if <condition> is true, else <FHEM commands2> are executed.

    IF can be used anywhere where FHEM commands can be used.

    The ELSE-case is optional.

    The <condition> is the same as in perl-if.

    In addition, readings can be specified in the form:

    [<device>:<reading>:<format>|[<regular expression>]]

    In addition, internals can be specified with & in the form:

    [<device>:&<internal>:<format>|[<regular expression>]]

    <format> and [<regular expression>] are filter options und are optional.

    possible <format>:

    'd' for decimal number

    If only the state of a device is to be used, then only the device can be specified:

    [<device>] corresponsed to [<device>:&STATE]

    Examples:

    IF in combination with at-module, Reading specified in the condition:

    define check at +00:10 IF ([outdoor:humidity] > 70) (set switch1 off) ELSE (set switch1 on)

    IF state query of the device "outdoor" in the condition:

    define check at +00:10 IF ([outdoor] eq "open") (set switch1 on)

    corresponds with details of the internal:

    define check at +00:10 IF ([outdoor:&STATE] eq "open") (set switch1 on)

    If the reading "state" to be queried, then the name of reading is specified without &:

    define check at +00:10 IF ([outdoor:state] eq "open") (set switch1 on)

    Nested IF commands (It can be entered in the DEF input on multiple lines with indentation for better representation):

    define test notify lamp
    IF ([lampe] eq "on") (
      IF ([outdoor:humidity] < 70)
        (set lamp off)
      ELSE
        (set lamp on)
    ) ELSE
      (set switch on)

    Filter by numbers in Reading "temperature":

    define settemp at 22:00 IF ([tempsens:temperature:d] >= 10) (set heating on)

    Filter by "on" and "off" in the status of the device "move":

    define activity notify move IF ([move:&STATE:[(on|off)]] eq "on" and $we) (set lamp off)

    Example of the use of Readings in the then-case:

    define temp at 18:00 IF ([outdoor:temperature] > 10) (set lampe [dummy])

    If an expression is to be evaluated first in a FHEM command, then it must be enclosed in brackets.
    For example, if at 18:00 clock the outside temperature is higher than 10 degrees, the desired temperature is increased by 1 degree:

    define temp at 18:00 IF ([outdoor:temperature] > 10) (set thermostat desired-temp {([thermostat:desired-temp:d]+1)})

    Multiple commands are separated by a comma instead of a semicolon, thus eliminating the doubling, quadrupling, etc. of the semicolon:

    define check at +00:10 IF ([outdoor:humidity] > 10) (set switch1 off,set switch2 on) ELSE (set switch1 on,set switch2 off)

    If a comma in FHEM expression occurs, this must be additionally bracketed so that the comma is not recognized as a delimiter:

    define check at +00:10 IF ([outdoor:humidity] > 10) ((set switch1,switch2 off))

    IF in combination with a define at multiple set commands:

    define check at *10:00 IF ([indoor] eq "on") (define a_test at +00:10 set lampe1 on;;set lampe2 off;;set temp desired 20)

    The comma can be combined as a separator between the FHEM commands with double semicolon, eg:

    define check at *10:00 IF ([indoor] eq "on") (set lamp1 on,define a_test at +00:10 set lampe2 on;;set lampe3 off;;set temp desired 20)

    sleep can be used with comma, it is not blocking:

    define check at *10:00 IF ([indoor] eq "on") (sleep 2,set lampe1 on,sleep 3,set lampe2 on)

    Time-dependent switch: In the period 20:00 to 22:00 clock the light should go off when it was on and I leave the room:

    define n_lamp_off notify sensor IF ($hms gt "20:00" and $hms lt "22:00" and [sensor] eq "absent") (set lamp:FILTER=STATE!=off off)

    Combination of Perl and FHEM commands ($NAME and $EVENT can also be used):

    define mail notify door:open IF ([alarm] eq "on")({system("wmail $NAME:$EVENT")},set alarm_signal on)
=end html =begin html_DE

IF

    IF (<Bedingung>) (<FHEM-Kommandos1>) ELSE (<FHEM-Kommandos2>)

    Es werden <FHEM-Kommandos1> ausgeführt, wenn <Bedingung> erfüllt ist, sonst werden <FHEM-Kommanodos2> ausgeführt.

    Beim IF-Befehl (IF in Großbuchstaben) handelt es sich um einen FHEM-Befehl. Der Befehl kann überall dort genutzt werden, wo FHEM-Befehle vorkommen dürfen. Im Gegensatz zu Perl-if (if in Kleinbuchstaben) bleibt man auf der FHEM-Ebene und muss nicht auf die Perl-Ebene, um FHEM-Befehle mit Hilfe der fhem-Funktion auszuführen.

    IF ist kein eigenständig arbeitendes Modul, sondern ein FHEM-Befehl, der nur in Kombination mit anderen Modulen, wie z. B. notify oder at, sinnvoll eingesetzt werden kann. Es gibt inzwischen ein neueres DOIF-Modul, welches auf der Syntax vom IF-Befehl aufbaut. Es arbeitet im Gegensatz zu IF als Modul selbstständig ereignis- und zeitgesteuert ohne notify bzw. at. Damit lassen sich viele Problemlösungen eleganter, jeweils mit einem einzigen Modul, realisieren.

    In der Bedingung des IF-Befehls wird die vollständige Syntax des Perl-if unterstützt. Stati und Readings von Devices werden in eckigen Klammern angegeben.


    Beispiele:

    IF in Kombination mit at-Modul, Readingangabe [<Device>:<Reading>] in der Bedingung:

    define check at +00:10 IF ([outdoor:humidity] > 70) (set switch1 off) ELSE (set switch1 on)

    IF Statusabfrage des Devices "outdoor" in der Bedingung:

    define check at +00:10 IF ([outdoor] eq "open") (set switch1 on)

    entspricht mit Angabe des Internals mit &:

    define check at +00:10 IF ([outdoor:&STATE] eq "open") (set switch1 on)

    Wenn der Reading "state" abgefragt werden soll, dann wird der Readingname ohne & angegeben:

    define check at +00:10 IF ([outdoor:state] eq "open") (set switch1 on)

    Geschachtelte Angabe von mehreren IF-Befehlen kann in mehreren Zeilen mit Einrückungen zwecks übersichtlicher Darstellung über FHEM-Weboberfläche in der DEF-Eingabe eingegeben werden.
    Die erste Zeile "define test notify lamp " muss mit einem Leerzeichen enden, bevor die Zeile mit Enter umgebrochen wird - das ist eine Eigenschaft von notify und nicht von IF:

    define test notify lamp
    IF ([lamp] eq "on") (
      IF ([outdoor:humidity] < 70)
        (set lamp off)
      ELSE
        (set lamp on)
    ) ELSE
      (set switch on)

    Mehrzeilige Eingaben in der cfg-Datei müssen dagegen jeweils am Zeilenende mit \ verknüpft werden (das ist eine Eigenschaft von FHEM und nicht von IF):

    define test notify lamp \
    IF ([lamp] eq "on") (\
      IF ([outdoor:humidity] < 70)\
        (set lamp off)\
      ELSE\
        (set lamp on)\
    ) ELSE\
      (set switch on)

    Filtern nach Zahlen im Reading "temperature":

    define settemp at 22:00 IF ([tempsens:temperature:d] >= 10) (set heating on)

    Filtern nach "on" und "off" im Status des Devices "move":

    define activity notify move IF ([move:&STATE:[(on|off)]] eq "on" and $we) (set lamp off)

    Beispiel für die Nutzung des Status eines Devices im Ausführungsteil. Hier: "lamp1" wird mit dem Status von "lamp2" geschaltet:

    define temp at 18:00 IF ([outdoor:temperature] > 10) (set lamp1 [lamp2])

    Falls bei einem FHEM-Befehl ein Perl-Ausdruck mit Readings zuvor ausgewertet werden soll, so muss er in geschweifte und runde Klammern gesetzt werden.
    Beispiel: Wenn um 18:00 Uhr die Außentemperatur höher ist als 10 Grad, dann wird die Solltemperatur um 1 Grad erhöht.

    define temp at 18:00 IF ([outdoor:temperature] > 10) (set thermostat desired-temp {([thermostat:desired-temp:d]+1)})

    Mehrerer Befehle werden durch ein Komma statt durch ein Semikolon getrennt, dadurch entfällt das Doppeln, Vervierfachen usw. des Semikolons:

    define check at +00:10 IF ([outdoor:humidity] > 10) (set switch1 off,set switch2 on) ELSE (set switch1 on,set switch2 off)

    Falls ein Komma im FHEM-Ausdruck vorkommt, muss dieser zusätzlich geklammert werden, damit das Komma nicht als Trennzeichen erkannt wird:

    define check at +00:10 IF ([outdoor:humidity] > 10) ((set switch1,switch2 off))

    IF in Kombination mit einem define at mit mehreren set-Befehlen (Eingabe muss wegen der Semikolons im DEF-Editor erfolgen, einfaches Semikolon ist nicht erlaubt - es würde vom FHEM-Parser "geschluckt" werden und beim IF nicht mehr ankommen):

    define check at *10:00 IF ([indoor] eq "on") (define a_test at +00:10 set lampe1 on;;set lampe2 off;;set temp desired 20)

    Man kann die Problematik des Doppelns von Semikolons wie folgt umgehen:

    define check at *10:00 IF ([indoor] eq "on") (define a_test at +00:10 IF (1) (set lampe1 on,set lampe2 off,set temp desired 20))

    Das Komma als Trennzeichen zwischen den FHEM-Befehlen lässt sich mit ;; kombinieren, z. B.:

    define check at *10:00 IF ([indoor] eq "on") (set lamp1 on,define a_test at +00:10 set lampe2 on;;set lampe3 off;;set temp desired 20)

    sleep kann mit Komma verwendet werden, dabei wirkt das sleep nicht blockierend:

    define check at *10:00 IF ([indoor] eq "on") (sleep 2,set lampe1 on,sleep 3,set lampe2 on)

    Zeitabhängig schalten: In der Zeit zwischen 20:00 und 22:00 Uhr soll das Licht ausgehen, wenn es an war und ich den Raum verlasse:

    define n_lamp_off notify sensor IF ($hms gt "20:00" and $hms lt "22:00" and [sensor] eq "absent") (set lamp:FILTER=STATE!=off off)

    Kombination von Perl und FHEM-Befehlen ($NAME sowie $EVENT können ebenso benutzt werden):

    define mail notify door:open IF ([alarm] eq "on")({system("wmail $NAME:$EVENT")},set alarm_signal on)

    Der IF-Befehl dient in erster Linie zur Vereinfachung der Schreibweise in Kombination mit anderen FHEM-Modulen wie at, notify oder DOIF. Intern wird der IF-Befehl zur Ausführung in einen Perl if-Befehl umgesetzt. Das soll anhand von Beispielen verdeutlicht werden:

    IF ([switch] eq "off") (set lamp on)

    entspricht:

    {if (Value('switch') eq "off"){fhem('set lamp on')}}


    IF ([living_room:temperature] > 12) (set lamp on, set lamp2 off)

    entspricht:

    {if (ReadingVal('living_room','temperature','') > 12) {fhem('set lamp on');;fhem('set lamp2 off')}}


    IF ([bathroom:humidity] > 70) (set led red) ELSE (set led green)

    entspricht:

    {if (ReadingsVal('bathroom','humidity','') > 70) {fhem('set led red')} else {fhem('set led green')}}


=end html_DE =cut