From bd285f94a7dfb081ee561b63bfd3f249738c50ae Mon Sep 17 00:00:00 2001 From: justme-1968 Date: Sat, 26 Jan 2019 16:45:12 +0000 Subject: [PATCH] 30_tradfri.pm: new module CoProcess.pm: new file git-svn-id: https://svn.fhem.de/fhem/trunk@18425 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/30_tradfri.pm | 679 ++++++++++++++++++++++++++++++++++++++++ fhem/FHEM/CoProcess.pm | 416 ++++++++++++++++++++++++ fhem/MAINTAINER.txt | 9 +- 4 files changed, 1102 insertions(+), 3 deletions(-) create mode 100644 fhem/FHEM/30_tradfri.pm create mode 100644 fhem/FHEM/CoProcess.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 695890521..775e52940 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 30_tradfri: initial release - new: 42_npmjs: Module to update Node.js modules via NPM package manager - feature: 98_Hyperion: add set active/inactive - bugfix: 22_HOMEMODE: fix zero devider in HOMEMODE_ContactOpenCheck diff --git a/fhem/FHEM/30_tradfri.pm b/fhem/FHEM/30_tradfri.pm new file mode 100644 index 000000000..5b67aed01 --- /dev/null +++ b/fhem/FHEM/30_tradfri.pm @@ -0,0 +1,679 @@ + +# $Id$ + +package main; + +use strict; +use warnings; + +use CoProcess; +#require "30_HUEBridge.pm"; +require "$attr{global}{modpath}/FHEM/30_HUEBridge.pm"; + +use JSON; +use Data::Dumper; + +use POSIX; +use Socket; + +use vars qw(%modules); +use vars qw(%defs); +use vars qw(%attr); +use vars qw($readingFnAttributes); +use vars qw($FW_ME); + +sub Log($$); +sub Log3($$$); + +sub +tradfri_Initialize($) +{ + my ($hash) = @_; + + $hash->{ReadFn} = "tradfri_Read"; + $hash->{WriteFn} = "tradfri_Write"; + + $hash->{DefFn} = "tradfri_Define"; + $hash->{NotifyFn} = "tradfri_Notify"; + $hash->{UndefFn} = "tradfri_Undefine"; + $hash->{DelayedShutdownFn} = "tradfri_DelayedShutdown"; + $hash->{ShutdownFn} = "tradfri_Shutdown"; + $hash->{SetFn} = "tradfri_Set"; + $hash->{GetFn} = "tradfri_Get"; + $hash->{AttrFn} = "tradfri_Attr"; + $hash->{AttrList} = "tradfriFHEM-cmd ". + "tradfriFHEM-params ". + "tradfriFHEM-securityCode ". + "tradfriFHEM-sshHost tradfriFHEM-sshUser ". + "disable:1 disabledForIntervals ". + "createGroupReadings:1,0 ". + $readingFnAttributes; +} + +##################################### + +sub +tradfri_AttrDefaults($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + +} + +sub +tradfri_Define($$) +{ + my ($hash, $def) = @_; + + my @a = split("[ \t][ \t]*", $def); + + return "Usage: define tradfri" if(@a != 2); + + my $name = $a[0]; + $hash->{NAME} = $name; + + my $d = $modules{$hash->{TYPE}}{defptr}; + return "$hash->{TYPE} device already defined as $d->{NAME}." if( defined($d) && $name ne $d->{NAME} ); + $modules{$hash->{TYPE}}{defptr} = $hash; + + tradfri_AttrDefaults($hash); + + $hash->{NOTIFYDEV} = "global"; + + if( !AttrVal($name, 'devStateIcon', undef ) ) { + CommandAttr(undef, "$name createGroupReadings 0"); + CommandAttr(undef, "$name stateFormat tradfri-fhem"); + CommandAttr(undef, "$name devStateIcon stopped:control_home\@red:start stopping:control_on_off\@orange running.*:control_on_off\@green:stop") + } + + $hash->{CoProcess} = { name => 'tradfri-fhem', + cmdFn => 'tradfri_getCmd', + }; + + $hash->{helper}{scenes} = {} if( !$hash->{helper}{scenes} ); + + if( $init_done ) { + CoProcess::start($hash); + } else { + $hash->{STATE} = 'active'; + } + + return undef; +} + +sub +tradfri_Notify($$) +{ + my ($hash,$dev) = @_; + + return if($dev->{NAME} ne "global"); + return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); + + CoProcess::start($hash); + + return undef; +} + +sub +tradfri_Undefine($$) +{ + my ($hash, $name) = @_; + + CoProcess::terminate($hash); + + delete $modules{$hash->{TYPE}}{defptr}; + + return undef; +} +sub +tradfri_DelayedShutdown($) +{ + my ($hash) = @_; + + if( $hash->{PID} ) { + $hash->{shutdown} = 1; + $hash->{shutdown} = $hash->{CL} if( $hash->{CL} ); + + $hash->{reason} = 'shutdown'; + CoProcess::stop($hash); + + return 1; + } + + return undef; +} + +sub +tradfri_Shutdown($) +{ + my ($hash) = @_; + + CoProcess::terminate($hash); + + delete $modules{$hash->{TYPE}}{defptr}; + + return undef; +} + +sub +tradfri_processEvent($$) { + my ($hash,$decoded) = @_; + my $name = $hash->{NAME}; + + my $id = $decoded->{id} ; + + if( $decoded->{r} eq 'scene' ) { + if( $decoded->{t} eq 'remove' ) { + delete $hash->{helper}{scenes}{$id}; + Log3 $name, 4, "$name: deleted scene $id"; + } else { + Log3 $name, 4, "$name: ". ($hash->{helper}{scenes}{$id}?'updated':'added') ." scene $id"; + $hash->{helper}{scenes}{$id} = $decoded; + } + return; + } + + my $code = ''; + $code = $name ."-". $id if( $decoded->{r} eq 'lights' ); + $code = $name ."-G". $id if( $decoded->{r} eq 'group' ); + $code = $name ."-S". $id if( $decoded->{r} eq 'sensor' ); + + my $chash = $modules{HUEDevice}{defptr}{$code}; + + if( $decoded->{t} eq 'remove' ) { + if( $chash ) { + fhem( "delete $chash->{NAME} " ); + } + return; + } + + if( defined($chash) ) { + HUEDevice_Parse($chash,$decoded); + HUEBridge_updateGroups($hash, $chash->{ID}) if( !$chash->{helper}{devtype} ); + + } else { + + my $group; + my $cname; + my $define; + if( $decoded->{r} eq 'lights' ) { + $group = 'HUEDevice'; + $cname = "HUEDevice" . $id; + #$cname = $name ."_". $cname if( $hash->{helper}{count} ); + $define= "$cname HUEDevice $id IODev=$name"; + } elsif( $decoded->{r} eq 'group' ) { + $group = 'HUEGroup'; + $cname = "HUEGroup" . $id; + #$cname = $name ."_". $cname if( $hash->{helper}{count} ); + $define= "$cname HUEDevice group $id IODev=$name"; + } elsif( $decoded->{r} eq 'sensor' ) { + $group = 'HUESensor'; + $cname = "HUESensor" . $id; + #$cname = $name ."_". $cname if( $hash->{helper}{count} ); + $define= "$cname HUEDevice sensor $id IODev=$name"; + } + + + if( $define ) { + Log3 $name, 4, "$name: create new device '$cname' for address '$id'"; + Log3 $name, 5, "$name: $define"; + + if( my $ret = CommandDefine(undef,$define) ) { + Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $ret"; + + } else { + CommandAttr(undef,"$cname alias ".$decoded->{name}); + CommandAttr(undef,"$cname room Tradfri"); + #CommandAttr(undef,"$cname IODev $name"); + + CommandAttr(undef, "$name createGroupReadings 1") if( $decoded->{r} eq 'group' ); + + HUEDeviceSetIcon($cname); + $defs{$cname}{helper}{fromAutocreate} = 1 ; + + CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); + + my $chash = $modules{HUEDevice}{defptr}{$code}; + if( defined($chash) ) { + HUEDevice_Parse($chash,$decoded); + HUEBridge_updateGroups($hash, $chash->{ID}) if( !$chash->{helper}{devtype} ); + } + } + + } else { + Log3 $name, 4, "$name: message for unknow device received: $code: ". Dumper $decoded; + } + } +} + +sub +tradfri_Read($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $buf = CoProcess::readFn($hash); + return undef if( !$buf ); + + my $data = $hash->{helper}{PARTIAL}; + $data .= $buf; + + while($data =~ m/\n/) { + ($buf,$data) = split("\n", $data, 2); + + Log3 $name, 5, "$name: read: $buf"; + + if( $buf =~ m/^\*\*\* ([^\s]+) (.+)/ ) { + my $service = $1; + my $message = $2; + + if( $service eq 'FHEM:' ) { + if( $message =~ m/^connection failed(, (.*))?/ ) { + my $reason = $2; + + $hash->{reason} = 'failed to connect to gateway'; + $hash->{reason} .= ": $reason" if( $reason ); + + if( $reason eq 'credentials wrong' ) { + fhem( "deletereading $name identity" ); + fhem( "deletereading $name psk" ); + + CoProcess::start($hash); + + } elsif( $reason eq 'credentials missing' ) { + CoProcess::stop($hash); + + } elsif( $reason ) { #secret wrong + CoProcess::stop($hash); + + } else { + CoProcess::start($hash); + + } + + } elsif( $message =~ m/^identity: (.*)/ ) { + my $identity = $1; + readingsSingleUpdate($hash, 'identity', tradfri_encrypt($identity), 1 ); + + } elsif( $message =~ m/^psk: (.*)/ ) { + my $psk = $1; + readingsSingleUpdate($hash, 'psk', tradfri_encrypt($psk), 1 ); + + } else { + Log3 $name, 4, "$name: unhandled message: $message"; + } + } + + } elsif( $buf =~ m/this is tradfri-fhem (.*)/ ) { + $hash->{'tradfri-fhem version'} = $1; + + } elsif( $buf =~ m/^\{.*\}/ ) { + if( my $decoded = eval { JSON->new->utf8(0)->decode($buf) } ) { + tradfri_processEvent($hash,$decoded); + + } else { + Log3 $name, 2, "$name: json error: $@ in $buf"; + + } + + } else { + Log3 $name, 4, "$name: $buf"; + } + } + + $hash->{PARTIAL} = $data; + + return undef; +} + +sub +tradfri_Write($@) +{ + my ($hash,$chash,$cname,$id,$obj)= @_; + my $name = $hash->{NAME}; + + return undef if( IsDisabled($name) ); + + #$id =~ s'/.*''g; + #$obj->{id} = $id; + + $id = $1 if( $id =~ m/^G(\d+)/ ); + $id = $1 if( $id =~ m/^(\d+)/ ); + + $obj->{id} = $id; + + $obj->{t} = 'lights'; + $obj->{t} = 'group' if( $chash->{helper}{devtype} eq 'G' ); + + if( $hash->{FH} ) { + my $encoded = encode_json($obj); + + Log3 $name, 5, "$name: writing: $encoded"; + + $encoded .= "\n"; + syswrite( $hash->{FH}, $encoded ); + } else { + Log3 $name, 3, "$name: not connected"; + } + + return undef; +} + + +sub +tradfri_getCmd($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + return undef if( !$init_done ); + + my $ssh_cmd; + if( my $host = AttrVal($name, 'tradfriFHEM-sshHost', undef ) ) { + my $ssh = qx( which ssh ); chomp( $ssh ); + if( my $user = AttrVal($name, 'tradfriFHEM-sshUser', undef ) ) { + $ssh_cmd = "$ssh $host -u $user"; + } else { + $ssh_cmd = "$ssh $host"; + } + + Log3 $name, 3, "$name: using ssh cmd $ssh_cmd"; + } + + my $cmd; + if( $ssh_cmd ) { + $cmd = AttrVal( $name, "tradfriFHEM-cmd", qx( $ssh_cmd which tradfri-fhem ) ); + } else { + $cmd = AttrVal( $name, "tradfriFHEM-cmd", qx( which tradfri-fhem ) ); + } + chomp( $cmd ); + + if( !$ssh_cmd && !(-X $cmd) ) { + my $msg = "tradfri-fhem not installed. install with 'sudo npm install -g tradfri-fhem'."; + $msg = "$cmd does not exist" if( $cmd ); + return (undef, $msg); + } + + $cmd = "$ssh_cmd $cmd" if( $ssh_cmd ); + + if( my $security_code = AttrVal($name, 'tradfriFHEM-securityCode', undef ) ) { + $cmd .= ' -s '. tradfri_decrypt($security_code); + } else { + my $msg = 'security code missing'; + return (undef, $msg); + } + + if( my $identity = ReadingsVal($name, 'identity', undef ) ) { + $cmd .= " -i ". tradfri_decrypt($identity) ; + } + + if( my $psk = ReadingsVal($name, 'psk', undef ) ) { + $cmd .= " -p ". tradfri_decrypt($psk) ; + } + + if( my $params = AttrVal($name, 'tradfriFHEM-params', undef ) ) { + $cmd .= " $params"; + } + + if( AttrVal( $name, 'verbose', 3 ) == 5 ) { + Log3 $name, 2, "$name: starting tradfri-fhem: $cmd"; + } else { + my $msg = $cmd; + $msg =~ s/-s\s+[^\s]+/-s sssss/g; + $msg =~ s/-i\s+[^\s]+/-i iiiii/g; + $msg =~ s/-p\s+[^\s]+/-p ppppp/g; + Log3 $name, 2, "$name: starting tradfri-fhem: $msg"; + } + + return $cmd; +} + +sub +tradfri_Set($$@) +{ + my ($hash, $name, $cmd, @args) = @_; + + my $list = ""; + + if( $cmd eq 'scene' ) { + return "usage: scene " if( @args != 1 ); + + my $id = $args[0]; + if( !defined($hash->{helper}{scenes}{$id}) ) { + foreach my $key ( keys %{$hash->{helper}{scenes}} ) { + if( $id eq $hash->{helper}{scenes}{$key}{name} ) { + $id = $key; + last; + } + } + + return "no such scene" if( !defined($hash->{helper}{scenes}{$id}) ); + } + + my $scene = $hash->{helper}{scenes}{$id}; + + my $obj = { 'sceneId' => 0+$id }; + + my $code = $name ."-G". $scene->{group}; + my $chash = $modules{HUEDevice}{defptr}{$code}; + tradfri_Write($hash, $chash, $chash->{NAME}, $chash->{ID} , $obj); + + return undef; + + } elsif( $cmd eq 'statusRequest' ) { + #unused + return undef; + + } + + my $scenes; + foreach my $key ( sort {$a cmp $b} keys %{$hash->{helper}{scenes}} ) { + $scenes .=',' if( $scenes ); + my $name = $hash->{helper}{scenes}{$key}{name}; + $name =~ s/ /#/g; + $scenes .= $name; + } + if( $scenes ) { + $list .= " " if( $list ); + $list .= " scene:$scenes"; + } + + return CoProcess::setCommands($hash, $list, $cmd, @args); +} + + + +sub +tradfri_Get($$@) +{ + my ($hash, $name, $cmd) = @_; + + my $list = 'scenes:noArg'; + + if( $cmd eq 'scenes' ) { + my $ret; + foreach my $key ( sort {$a cmp $b} keys %{$hash->{helper}{scenes}} ) { + my $scene = $hash->{helper}{scenes}{$key}; + + my $group = $scene->{group}; + my $code = $name ."-G". $scene->{group}; + if( my $chash = $modules{HUEDevice}{defptr}{$code} ) { + $group = AttrVal( $chash->{NAME}, 'alias', $group ); + } + + $ret .= sprintf( "%-20s %-20s %-20s", $key, $group, $scene->{name} ); + $ret .= sprintf( " %s\n", join( ",", @{$scene->{lights}} ) ); + } + if( $ret ) { + my $header = sprintf( "%-20s %-20s %-20s", "ID", "GROUP", "NAME" ); + $header .= sprintf( " %s\n", "LIGHTS" ); + $ret = $header . $ret; + } + return $ret; + + } + + return "Unknown argument $cmd, choose one of $list"; +} + +sub +tradfri_Parse($$;$) +{ + my ($hash,$data,$peerhost) = @_; + my $name = $hash->{NAME}; +} + +sub +tradfri_encrypt($) +{ + my ($decoded) = @_; + my $key = getUniqueId(); + + return "" if( !$decoded ); + return $decoded if( $decoded =~ /^crypt:(.*)/ ); + + my $encoded; + for my $char (split //, $decoded) { + my $encode = chop($key); + $encoded .= sprintf("%.2x",ord($char)^ord($encode)); + $key = $encode.$key; + } + + return 'crypt:'. $encoded; +} +sub +tradfri_decrypt($) +{ + my ($encoded) = @_; + my $key = getUniqueId(); + + return "" if( !$encoded ); + + $encoded = $1 if( $encoded =~ /^crypt:(.*)/ ); + + my $decoded; + for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) { + my $decode = chop($key); + $decoded .= chr(ord($char)^ord($decode)); + $key = $decode.$key; + } + + return $decoded; +} + +sub +tradfri_Attr($$$) +{ + my ($cmd, $name, $attrName, $attrVal) = @_; + + my $orig = $attrVal; + + my $hash = $defs{$name}; + if( $attrName eq 'disable' ) { + my $hash = $defs{$name}; + if( $cmd eq "set" && $attrVal ne "0" ) { + $attrVal = 1; + CoProcess::stop($hash); + + } else { + $attr{$name}{$attrName} = 0; + CoProcess::start($hash); + + } + + } elsif( $attrName eq 'disabledForIntervals' ) { + $attr{$name}{$attrName} = $attrVal; + + CoProcess::start($hash); + + } elsif( $attrName eq 'tradfriFHEM-params' ) { + $attr{$name}{$attrName} = $attrVal; + + CoProcess::start($hash); + + } elsif( $attrName eq 'tradfriFHEM-sshHost' ) { + $attr{$name}{$attrName} = $attrVal; + + CoProcess::start($hash); + + } elsif( $attrName eq 'tradfriFHEM-sshUser' ) { + $attr{$name}{$attrName} = $attrVal; + + CoProcess::start($hash); + + } elsif( $attrName eq 'tradfriFHEM-securityCode' ) { + if( $cmd eq "set" && $attrVal ) { + $attrVal = tradfri_encrypt($attrVal); + } + $attr{$name}{$attrName} = $attrVal; + + CoProcess::start($hash); + + if( $cmd eq "set" && $orig ne $attrVal ) { + $attr{$name}{$attrName} = $attrVal; + return "stored obfuscated security code"; + } + + } + + + if( $cmd eq 'set' ) { + if( $orig ne $attrVal ) { + $attr{$name}{$attrName} = $attrVal; + return "stored modified value"; + } + + } else { + delete $attr{$name}{$attrName}; + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday(), "tradfri_AttrDefaults", $hash, 0); + } + + return; +} + + +1; + +=pod +=item summary Module to control the FHEM/tradfri integration +=item summary_DE Modul zur Konfiguration der FHEM/tradfri Integration +=begin html + + +

