From 1323f352804f813593537d0cb6960a9c78f6b5bf Mon Sep 17 00:00:00 2001 From: rudolfkoenig <> Date: Mon, 25 Aug 2008 09:51:57 +0000 Subject: [PATCH] First CUL version, small changes git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@232 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- FHEM/00_CUL.pm | 483 ++++++++++++++++++++++++++++++++++++++++++++++++ FHEM/00_FHZ.pm | 37 ++-- FHEM/10_FS20.pm | 14 +- 3 files changed, 507 insertions(+), 27 deletions(-) create mode 100755 FHEM/00_CUL.pm diff --git a/FHEM/00_CUL.pm b/FHEM/00_CUL.pm new file mode 100755 index 000000000..14600c975 --- /dev/null +++ b/FHEM/00_CUL.pm @@ -0,0 +1,483 @@ +############################################## +# Implemented: +# - Transmit limit trigger: Fire if more then 1% airtime +# is used in the last hour +# - reconnect +# - message flow control (send one F message every 0.25 seconds) +# - repeater/filtertimeout +# - FS20 rcv +# - FS20 xmit +# - FHT rcv + +# TODO: +# - FHT xmit +# - HMS rcv +# - KS300 rcv +# - EMEM rcv +# - EMWZ rcv +# - EMGZ rcv +# - S300TH rcv + + +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); + + +sub CUL_Write($$$); +sub CUL_Read($); +sub CUL_ReadAnswer($$); +sub CUL_Ready($$); + +my %msghist; # Used when more than one CUL is attached +my $msgcount = 0; +my %gets = ( + "ccreg" => "C", + "version" => "V", +); + +sub +CUL_Initialize($) +{ + my ($hash) = @_; + +# Provider + $hash->{ReadFn} = "CUL_Read"; + $hash->{WriteFn} = "CUL_Write"; + $hash->{Clients} = ":CUL:FS20:FHT:"; + $hash->{ReadyFn} = "CUL_Ready" if ($^O eq 'MSWin32'); + +# Normal devices + $hash->{DefFn} = "CUL_Define"; + $hash->{UndefFn} = "CUL_Undef"; + $hash->{GetFn} = "CUL_Get"; + $hash->{SetFn} = "CUL_Set"; + $hash->{StateFn} = "CUL_SetState"; + $hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 filtertimeout repeater:1,0 " . + "showtime:1,0 model:CUL loglevel:0,1,2,3,4,5,6"; +} + +##################################### +sub +CUL_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + my $po; + $hash->{STATE} = "Initialized"; + + delete $hash->{PortObj}; + delete $hash->{FD}; + + my $name = $a[0]; + my $dev = $a[2]; + + $attr{$name}{savefirst} = 1; + $attr{$name}{repeater} = 1; + + if($dev eq "none") { + Log 1, "CUL device is none, commands will be echoed only"; + $attr{$name}{dummy} = 1; + return undef; + } + + Log 3, "CUL opening CUL device $dev"; + if ($^O=~/Win/) { + require Win32::SerialPort; + $po = new Win32::SerialPort ($dev); + } else { + require Device::SerialPort; + $po = new Device::SerialPort ($dev); + } + return "Can't open $dev: $!\n" if(!$po); + Log 3, "CUL opened CUL device $dev"; + + $hash->{PortObj} = $po; + $hash->{FD} = $po->FILENO if !( $^O =~ /Win/ ); + + $hash->{DeviceName} = $dev; + $hash->{PARTIAL} = ""; + return CUL_DoInit($hash); +} + +##################################### +sub +CUL_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) + { + Log GetLogLevel($name,2), "deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + $hash->{PortObj}->close() if($hash->{PortObj}); + return undef; +} + +##################################### +sub +CUL_Set($@) +{ + my ($hash, @a) = @_; + return "NYI"; +} + +##################################### +sub +CUL_Get($@) +{ + my ($hash, @a) = @_; + + return "\"get CUL\" needs at leass one parameter" if(@a < 2); + return "Unknown argument $a[1], choose one of " . join(",", sort keys %gets) + if(!defined($gets{$a[1]})); + + my $arg = ($a[2] ? $a[2] : ""); + CUL_Write($hash, $gets{$a[1]}, $arg) if(!IsDummy($hash->{NAME})); + my $msg = CUL_ReadAnswer($hash, $a[1]); + $msg =~ s/[\r\n]//g; + + $hash->{READINGS}{$a[1]}{VAL} = $msg; + $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); + + return "$a[0] $a[1] => $msg"; + return "NYI" +} + +##################################### +sub +CUL_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + return "NYI"; +} + +##################################### +sub +CUL_DoInit($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + + # Clear the pipe + $hash->{RA_Timeout} = 0.1; + for(;;) { + last if(CUL_ReadAnswer($hash, "Clear") =~ m/^Timeout/); + } + delete($hash->{RA_Timeout}); + + $hash->{PortObj}->write("V\n"); + my $ver = CUL_ReadAnswer($hash, "Version"); + if($ver !~ m/^V/) { + $attr{$name}{dummy} = 1; + $hash->{PortObj}->close(); + my $msg = "Not an CUL device, receives for V: $ver"; + Log 1, $msg; + return $msg; + } + $hash->{PortObj}->write("XFE\n"); # Enable message reporting + + # Reset the counter + delete($hash->{XMIT_TIME}); + delete($hash->{NR_CMD_LAST_H}); +} + +##################################### +# This is a direct read for commands like get +sub +CUL_ReadAnswer($$) +{ + my ($hash,$arg) = @_; + + return undef if(!$hash || !defined($hash->{FD})); + my ($mculdata, $rin) = ("", ''); + my $nfound; + for(;;) { + if($^O eq 'MSWin32') { + $nfound=CUL_Ready($hash, undef); + } else { + vec($rin, $hash->{FD}, 1) = 1; + my $to = 3; # 3 seconds timeout + $to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less + $nfound = select($rin, undef, undef, $to); + if($nfound < 0) { + next if ($! == EAGAIN() || $! == EINTR() || $! == 0); + die("Select error $nfound / $!\n"); + } + } + return "Timeout reading answer for get $arg" if($nfound == 0); + my $buf = $hash->{PortObj}->input(); + + Log 5, "CUL/RAW: $buf"; + $mculdata .= $buf; + return $mculdata if($mculdata =~ m/\r\n/); + } +} + +##################################### +# Check if the 1% limit is reached and trigger notifies +sub +CUL_XmitLimitCheck($$) +{ + my ($hash,$fn) = @_; + my $now = time(); + + if(!$hash->{XMIT_TIME}) { + $hash->{XMIT_TIME}[0] = $now; + $hash->{NR_CMD_LAST_H} = 1; + return; + } + + my $nowM1h = $now-3600; + my @b = grep { $_ > $nowM1h } @{$hash->{XMIT_TIME}}; + + if(@b > 163) { # Maximum nr of transmissions per hour (unconfirmed). + + my $me = $hash->{NAME}; + Log GetLogLevel($me,2), "CUL TRANSMIT LIMIT EXCEEDED"; + DoTrigger($me, "TRANSMIT LIMIT EXCEEDED"); + + } else { + + push(@b, $now); + + } + $hash->{XMIT_TIME} = \@b; + $hash->{NR_CMD_LAST_H} = int(@b); +} + +##################################### +sub +CUL_Write($$$) +{ + my ($hash,$fn,$msg) = @_; + + if(!$hash || !defined($hash->{PortObj})) { + Log 5, "CUL device $hash->{NAME} is not active, cannot send"; + return; + } + + ################### + # Rewrite message from FHZ -> CUL + if(length($fn) == 1) { # CUL Native + } elsif($fn eq "04" && substr($msg,0,6) eq "010101") { # FS20 + $fn = "F"; + $msg = substr($msg,6); + } else { + Log 1, "CUL cannot translate $fn $msg"; + return; + } + + + ############### + # insert value into the msghist. At the moment this only makes sense for FS20 + # devices. As the transmitted value differs from the received one, we have to + # recompute. + if($fn eq "F" || $fn eq "T") { + $msghist{$msgcount}{TIME} = gettimeofday(); + $msghist{$msgcount}{NAME} = $hash->{NAME}; + $msghist{$msgcount}{MSG} = "$fn$msg"; + $msgcount++; + } + + Log 5, "CUL sending $fn$msg"; + my $bstring = "$fn$msg\n"; + + if($fn eq "F") { + if(!$hash->{QUEUECNT}) { + + CUL_XmitLimitCheck($hash, $bstring); + $hash->{PortObj}->write($bstring); + + ############## + # Write the next buffer not earlier than 0.227 seconds (= 65.6ms + 10ms + + # 65.6ms + 10ms + 65.6ms + 10ms) + InternalTimer(gettimeofday()+0.25, "CUL_HandleWriteQueue", $hash, 1); + + } elsif($hash->{QUEUECNT} == 1) { + $hash->{QUEUE} = [ $bstring ]; + } else { + push(@{$hash->{QUEUE}}, $bstring); + } + $hash->{QUEUECNT}++; + + } else { + + $hash->{PortObj}->write($bstring); + + } + +} + +##################################### +sub +CUL_HandleWriteQueue($) +{ + my $hash = shift; + my $cnt = --$hash->{QUEUECNT}; + if($cnt > 0) { + my $bstring = shift(@{$hash->{QUEUE}}); + CUL_XmitLimitCheck($hash,$bstring); + $hash->{PortObj}->write($bstring); + InternalTimer(gettimeofday()+0.25, "CUL_HandleWriteQueue", $hash, 1); + } +} + +##################################### +sub +CUL_Read($) +{ + my ($hash) = @_; + + my $buf = $hash->{PortObj}->input(); + my $iohash = $modules{$hash->{TYPE}}; # Our (CUL) module pointer + my $name = $hash->{NAME}; + + ########### + # Lets' try again: Some drivers return len(0) on the first read... + if(defined($buf) && length($buf) == 0) { + $buf = $hash->{PortObj}->input(); + } + + if(!defined($buf) || length($buf) == 0) { + + my $devname = $hash->{DeviceName}; + Log 1, "USB device $devname disconnected, waiting to reappear"; + $hash->{PortObj}->close(); + for(;;) { + sleep(5); + if ($^O eq 'MSWin32') { + $hash->{PortObj} = new Win32::SerialPort($devname); + }else{ + $hash->{PortObj} = new Device::SerialPort($devname); + } + + if($hash->{PortObj}) { + Log 1, "USB device $devname reappeared"; + $hash->{FD} = $hash->{PortObj}->FILENO if !($^O eq 'MSWin32'); + CUL_DoInit($hash); + return; + } + } + } + + my $culdata = $hash->{PARTIAL}; + Log 5, "CUL/RAW: $culdata/$buf"; + $culdata .= $buf; + + while($culdata =~ m/\n/) { + + my $dmsg; + ($dmsg,$culdata) = split("\n", $culdata); + $dmsg =~ s/\r//; + + ############### + # check for duplicate msg from different CUL's + my $now = gettimeofday(); + my $skip; + my $meetoo = ($attr{$name}{repeater} ? 1 : 0); + + my $to = 0.3; + if(defined($attr{$name}) && defined($attr{$name}{filtertimeout})) { + $to = $attr{$name}{filtertimeout}; + } + foreach my $oidx (keys %msghist) { + if($now-$msghist{$oidx}{TIME} > $to) { + delete($msghist{$oidx}); + next; + } + if($msghist{$oidx}{MSG} eq $dmsg && + ($meetoo || $msghist{$oidx}{NAME} ne $name)) { + Log 5, "Skipping $msghist{$oidx}{MSG}"; + $skip = 1; + } + } + goto NEXTMSG if($skip); + $msghist{$msgcount}{TIME} = $now; + $msghist{$msgcount}{NAME} = $name; + $msghist{$msgcount}{MSG} = $dmsg; + $msgcount++; + +Log 1, "CUL: $dmsg"; + #Translate Message from CUL to FHZ + my $fn = substr($dmsg,0,1); + if($fn eq "F") { # FS20 + $dmsg = sprintf("81%02x04xx0101a001%s00%s", + length($dmsg)/2+5, + substr($dmsg,1,6), substr($dmsg,7)); + $dmsg = lc($dmsg); + + } elsif($fn eq "T") { # FHT + + $dmsg =~ s/([1-4]\d)79(..)$/${1}69$2/; # should be done in the FHT + + $dmsg = sprintf("81%02x04xx0909a001%s00%s", + length($dmsg)/2+5, + substr($dmsg,1,6), substr($dmsg,7)); + $dmsg = lc($dmsg); + + } else { + Log 5, "CUL: unknown message $dmsg"; + goto NEXTMSG; + } + + + my @found; + my $last_module; + foreach my $m (sort { $modules{$a}{ORDER} cmp $modules{$b}{ORDER} } + grep {defined($modules{$_}{ORDER});}keys %modules) { + next if($iohash->{Clients} !~ m/:$m:/); + + # Module is not loaded or the message is not for this module + next if(!$modules{$m}{Match} || $dmsg !~ m/$modules{$m}{Match}/i); + + no strict "refs"; + @found = &{$modules{$m}{ParseFn}}($hash,$dmsg); + use strict "refs"; + $last_module = $m; + last if(int(@found)); + } + if(!int(@found)) { + Log 1, "Unknown code $dmsg, help me!"; + goto NEXTMSG; + } + + goto NEXTMSG if($found[0] eq ""); # Special return: Do not notify + + if($found[0] =~ m/^(UNDEFINED) ([^ ]*) (.*)$/) { + my $d = $1; + $defs{$d}{NAME} = $1; + $defs{$d}{TYPE} = $last_module; + DoTrigger($d, "$2 $3"); + delete $defs{$d}; + goto NEXTMSG; + } + + foreach my $found (@found) { + DoTrigger($found, undef); + } +NEXTMSG: + } + $hash->{PARTIAL} = $culdata; +} + +##################################### +sub +CUL_Ready($$) # Windows - only +{ + my ($hash, $dev) = @_; + my $po=$hash->{PortObj}; + return undef if !$po; + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags)=$po->status; + return ($InBytes>0); +} + +1; diff --git a/FHEM/00_FHZ.pm b/FHEM/00_FHZ.pm index 57e413317..9ca57bbe4 100755 --- a/FHEM/00_FHZ.pm +++ b/FHEM/00_FHZ.pm @@ -9,9 +9,9 @@ use Time::HiRes qw(gettimeofday); sub FHZ_Write($$$); sub FHZ_Read($); sub FHZ_ReadAnswer($$); -sub FhzCrc(@); -sub CheckFhzCrc($); -sub XmitLimitCheck($$); +sub FHZ_Crc(@); +sub FHZ_CheckCrc($); +sub FHZ_XmitLimitCheck($$); my $msgstart = pack('H*', "81");# Every msg starts wit this @@ -47,7 +47,6 @@ my %codes = ( my $def; my %msghist; # Used when more than one FHZ is attached my $msgcount = 0; -my $xmit_limit = 163; # Maximum nr of transmissions per hour (unconfirmed). ##################################### # Note: we are a data provider _and_ a consumer at the same time @@ -61,16 +60,18 @@ FHZ_Initialize($) $hash->{ReadFn} = "FHZ_Read"; $hash->{WriteFn} = "FHZ_Write"; $hash->{Clients} = ":FHZ:FS20:FHT:HMS:KS300:"; + $hash->{ReadyFn} = "FHZ_Ready" if ($^O eq 'MSWin32'); # Consumer $hash->{Match} = "^81..C9..0102"; + $hash->{ParseFn} = "FHZ_Parse"; + +# Normal devices $hash->{DefFn} = "FHZ_Define"; $hash->{UndefFn} = "FHZ_Undef"; $hash->{GetFn} = "FHZ_Get"; $hash->{SetFn} = "FHZ_Set"; $hash->{StateFn} = "FHZ_SetState"; - $hash->{ParseFn} = "FHZ_Parse"; - $hash->{ReadyFn} = "FHZ_Ready" if ($^O eq 'MSWin32'); $hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 filtertimeout repeater:1,0 " . "showtime:1,0 model:fhz1000,fhz1300 loglevel:0,1,2,3,4,5,6 ". "fhtsoftbuffer:1,0"; @@ -85,6 +86,7 @@ FHZ_Ready($$) my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags)=$po->status; return ($InBytes>0); } + ##################################### sub FHZ_Set($@) @@ -134,7 +136,7 @@ FHZ_Set($@) } - FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); + FHZ_Write($hash, $fn, $arg) if(!IsDummy($hash->{NAME})); return undef; } @@ -154,7 +156,7 @@ FHZ_Get($@) my $name = $hash->{NAME}; Log GetLogLevel($name,2), "FHZ get $v"; - FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); + FHZ_Write($hash, $fn, $arg) if(!IsDummy($hash->{NAME})); my $msg = FHZ_ReadAnswer($hash, $a[1]); return $msg if(!$msg || $msg !~ /^81..c9..0102/); @@ -338,7 +340,7 @@ FHZ_Parse($$) ##################################### sub -FhzCrc(@) +FHZ_Crc(@) { my $sum = 0; map { $sum += $_; } @_; @@ -347,7 +349,7 @@ FhzCrc(@) ##################################### sub -CheckFhzCrc($) +FHZ_CheckCrc($) { my $msg = shift; return 0 if(length($msg) < 8); @@ -360,7 +362,7 @@ CheckFhzCrc($) # FS20 Repeater generate a CRC which is one or two greater then the computed # one. The FHZ1000 filters such pakets, so we do not see them - return (($crc eq FhzCrc(@data)) ? 1 : 0); + return (($crc eq FHZ_Crc(@data)) ? 1 : 0); } @@ -405,6 +407,7 @@ FHZ_ReadAnswer($$) } ############## +# Compute CRC, add header, glue fn and messages sub FHZ_CompleteMsg($$) { @@ -414,14 +417,14 @@ FHZ_CompleteMsg($$) for(my $i = 0; $i < $len; $i += 2) { push(@data, ord(pack('H*', substr($msg, $i, 2)))); } - return pack('C*', 0x81, $len/2+2, ord(pack('H*',$fn)), FhzCrc(@data), @data); + return pack('C*', 0x81, $len/2+2, ord(pack('H*',$fn)), FHZ_Crc(@data), @data); } ##################################### # Check if the 1% limit is reached and trigger notifies sub -XmitLimitCheck($$) +FHZ_XmitLimitCheck($$) { my ($hash,$bstring) = @_; my $now = time(); @@ -438,7 +441,7 @@ XmitLimitCheck($$) my $nowM1h = $now-3600; my @b = grep { $_ > $nowM1h } @{$hash->{XMIT_TIME}}; - if(@b > $xmit_limit) { + if(@b > 163) { # Maximum nr of transmissions per hour (unconfirmed). my $me = $hash->{NAME}; Log GetLogLevel($me,2), "FHZ TRANSMIT LIMIT EXCEEDED"; @@ -481,7 +484,7 @@ FHZ_Write($$$) if(!$hash->{QUEUECNT}) { - XmitLimitCheck($hash,$bstring); + FHZ_XmitLimitCheck($hash,$bstring); $hash->{PortObj}->write($bstring); ############## @@ -507,7 +510,7 @@ FHZ_HandleWriteQueue($) my $cnt = --$hash->{QUEUECNT}; if($cnt > 0) { my $bstring = shift(@{$hash->{QUEUE}}); - XmitLimitCheck($hash,$bstring); + FHZ_XmitLimitCheck($hash,$bstring); $hash->{PortObj}->write($bstring); InternalTimer(gettimeofday()+0.25, "FHZ_HandleWriteQueue", $hash, 1); } @@ -583,7 +586,7 @@ FHZ_Read($) last if(length($fhzdata) < $len); my $dmsg = unpack('H*', substr($fhzdata, 0, $len)); - if(CheckFhzCrc($dmsg)) { + if(FHZ_CheckCrc($dmsg)) { if(substr($fhzdata,2,1) eq $msgstart) { # Skip function 0x81 $fhzdata = substr($fhzdata, 2); diff --git a/FHEM/10_FS20.pm b/FHEM/10_FS20.pm index 410314eb3..734ac37be 100755 --- a/FHEM/10_FS20.pm +++ b/FHEM/10_FS20.pm @@ -177,13 +177,8 @@ FS20_Set($@) (undef, $v) = split(" ", $v, 2); # Not interested in the name... my $val; - if($na == 2) { - - IOWrite($hash, "04", "010101" . $hash->{XMIT} . $hash->{BTN} . $c) - if(!IsDummy($a[0])); - - } else { + if($na == 3) { # Timed command. $c =~ s/1/3/; # Set the extension bit ######################## @@ -202,12 +197,11 @@ FS20_Set($@) } } return "Specified timeout too large, max is 15360" if(length($c) == 2); - - IOWrite($hash, "04", "010101" . $hash->{XMIT} . $hash->{BTN} . $c) - if(!IsDummy($a[0])); - } + IOWrite($hash, "04", "010101" . $hash->{XMIT} . $hash->{BTN} . $c) + if(!IsDummy($a[0])); + ########################################### # Set the state of a device to off if on-for-timer is called if($follow{$a[0]}) {