################################################################ # # Copyright notice # # (c) 2009 Copyright: Kai 'wusel' Siering (wusel+fhem at uu dot org) # All rights reserved # # This code is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # This copyright notice MUST APPEAR in all copies of the script! ############################################### package main; ########################### # 70_WS3600.pm # Modul for FHEM # # Contributed by Kai 'wusel' Siering in 2009/2010 # Based in part on work for FHEM by other authors ... # $Id: 70_WS3600.pm,v 1.3 2010-01-30 16:25:37 painseeker Exp $ ########################### use strict; use warnings; #use Device::SerialPort; my %sets = ( "cmd" => "", "freq" => "", ); my %TranslatedCodes = ( "Date" => "Date", "Time" => "Time", "Ti" => "Temp-inside", "Timin" => "Temp-inside-min", "Timax" => "Temp-inside-max", "TTimin" => "Temp-inside-min-Time", "DTimin" => "Temp-inside-min-Date", "TTimax" => "Temp-inside-max-Time", "DTimax" => "Temp-inside-max-Date", "To" => "Temp-outside", "Tomin" => "Temp-outside-min", "Tomax" => "Temp-outside-max", "TTomin" => "Temp-outside-min-Time", "DTomin" => "Temp-outside-min-Date", "TTomax" => "Temp-outside-max-Time", "DTomax" => "Temp-outside-max-Date", "DP" => "Dew-Point", "DPmin" => "Dew-Point-min", "DPmax" => "Dew-Point-max", "TDPmin" => "Dew-Point-min-Time", "DDPmin" => "Dew-Point-min-Date", "TDPmax" => "Dew-Point-min-Time", "DDPmax" => "Dew-Point-min-Date", "RHi" => "rel-Humidity-inside", "RHimin" => "rel-Humidity-inside-min", "RHimax" => "rel-Humidity-inside-max", "TRHimin" => "rel-Humidity-inside-min-Time", "DRHimin" => "rel-Humidity-inside-min-Date", "TRHimax" => "rel-Humidity-inside-max-Time", "DRHimax" => "rel-Humidity-inside-max-Date", "RHo" => "rel-Humidity-outside", "RHomin" => "rel-Humidity-outside-min", "RHomax" => "rel-Humidity-outside-max", "TRHomin" => "rel-Humidity-outside-min-Time", "DRHomin" => "rel-Humidity-outside-min-Date", "TRHomax" => "rel-Humidity-outside-max-Time", "DRHomax" => "rel-Humidity-outside-max-Date", "WS" => "Wind-Speed", "DIRtext" => "Wind-Direction-Text", "DIR0" => "Wind-DIR0", "DIR1" => "Wind-DIR1", "DIR2" => "Wind-DIR2", "DIR3" => "Wind-DIR3", "DIR4" => "Wind-DIR4", "DIR5" => "Wind-DIR5", "WC" => "Wind-Chill", "WCmin" => "Wind-Chill-min", "WCmax" => "Wind-Chill-max", "TWCmin" => "Wind-Chill-min-Time", "DWCmin" => "Wind-Chill-min-Date", "TWCmax" => "Wind-Chill-max-Time", "DWCmax" => "Wind-Chill-max-Date", "WSmin" => "Wind-Speed-min", "WSmax" => "Wind-Speed-max", "TWSmin" => "Wind-Speed-min-Time", "DWSmin" => "Wind-Speed-min-Date", "TWSmax" => "Wind-Speed-max-Time", "DWSmax" => "Wind-Speed-max-Date", "R1h" => "Rain-1h", "R1hmax" => "Rain-1h-hmax", "TR1hmax" => "Rain-1h-hmax-Time", "DR1hmax" => "Rain-1h-hmax-Date", "R24h" => "Rain-24h", "R24hmax" => "Rain-24-hmax", "TR24hmax" => "Rain-24h-max-Time", "DR24hmax" => "Rain-24h-max-Date", "R1w" => "Rain-1w", "R1wmax" => "Rain-1w-max", "TR1wmax" => "Rain-1w-max-Time", "DR1wmax" => "Rain-1w-max-Date", "R1m" => "Rain-1M", "R1mmax" => "Rain-1M-max", "TR1mmax" => "Rain-1M-max-Time", "DR1mmax" => "Rain-1M-max-Date", "Rtot" => "Rain-total", "TRtot" => "Rain-total-Time", "DRtot" => "Rain-total-Date", "RP" => "rel-Pressure", "AP" => "abs-Pressure", "RPmin" => "rel-Pressure-min", "RPmax" => "rel-Pressure-max", "TRPmin" => "rel-Pressure-min-Time", "DRPmin" => "rel-Pressure-min-Date", "TRPmax" => "rel-Pressure-max-Time", "DRPmax" => "rel-Pressure-max-Date", "Tendency" => "Tendency", "Forecast" => "Forecast", ); my %WantedCodesForStatus = ( "Ti" => "Ti:", "To" => "T:", "DP" => "DP:", "RHi" => "Hi:", "RHo" => "H:", "WS" => "W:", "DIRtext" => "Dir:", "WC" => "WC:", "R1h" => "R:", "RP" => "P:", "Tendency" => "Tendency:", "Forecast" => "Forecast:", ); ##################################### sub WS3600_Initialize($) { my ($hash) = @_; # Consumer $hash->{DefFn} = "WS3600_Define"; $hash->{AttrList}= "model:WS3600,WS2300 loglevel:0,1,2,3,4,5,6"; $hash->{ReadFn} = "WS3600_Read"; $hash->{UndefFn} = "WS3600_Undef"; } ##################################### sub WS3600_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "Define the /path/to/fetch3600 as a parameter" if(@a != 3); my $FH; my $dev = sprintf("%s |", $a[2]); Log 3, "WS3600 using \"$dev\" as parameter to open(); trying ..."; open($FH, $dev); if(!$FH) { return "WS3600 Can't start $dev: $!"; } local $_; while (<$FH>) { # my ($reading, $val)=split(/ /, $_); if(/^(Date) (.*)/ || /^(Time) (.*)/ || /^(Ti) (.*)/ || /^(To) (.*)/) { Log 3, "WS3600 initial read: $1 $2"; } } close($FH); Log 3, "WS3600 initial read done"; $hash->{DeviceName} = $dev; $hash->{Timer} = 64; # call every 64 seconds; normal wireless update interval # is 128 sec, on wind >10 km/h 32 sec. 64 sec should ensure # quite current data. # my $tn = TimeNow(); # $hash->{READINGS}{"freq"}{TIME} = $tn; # $hash->{READINGS}{"freq"}{VAL} = $hash->{Timer}; # $hash->{CHANGED}[0] = "freq: $hash->{Timer}"; # InternalTimer blocks if init_done is not true # my $oid = $init_done; # $init_done = 1; # WS3600_GetStatus($hash); # $init_done = $oid; Log 3, "WS3600 setting callback timer"; my $oid = $init_done; $init_done = 1; InternalTimer(gettimeofday()+ $hash->{Timer}, "WS3600_GetStatus", $hash, 1); $init_done = $oid; Log 3, "WS3600 initialized"; $hash->{STATE} = "initialized"; $hash->{TMPSTATE} = ""; return undef; } ##################################### sub WS3600_Undef($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $hash->{NAME}; if(defined($hash->{FD})) { close($hash->{FD}); delete $hash->{FD}; } delete $selectlist{"$name.pipe"}; $hash->{STATE}='undefined'; Log GetLogLevel($name,3), "$name shutdown complete"; return undef; } ##################################### sub WS3600_GetStatus($) { my ($hash) = @_; my $dnr = $hash->{DEVNR}; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; my $FH; # Call us in n seconds again. # InternalTimer(gettimeofday()+ $hash->{Timer}, "WS3600_GetStatus", $hash, 1); Log GetLogLevel($name,4), "WS3600 contacting station"; open($FH, $dev); if(!$FH) { return "WS3600 Can't start $dev: $!"; } $hash->{FD}=$FH; $selectlist{"$name.pipe"} = $hash; Log GetLogLevel($name,4), "WS3600 pipe opened"; # $hash->{STATE} = "running"; $hash->{pipeopentime} = time(); # InternalTimer(gettimeofday() + 6, "WS3600_Read", $hash, 1); return $hash->{STATE}; } ##################################### sub WS3600_Read($) { my ($hash) = @_; my $dnr = $hash->{DEVNR}; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; my $FH; my $inputline; Log GetLogLevel($name,4), "WS3600 Read entered"; if(!defined($hash->{FD})) { Log GetLogLevel($name,3), "Oops, WS3600 FD undef'd"; return undef; } if(!$hash->{FD}) { Log GetLogLevel($name,3), "Oops, WS3600 FD empty"; return undef; } $FH = $hash->{FD}; Log GetLogLevel($name,4), "WS3600 reading started"; my @lines; my $eof; my $i=0; my $tn = TimeNow(); my $StateString=$hash->{TMPSTATE}; my $HumidString=""; my $TempsString=""; my $OtherString=""; my $reading; my $readingforstatus; ($eof, @lines) = nonblockGetLines($FH); if(!defined($eof)) { Log GetLogLevel($name,4), "WS3600 FIXME: eof undefined?!"; $eof=0; } Log GetLogLevel($name,4), "WS3600 reading ended with eof==$eof"; # FIXME! Current observed behaviour is "would block", then read of only EOF. # Not sure if it's always that way; more correct would be checking # for empty $inputline or undef'd $rawreading,$val. -wusel, 2010-01-04 if($eof != 1) { foreach my $inputline ( @lines ) { $inputline =~ s/\s+$//; my ($rawreading, $val)=split(/ /, $inputline); Log GetLogLevel($name,5), "WS3600 read $inputline:$rawreading:$val"; if(defined($TranslatedCodes{$rawreading})) { # delete $defs{$name}{READINGS}{" $rawreading"}; $reading=$TranslatedCodes{$rawreading}; $defs{$name}{READINGS}{$reading}{VAL} = $val; $defs{$name}{READINGS}{$reading}{TIME} = $tn; # # -wusel, 2010-01-30: BIG CHANGE: only put into CHANGED[] what will be in # STATE as well; this is done to reduce the burden on # the notification framework (each one currently leads # to a separate notify which will in turn lead a call # of EVERY NotifyFn()) and to improve FHEMs overall # performance. # Every value is still be stored in READINGS though. # # $hash->{CHANGED}[$i++] = "$reading: $val"; if(defined($WantedCodesForStatus{$rawreading})) { $readingforstatus=$WantedCodesForStatus{$rawreading}; $StateString=sprintf("%s %s %s", $StateString, $readingforstatus, $val); $hash->{CHANGED}[$i++] = "$reading: $val"; } # if($rawreading =~ m/^(Tendency|Forecast)/) { # $hash->{CHANGED}[$i++] = "$reading: $val"; # $StateString=sprintf("%s %s: %s", $StateString, $reading, $val); # } # if($rawreading =~ m/^(Ti$|To$|WC$)/) { # $hash->{CHANGED}[$i++] = "$reading: $val"; # $TempsString=sprintf("%s %s: %s °C", $TempsString, $reading, $val); # } # if($rawreading =~ m/^(RHi$|RHo$)/) { # $hash->{CHANGED}[$i++] = "$reading: $val"; # $HumidString=sprintf("%s %s: %s %%", $HumidString, $reading, $val); # } # if($rawreading =~ m/^(R1h$|R24h$)/) { # $hash->{CHANGED}[$i++] = "$reading: $val"; # $OtherString=sprintf("%s %s: %s mm", $OtherString, $reading, $val); # } # if($rawreading =~ m/^(RP$|AP$)/) { # $hash->{CHANGED}[$i++] = "$reading: $val"; # $OtherString=sprintf("%s %s: %s hPa", $OtherString, $reading, $val); # } } } $hash->{TMPSTATE} = $StateString; } if($eof) { close($FH); delete $hash->{FD}; delete $selectlist{"$name.pipe"}; InternalTimer(gettimeofday()+ $hash->{Timer}, "WS3600_GetStatus", $hash, 1); Log GetLogLevel($name,4), "WS3600 done reading pipe"; } else { Log GetLogLevel($name,4), "WS3600 (further) reading would block"; } # $OtherString =~ s/^\s+//; # $HumidString =~ s/^\s+//; # $TempsString =~ s/^\s+//; # $StateString =~ s/^\s+//; # # $defs{$name}{READINGS}{"Humidity"}{VAL} = $HumidString; # $defs{$name}{READINGS}{"Humidity"}{TIME} = $tn; # $hash->{CHANGED}[$i++] = $HumidString; # $defs{$name}{READINGS}{"Temperatures"}{VAL} = $TempsString; # $defs{$name}{READINGS}{"Temperatures"}{TIME} = $tn; # $hash->{CHANGED}[$i++] = $TempsString; # $defs{$name}{READINGS}{"Rain/Pressure"}{VAL} = $OtherString; # $defs{$name}{READINGS}{"Rain/Pressure"}{TIME} = $tn; # $hash->{CHANGED}[$i++] = $OtherString; # $defs{$name}{READINGS}{"Forecast"}{VAL} = $StateString; # $defs{$name}{READINGS}{"Forecast"}{TIME} = $tn; # $hash->{CHANGED}[$i++] = $StateString; # -wusel, 2010-01-06: FIXME: does this logic with STATE work? # -wusel, 2010-01-30: Removed setting STATE to "reading". if($eof) { # $hash->{CHANGED}[$i++] = "Status: $StateString"; $hash->{STATE} = $hash->{TMPSTATE}; $hash->{TMPSTATE} = ""; DoTrigger($name, undef); # } else { # $hash->{STATE} = "reading"; } return $hash->{STATE}; } # From http://www.perlmonks.org/?node_id=713384 / http://davesource.com/Solutions/20080924.Perl-Non-blocking-Read-On-Pipes-Or-Files.html # # Used, hopefully, with permission ;) # # An non-blocking filehandle read that returns an array of lines read # Returns: ($eof,@lines) my %nonblockGetLines_last; sub nonblockGetLines { my ($fh,$timeout) = @_; $timeout = 0 unless defined $timeout; my $rfd = ''; $nonblockGetLines_last{$fh} = '' unless defined $nonblockGetLines_last{$fh}; vec($rfd,fileno($fh),1) = 1; return unless select($rfd, undef, undef, $timeout)>=0; # I'm not sure the following is necessary? return unless vec($rfd,fileno($fh),1); my $buf = ''; my $n = sysread($fh,$buf,1024*1024); # If we're done, make sure to send the last unfinished line return (1,$nonblockGetLines_last{$fh}) unless $n; # Prepend the last unfinished line $buf = $nonblockGetLines_last{$fh}.$buf; # And save any newly unfinished lines $nonblockGetLines_last{$fh} = (substr($buf,-1) !~ /[\r\n]/ && $buf =~ s/([^\r\n]*)$//) ? $1 : ''; $buf ? (0,split(/\n/,$buf)) : (0); } 1;