tradfri

+
    + Module to control the integration of IKEA tradfri devices with FHEM.

    + + Notes: +
      +
    • JSON has to be installed on the FHEM host.
    • +
    + + + Set +
      +
    • scene <name|id>
      +
    • +
    + + + Get +
      +
    • scenes
      +
    • +
    + + + Attr +
      +
    • tradfriFHEM-securityCode
      + the security code on the back of the gateway
    • +
    • tradfriFHEM-cmd
      + The command to use as tradfri-fhem
    • +
    • tradfriFHEM-params
      + Additional tradfri-fhem cmdline params.
    • +
    +

+ +=end html +=cut diff --git a/fhem/FHEM/CoProcess.pm b/fhem/FHEM/CoProcess.pm new file mode 100644 index 000000000..890d1c905 --- /dev/null +++ b/fhem/FHEM/CoProcess.pm @@ -0,0 +1,416 @@ + +# $Id$ + +use strict; + +use vars qw(%cmds); +use vars qw(%defs); +use vars qw($init_done); +use vars qw(%selectlist); + +sub CoProcess::Info($$@); + +my %hash = ( + Fn => "CoProcess::Info", + Hlp => ",show info about processes started by CoProcess" +); +$cmds{coprocessinfo} = \%hash; + +sub +CoProcess_Initialize() { +} + + +package CoProcess; + +use POSIX; +use Socket; + +sub +Info($$@) { + my @ret; + + foreach my $d (keys %main::defs) { + my $h =$main::defs{$d}; + next if( !defined($h->{CoProcess}) ); + + my $line; + + $line = sprintf( "%-15s %-15s %-35s %-19s %-19s %8s %s", $h->{NAME}, $h->{CoProcess}{name}, $h->{CoProcess}{state}, $h->{LAST_START}, $h->{LAST_STOP}, $h->{PID}, $h->{logfile} ); + + push @ret, $line; + } + + unshift @ret, sprintf( "\n%-15s %-15s %-35s %-19s %-19s %8s %s", 'DEVICE', 'NAME', 'state', 'LAST START', 'LAST STOP', 'PID', 'logfile' ) if( @ret ); + push @ret, "No CoProcesses are currently used" if(!@ret); + + return join("\n", @ret) ."\n"; +} + + +sub +callFn($$) { + my ($hash,$n,@params) = @_; + my $name = $hash->{NAME}; + + if( !defined($hash->{CoProcess}) || !defined($hash->{CoProcess}{$n}) ) { + main::Log3 $name, 4, "$name: CoProcess: no such function: $n"; + return undef; + } + + my $fn = "main::$hash->{CoProcess}{$n}"; + if(wantarray) { + no strict "refs"; + my @ret = &{$fn}($hash, @params); + use strict "refs"; + return @ret; + } else { + no strict "refs"; + my $ret = &{$fn}($hash, @params); + use strict "refs"; + return $ret; + } +} +sub +openLogfile($;$) { + my ($hash,$logfile) = @_; + my $name = $hash->{NAME}; + + closeLogfile($hash) if( $hash->{log} ); + + if( !$logfile ) { + $logfile = $hash->{logfile}; + + if( !$logfile ) { + $logfile = main::AttrVal($name, 'CoProc-log', undef); + $hash->{logfile} = $logfile if( $logfile ); + } + + if( $logfile && $logfile ne 'FHEM' ) { + $hash->{logfile} = $logfile; + my @t = localtime(time()); + $logfile = main::ResolveDateWildcards($logfile, @t); + } + } + + if( $logfile && $logfile ne 'FHEM' ) { + $hash->{currentlogfile} = $logfile; + + main::HandleArchiving($hash); + + if( open( my $fh, ">>$logfile") ) { + $fh->autoflush(1); + + $hash->{log} = $fh; + + main::Log3 $name, 3, "$name: using logfile: $logfile"; + + } else { + main::Log3 $name, 2, "$name: failed to open logile: $logfile: $!"; + } + } + main::Log3 $name, 3, "$name: using FHEM logfile" if( !$hash->{log} ); +} +sub +closeLogfile($) { + my ($hash) = @_; + + close($hash->{log}) if( $hash->{log} ); + delete $hash->{log}; + + delete $hash->{currentlogfile}; +} + + +# start co process +sub +start($;$) { + my ($hash,$cmd) = @_; + my $name = $hash->{NAME}; + my $error; + ($cmd,$error) = callFn($hash,'cmdFn') if( !$cmd ); + + if( $error ) { + $hash->{CoProcess}{state} = "stopped; $error"; + main::readingsSingleUpdate($hash, $hash->{CoProcess}{name}, $hash->{CoProcess}{state}, 1 ) if( $hash->{CoProcess}{name} ); + main::Log3 $name, 2, "$name: $error"; + } + + return undef if( !$cmd ); + return undef if( !$main::init_done ); + return undef if( !$hash->{CoProcess} ); + return undef if( main::IsDisabled($name) ); + + if( $hash->{PID} ) { + $hash->{restart} = 1; + stop($hash); + return undef; + } + delete $hash->{restart}; + + my ($child, $parent); + # SOCK_NONBLOCK ? + if( socketpair($child, $parent, AF_UNIX, SOCK_STREAM, PF_UNSPEC) ) { + $child->autoflush(1); + $parent->autoflush(1); + + my $pid = main::fhemFork(); + + if( !defined($pid) ) { + close $parent; + close $child; + + main::Log3 $name, 1, "$name: Cannot fork: $!"; + return; + } + + if( $pid ) { + close $parent; + + $hash->{STARTS}++; + + $hash->{FH} = $child; + $hash->{FD} = fileno($child); + $hash->{PID} = $pid; + + $main::selectlist{$name} = $hash; + + main::Log3 $name, 3, "$name: starting"; + $hash->{LAST_START} = main::FmtDateTime( time() ); + $cmd = (split( ' ', $cmd, 2 ))[0]; + $hash->{CoProcess}{state} = "running $cmd"; + main::readingsSingleUpdate($hash, $hash->{CoProcess}{name}, $hash->{CoProcess}{state}, 1 ) if( $hash->{CoProcess}{name} ); + + openLogfile($hash); + + } else { + close $child; + + close STDIN; + close STDOUT; + close STDERR; + + my $fn = $parent->fileno(); + open(STDIN, "<&$fn") or die "can't redirect STDIN $!"; + open(STDOUT, ">&$fn") or die "can't redirect STDOUT $!"; + open(STDERR, ">&$fn") or die "can't redirect STDERR $!"; + + STDOUT->autoflush(1); + STDERR->autoflush(1); + + close $parent; + + exec split( ' ', $cmd ) or main::Log3 $name, 1, "exec failed"; + + main::Log3 $name, 1, "set the alexaFHEM-cmd attribut to: /alexa-fhem"; + + POSIX::_exit(0);; + } + + } else { + main::Log3 $name, 3, "$name: socketpair failed"; + main::InternalTimer(time()+20, "CoProcess::start", $hash, 0); + } +} + +# stop coprocess +sub +stop($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + main::RemoveInternalTimer($hash); + + return undef if( !$hash->{PID} ); + + if( $hash->{PID} ) { + kill( SIGTERM, $hash->{PID} ); + #kill( SIGkILL, $hash->{PID} ); + # waitpid($hash->{PID}, 0); + # delete $hash->{PID}; + } + + $hash->{CoProcess}{state} = 'stopping'; + main::readingsSingleUpdate($hash, $hash->{CoProcess}{name}, $hash->{CoProcess}{state}, 1 ) if( $hash->{CoProcess}{name} ); + + main::InternalTimer(time()+5, "CoProcess::terminate", $hash, 0); +} + +# kill co process imediately +sub +terminate($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + main::RemoveInternalTimer($hash); + + return undef if( !$hash->{PID} ); + return undef if( !$hash->{FD} ); + + kill( SIGKILL, $hash->{PID} ); + waitpid($hash->{PID}, 0); + delete $hash->{PID}; + + close($hash->{FH}) if($hash->{FH}); + delete($hash->{FH}); + delete($hash->{FD}); + delete($main::selectlist{$name}); + + closeLogfile($hash) if( $hash->{log} ); + + $hash->{PARTIAL} = "" if( defined($hash->{PARTIAL}) ); + + main::Log3 $name, 3, "$name: stopped"; + $hash->{LAST_STOP} = main::FmtDateTime( time() ); + + $hash->{CoProcess}{state} = 'stopped'; + if( $hash->{reason} ) { + $hash->{CoProcess}{state} .= "; $hash->{reason}"; + delete $hash->{reason}; + } + main::readingsSingleUpdate($hash, $hash->{CoProcess}{name}, $hash->{CoProcess}{state}, 1 ) if( $hash->{CoProcess}{name} ); + + if( $hash->{undefine} ) { + my $cl = $hash->{undefine}; + + delete $hash->{undefine}; + main::CommandDelete(undef, $name); + main::Log3 $name, 2, "$name: deleted"; + + main::asyncOutput( $cl, "$name: deleted\n" ) if( ref($cl) eq 'HASH' && $cl->{canAsyncOutput} ); + + } elsif( $hash->{shutdown} ) { + my $cl = $hash->{shutdown}; + + delete $hash->{shutdown}; + main::asyncOutput( $cl, "$name: stopped\n" ) if( ref($cl) eq 'HASH' && $cl->{canAsyncOutput} ); + main::CancelDelayedShutdown($name); + + } elsif( $hash->{restart} ) { + start($hash) + + } +} + +#read from co process and handle logging +sub +readFn($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $buf; + my $ret = sysread($hash->{FH}, $buf, 65536 ); + + if(!defined($ret) || $ret <= 0) { + main::Log3 $name, 3, "$name: read: error during sysread: $!" if(!defined($ret)); + main::Log3 $name, 3, "$name: read: end of file reached while sysread" if( $ret <= 0); + + my $oldstate = $hash->{CoProcess}{state}; + + terminate($hash); + + return undef if( $oldstate !~ m/^running/ ); + + my $delay = 20; + if( $hash->{'LAST_START'} && $hash->{'LAST_STOP'} ) { + my $diff = main::time_str2num($hash->{'LAST_STOP'}) - main::time_str2num($hash->{'LAST_START'}); + + if( $diff > 60 ) { + $delay = 0; + main::Log3 $name, 4, "$name: last run duration was $diff sec, restarting imediately"; + } else { + main::Log3 $name, 4, "$name: last run duration was only $diff sec, restarting with delay"; + } + } else { + main::Log3 $name, 4, "$name: last run duration unknown, restarting with delay"; + } + main::InternalTimer(time()+$delay, "CoProcess::start", $hash, 0); + + return undef; + } + + if( $hash->{logfile} ) { + if( $hash->{log} ) { + my @t = localtime(time()); + my $logfile = main::ResolveDateWildcards($hash->{logfile}, @t); + openLogfile($hash, $logfile) if( $hash->{currentlogfile} ne $logfile ); + } + + if( $hash->{log} ) { + print {$hash->{log}} "$buf"; + + } else { + #my $buf = $buf; + $buf =~ s/\n$//s; + main::Log3 $name, 3, "$name: $buf"; + + } + } + + if( my $disabled = main::IsDisabled($hash) && !$hash->{CoProcess}{state} eq 'stopping' ) { + $hash->{reason} = 'disabledForIntervals'; + CoProcess::stop($hash); + + if( $disabled == 2 ) { + $hash->{disabled} = 1; + #FIXME: add timer to restart coprocess if disabledForIntervals has elapsed + } + } + + return $buf; +} + +# add CoProcess specific commands +sub +setCommands($$@) { + my ($hash, $list, $cmd, @a) = @_; + + #my %cp_list = ( + # 'start' => ':noArg', + # 'stop' => ':noArg', + # 'restart' => ':noArg', + #); + + + if( $cmd eq 'start' ) { + start($hash); + + return undef; + + } elsif( $cmd eq 'stop' ) { + stop($hash); + + return undef; + + } elsif( $cmd eq 'restart' ) { + start($hash); + + return undef; + } + + + $list .= ' ' if( $list ); + $list .= 'start:noArg stop:noArg restart:noArg'; + #foreach my $key ( sort keys %cp_list ) { + # if( $list !~ m/\b$key\b/ ) { + # $list .= ' ' if( $list ); + # $list .= $key.$cp_list{$key}; + # } + #} + + return "Unknown argument $cmd, choose one of $list"; +} + +# add CoProcess specific attributes +use vars qw($CoProcessAttributes); +#no warnings 'qw'; +my @attrList = qw( + CoProc-cmd + CoProc-log + CoProc-params + CoProc-sshHost + CoProc-sshUser +); +$CoProcessAttributes = join(" ", @attrList); + +#TODO... diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index c303b1050..8854f708b 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -152,8 +152,8 @@ FHEM/24_TPLinkHS110.pm VolkerKettenbach Sonstige Systeme FHEM/26_tahoma.pm mike3436 Sonstige Systeme FHEM/30_DUOFERN telekatz Sonstige Systeme FHEM/30_ENECSYSGW.pm akw Sonstige Systeme -FHEM/30_HUEBridge.pm justme1968 Beleuchtung -FHEM/30_LIGHTIFY.pm justme1968 Beleuchtung +FHEM/30_HUEBridge.pm justme1968 Zigbee +FHEM/30_LIGHTIFY.pm justme1968 Zigbee FHEM/30_MilightBridge.pm mattwire Beleuchtung FHEM/30_pilight_contact.pm risiko Sonstige Systeme FHEM/30_pilight_dimmer.pm risiko Sonstige Systeme @@ -161,9 +161,10 @@ FHEM/30_pilight_raw.pm risiko Sonstige Systeme FHEM/30_pilight_smoke.pm risiko Sonstige Systeme FHEM/30_pilight_switch.pm risiko Sonstige Systeme FHEM/30_pilight_temp.pm risiko Sonstige Systeme +FHEM/30_tradfri.pm justme1968 Zigbee FHEM/31_Aurora.pm justme1968 Beleuchtung FHEM/31_ENECSYSINV.pm akw Sonstige Systeme -FHEM/31_HUEDevice.pm justme1968 Beleuchtung +FHEM/31_HUEDevice.pm justme1968 Zigbee FHEM/31_LightScene.pm justme1968 Automatisierung FHEM/31_MilightDevice.pm mattwire Beleuchtung FHEM/31_Nello.pm neumann Sonstige Systeme @@ -521,6 +522,8 @@ FHEM/Blocking.pm rudolfkoenig Automatisierung FHEM/Color.pm justme1968 Sonstiges FHEM/DarkSkyAPI.pm CoolTux Unterstuetzende Dienste/Wettermodule FHEM/DevIo.pm rudolfkoenig Sonstiges +FHEM/CoProcess.pm justme1968 Sonstiges +FHEM/Color.pm justme1968 Sonstiges FHEM/FritzBoxUtils.pm rudolfkoenig FRITZ!Box FHEM/GPUtils.pm ntruchsess FHEM Development FHEM/HMCCUConf.pm zap HomeMatic