2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-04-09 01:34:19 +00:00
fhem-mirror/fhem/FHEM/66_ECMD.pm

594 lines
16 KiB
Perl

#
#
# 66_ECMD.pm
# written by Dr. Boris Neubert 2011-01-15
# e-mail: omega at online dot de
#
##############################################
package main;
use strict;
use warnings;
use Time::HiRes qw(gettimeofday);
#sub ECMD_Attr(@);
sub ECMD_Clear($);
#sub ECMD_Parse($$$$$);
#sub ECMD_Read($);
sub ECMD_ReadAnswer($$);
#sub ECMD_Ready($);
sub ECMD_Write($$);
sub ECMD_OpenDev($$);
sub ECMD_CloseDev($);
sub ECMD_SimpleWrite(@);
sub ECMD_SimpleRead($);
sub ECMD_Disconnected($);
use vars qw {%attr %defs};
#####################################
sub
ECMD_Initialize($)
{
my ($hash) = @_;
# Provider
$hash->{WriteFn} = "ECMD_Write";
#$hash->{ReadFn} = "ECMD_Read";
$hash->{Clients}= ":ECMDDevice:";
# Consumer
$hash->{DefFn} = "ECMD_Define";
$hash->{UndefFn} = "ECMD_Undef";
$hash->{GetFn} = "ECMD_Get";
$hash->{SetFn} = "ECMD_Set";
$hash->{AttrFn} = "ECMD_Attr";
$hash->{AttrList}= "classdefs loglevel:0,1,2,3,4,5";
}
#####################################
sub
ECMD_Define($$)
{
my ($hash, $def) = @_;
my @a = split("[ \t]+", $def);
my $name = $a[0];
my $protocol = $a[2];
if(@a < 4 || @a > 4 || (($protocol ne "telnet") && ($protocol ne "serial"))) {
my $msg = "wrong syntax: define <name> ECMD telnet <ipaddress[:port]> or define <name> ECMD serial <devicename[\@baudrate]>";
Log 2, $msg;
return $msg;
}
ECMD_CloseDev($hash);
$hash->{Protocol}= $protocol;
my $devicename= $a[3];
$hash->{DeviceName} = $devicename;
my $ret = ECMD_OpenDev($hash, 0);
return $ret;
}
#####################################
sub
ECMD_Undef($$)
{
my ($hash, $arg) = @_;
my $name = $hash->{NAME};
foreach my $d (sort keys %defs) {
if(defined($defs{$d}) &&
defined($defs{$d}{IODev}) &&
$defs{$d}{IODev} == $hash)
{
my $lev = ($reread_active ? 4 : 2);
Log GetLogLevel($name,$lev), "deleting port for $d";
delete $defs{$d}{IODev};
}
}
ECMD_CloseDev($hash);
return undef;
}
#####################################
sub
ECMD_CloseDev($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $dev = $hash->{DeviceName};
return if(!$dev);
if($hash->{TCPDev}) {
$hash->{TCPDev}->close();
delete($hash->{TCPDev});
} elsif($hash->{USBDev}) {
$hash->{USBDev}->close() ;
delete($hash->{USBDev});
}
($dev, undef) = split("@", $dev); # Remove the baudrate
delete($selectlist{"$name.$dev"});
delete($readyfnlist{"$name.$dev"});
delete($hash->{FD});
}
########################
sub
ECMD_OpenDev($$)
{
my ($hash, $reopen) = @_;
my $protocol = $hash->{Protocol};
my $name = $hash->{NAME};
my $devicename = $hash->{DeviceName};
$hash->{PARTIAL} = "";
Log 3, "ECMD opening $name (protocol $protocol, device $devicename)"
if(!$reopen);
if($hash->{Protocol} eq "telnet") {
# This part is called every time the timeout (5sec) is expired _OR_
# somebody is communicating over another TCP connection. As the connect
# for non-existent devices has a delay of 3 sec, we are sitting all the
# time in this connect. NEXT_OPEN tries to avoid this problem.
if($hash->{NEXT_OPEN} && time() < $hash->{NEXT_OPEN}) {
return;
}
my $conn = IO::Socket::INET->new(PeerAddr => $devicename);
if($conn) {
delete($hash->{NEXT_OPEN})
} else {
Log(3, "Can't connect to $devicename: $!") if(!$reopen);
$readyfnlist{"$name.$devicename"} = $hash;
$hash->{STATE} = "disconnected";
$hash->{NEXT_OPEN} = time()+60;
return "";
}
$hash->{TCPDev} = $conn;
$hash->{FD} = $conn->fileno();
delete($readyfnlist{"$name.$devicename"});
$selectlist{"$name.$devicename"} = $hash;
} else {
my $baudrate;
($devicename, $baudrate) = split("@", $devicename);
my $po;
if ($^O=~/Win/) {
require Win32::SerialPort;
$po = new Win32::SerialPort ($devicename);
} else {
require Device::SerialPort;
$po = new Device::SerialPort ($devicename);
}
if(!$po) {
return undef if($reopen);
Log(3, "Can't open $devicename: $!");
$readyfnlist{"$name.$devicename"} = $hash;
$hash->{STATE} = "disconnected";
return "";
}
$hash->{USBDev} = $po;
if( $^O =~ /Win/ ) {
$readyfnlist{"$name.$devicename"} = $hash;
} else {
$hash->{FD} = $po->FILENO;
delete($readyfnlist{"$name.$devicename"});
$selectlist{"$name.$devicename"} = $hash;
}
if($baudrate) {
$po->reset_error();
Log 3, "CUL setting $name baudrate to $baudrate";
$po->baudrate($baudrate);
$po->databits(8);
$po->parity('none');
$po->stopbits(1);
$po->handshake('none');
# This part is for some Linux kernel versions whih has strange default
# settings. Device::SerialPort is nice: if the flag is not defined for your
# OS then it will be ignored.
$po->stty_icanon(0);
#$po->stty_parmrk(0); # The debian standard install does not have it
$po->stty_icrnl(0);
$po->stty_echoe(0);
$po->stty_echok(0);
$po->stty_echoctl(0);
# Needed for some strange distros
$po->stty_echo(0);
$po->stty_icanon(0);
$po->stty_isig(0);
$po->stty_opost(0);
$po->stty_icrnl(0);
}
$po->write_settings;
}
if($reopen) {
Log 1, "ECMD $name ($devicename) reappeared";
} else {
Log 3, "ECMD device opened";
}
$hash->{STATE}= ""; # Allow InitDev to set the state
my $ret = ECMD_DoInit($hash);
if($ret) {
Log 1, "$ret";
ECMD_CloseDev($hash);
Log 1, "Cannot init $name ($devicename), ignoring it";
}
DoTrigger($name, "CONNECTED") if($reopen);
return $ret;
}
#####################################
sub
ECMD_DoInit($)
{
my $hash = shift;
my $name = $hash->{NAME};
my $msg = undef;
ECMD_Clear($hash);
ECMD_SimpleWrite($hash, "version");
my ($err,$version)= ECMD_ReadAnswer($hash, "version");
return "$name: $err" if($err);
Log 2, "ECMD version: $version";
$hash->{VERSION} = $version;
#ECMD_SimpleWrite($hash, $hash->{initString});
$hash->{STATE} = "Initialized" if(!$hash->{STATE});
return undef;
}
########################
sub
ECMD_SimpleWrite(@)
{
my ($hash, $msg, $nonl) = @_;
return if(!$hash);
$msg .= "\n" unless($nonl);
$hash->{USBDev}->write($msg) if($hash->{USBDev});
syswrite($hash->{TCPDev}, $msg) if($hash->{TCPDev});
select(undef, undef, undef, 0.001);
}
########################
sub
ECMD_SimpleRead($)
{
my ($hash) = @_;
if($hash->{USBDev}) {
return $hash->{USBDev}->input();
}
if($hash->{TCPDev}) {
my $buf;
if(!defined(sysread($hash->{TCPDev}, $buf, 1024))) {
ECMD_Disconnected($hash);
return undef;
}
return $buf;
}
return undef;
}
#####################################
# This is a direct read for commands like get
sub
ECMD_ReadAnswer($$)
{
my ($hash, $arg) = @_;
#Log 5, "ECMD reading answer for get $arg...";
return ("No FD", undef)
if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD})));
my ($data, $rin) = ("", '');
my $buf;
my $to = 3; # 3 seconds timeout
$to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less
#Log 5, "Timeout is $to seconds";
for(;;) {
return ("Device lost when reading answer for get $arg", undef)
if(!$hash->{FD});
vec($rin, $hash->{FD}, 1) = 1;
my $nfound = select($rin, undef, undef, $to);
if($nfound < 0) {
next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
my $err = $!;
ECMD_Disconnected($hash);
return("Error reading answer for get $arg: $err", undef);
}
return ("Timeout reading answer for get $arg", undef)
if($nfound == 0);
$buf = ECMD_SimpleRead($hash);
return ("No data", undef) if(!defined($buf));
if($buf) {
chomp $buf; # remove line break
Log 5, "ECMD (ReadAnswer): $buf";
$data .= $buf;
}
return (undef, $data)
}
}
#####################################
sub
ECMD_SetState($$$$)
{
my ($hash, $tim, $vt, $val) = @_;
return undef;
}
#####################################
sub
ECMD_Clear($)
{
my $hash = shift;
# Clear the pipe
$hash->{RA_Timeout} = 0.1;
for(;;) {
my ($err, undef) = ECMD_ReadAnswer($hash, "clear");
last if($err && $err =~ m/^Timeout/);
}
delete($hash->{RA_Timeout});
}
#####################################
sub
ECMD_Disconnected($)
{
my $hash = shift;
my $dev = $hash->{DeviceName};
my $name = $hash->{NAME};
return if(!defined($hash->{FD})); # Already deleted o
Log 1, "$dev disconnected, waiting to reappear";
ECMD_CloseDev($hash);
$readyfnlist{"$name.$dev"} = $hash; # Start polling
$hash->{STATE} = "disconnected";
# Without the following sleep the open of the device causes a SIGSEGV,
# and following opens block infinitely. Only a reboot helps.
sleep(5);
DoTrigger($name, "DISCONNECTED");
}
#####################################
sub
ECMD_Get($@)
{
my ($hash, @a) = @_;
return "get needs at least one parameter" if(@a < 2);
my $name = $a[0];
my $cmd= $a[1];
my $arg = ($a[2] ? $a[2] : "");
my @args= @a; shift @args; shift @args;
my ($msg, $err);
return "No get $cmd for dummies" if(IsDummy($name));
if($cmd eq "raw") {
return "get raw needs an argument" if(@a< 3);
my $ecmd= join " ", @args;
Log 5, $ecmd;
ECMD_SimpleWrite($hash, $ecmd);
($err, $msg) = ECMD_ReadAnswer($hash, "raw");
return $err if($err);
} else {
return "get $cmd: unknown command ";
}
$hash->{READINGS}{$cmd}{VAL} = $msg;
$hash->{READINGS}{$cmd}{TIME} = TimeNow();
return "$name $cmd => $msg";
}
#####################################
sub
ECMD_EvalClassDef($$$)
{
my ($hash, $classname, $filename)=@_;
my $name= $hash->{NAME};
# refuse overwriting existing definitions
if(defined($hash->{fhem}{classDefs}{$classname})) {
my $err= "$name: class $classname is already defined.";
Log 1, $err;
return $err;
}
# try and open the class definition file
if(!open(CLASSDEF, $filename)) {
my $err= "$name: cannot open file $filename for class $classname.";
Log 1, $err;
return $err;
}
my @classdef= <CLASSDEF>;
close(CLASSDEF);
# add the class definition
Log 5, "$name: adding new class $classname from file $filename";
$hash->{fhem}{classDefs}{$classname}{filename}= $filename;
# format of the class definition:
# params <params> parameters for device definition
# get <cmdname> cmd {<perlexpression>} defines a get command
# get <cmdname> params <params> parameters for get command
# set <cmdname> cmd {<perlexpression>} defines a set command
# set <cmdname> params <params> parameters for get command
# all lines are optional
#
# eaxmple class definition 1:
# get adc cmd {"adc get %channel"}
# get adc params channel
#
# eaxmple class definition 1:
# params btnup btnstop btndown
# set up cmd {"io set ddr 2 ff\nio set port 2 1%btnup\nwait 1000\nio set port 2 00"}
# set stop cmd {"io set ddr 2 ff\nio set port 2 1%btnstop\nwait 1000\nio set port 2 00"}
# set down cmd {"io set ddr 2 ff\nio set port 2 1%btndown\nwait 1000\nio set port 2 00"}
foreach my $line (@classdef) {
# kill trailing newline
chomp $line;
# kill comments and blank lines
$line=~ s/\#.*$//;
$line=~ s/\s+$//;
next unless($line);
Log 5, "$name: evaluating >$line<";
# split line into command and definition
my ($cmd, $def)= split("[ \t]+", $line, 2);
if($cmd eq "params") {
Log 5, "$name: parameters are $def";
$hash->{fhem}{classDefs}{$classname}{params}= $def;
} elsif($cmd eq "set" || $cmd eq "get") {
my ($cmdname, $spec, $arg)= split("[ \t]+", $def, 3);
if($spec eq "params") {
if($cmd eq "set") {
Log 5, "$name: set $cmdname has parameters $arg";
$hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{params}= $arg;
} elsif($cmd eq "get") {
Log 5, "$name: get $cmdname has parameters $arg";
$hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{params}= $arg;
}
} elsif($spec eq "cmd") {
if($arg !~ m/^{.*}$/s) {
Log 1, "$name: command for $cmd $cmdname is not a perl command.";
next;
}
$arg =~ s/^(\\\n|[ \t])*//; # Strip space or \\n at the begginning
$arg =~ s/[ \t]*$//;
if($cmd eq "set") {
Log 5, "$name: set $cmdname defined as $arg";
$hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{cmd}= $arg;
} elsif($cmd eq "get") {
Log 5, "$name: get $cmdname defined as $arg";
$hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{cmd}= $arg;
}
}
} else {
Log 1, "$name: illegal tag $cmd for class $classname in file $filename.";
}
}
# store class definitions in attribute
$attr{$name}{classdefs}= "";
my @a;
foreach my $c (keys %{$hash->{fhem}{classDefs}}) {
push @a, "$c=$hash->{fhem}{classDefs}{$c}{filename}";
}
$attr{$name}{"classdefs"}= join(":", @a);
return undef;
}
#####################################
sub
ECMD_Attr($@)
{
my @a = @_;
my $hash= $defs{$a[1]};
if($a[0] eq "set" && $a[2] eq "classdefs") {
my @classdefs= split(/:/,$a[3]);
delete $hash->{fhem}{classDefs};
foreach my $classdef (@classdefs) {
my ($classname,$filename)= split(/=/,$classdef,2);
ECMD_EvalClassDef($hash, $classname, $filename);
}
}
return undef;
}
#####################################
sub
ECMD_Set($@)
{
my ($hash, @a) = @_;
my $name = $a[0];
# usage check
my $usage= "Usage: set $name classdef <classname> <filename> ";
return $usage if(@a != 4);
return $usage if($a[1] ne "classdef");
# from the definition
my $classname= $a[2];
my $filename= $a[3];
return ECMD_EvalClassDef($hash, $classname, $filename);
}
#####################################
sub
ECMD_Write($$)
{
my ($hash,$msg) = @_;
my $answer;
my @r;
my @ecmds= split "\n", $msg;
foreach my $ecmd (@ecmds) {
Log 5, "$hash->{NAME} sending $ecmd";
ECMD_SimpleWrite($hash, $ecmd);
$answer= ECMD_ReadAnswer($hash, "'ecmd");
push @r, $answer;
Log 5, $answer;
}
return join(";", @r);
}
#####################################
1;