commit 005ca11a9e66bf383e8cf74786f922efa3822b64 Author: rudolfkoenig <> Date: Tue Jan 30 12:47:36 2007 +0000 Initial version git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@3 2b470e98-0d58-463d-a4d8-8e2adae1ed80 diff --git a/CHANGED b/CHANGED new file mode 100644 index 000000000..9cebe924d --- /dev/null +++ b/CHANGED @@ -0,0 +1,301 @@ +- 2005-10-27 (1.3) + - Bugfix: multiple at commands at the same time. + +- 2005-11-10 (1.4) + - Reformatting the package and the documentation + - New links + +- 2005-12-26 (1.5) + - "modularized" in preparation for the FHZ80B -> each device has a type + - added relative "at" commands (with +HH:MM:SS) + - multiple commands on one line separated with ; + - sleeping 0.22 seconds after an ST command + - some commands/syntax changed: + - switch => set + - device => fhzdevice + - define ... => define ... + - the state of the devices and the at commands are saved + - at start always sending a "set 0001 00 01" to enable the FHZ receiever. + This is a workaround. + - doc rewrite, examples directory + +- 2006-01-03 (1.6) + - signal handling (to save the state on shutdown) + - module FHZ addded (for the FHZ1000PC device itself) + - added the get function (to make the initialization prettier) + - the module ST was renamed to FS20 + - FS20 timer commands added + - modules command removed (we are loading everything from the modpath + directory) + - FHT80b module added (yes, it is already useful, you can set + and view a lot of values) + - documentation adapted + - Added a TODO file + +- 2006-01-04 (1.7) + - the at command can be used to execute something repeatedly with * + - ntfy can filter on device or on device+event with a regexp + - checking the delete and notify regexps if they make sense + - the FHT init string is now a set command (refreshvalues) + - shutdown saves the detailed device information too + +- 2006-01-05 (1.8) + - Bugfix: detailed FS20 status was not set from external event + - Bugfix: setstate for FS20 returned the last state set + - Bugfix: undefined FS20 devices (can) crash the server + - HMS module added by Martin Mueller + (currently supporting the HMS100T & HMS100TF) + - Log modules added, the first one being a simple FileLog + (inspired by Martin Mueller) + - A little gnuplot script to display temperature and actuator changes + +- 2006-02-10 (1.9) + (aka as the Juergen release) + - The FHZ1300 is reported to work + - Bugfix: spaces before comment in the config file should be ignored + - added FS20STR codes to 10_FS20.pm + - names restricted to A-Za-z0-9.:- (especially _ is not allowed) + - delete calles now an UndefFn in the module + - implementation of FS20 function group/local master/global master + - the list command tells you the definition of the device too + +- 2006-02-12 (1.9a) + - Bugfix: wrong rights for HMS and wrong place for readonly + (thanks to Juergen) + +- 2006-02-12 (1.9b) + - Bugfix: Fixing the same bug again (thanks to Martin) + +- 2006-04-02 (2.0) + - XmlList and webfrontend/pgm1 programs from Raoul Matthiessen + - list tries to display the state and not the last command + - Both log facilities (FileLog and Log) take wildcards + (week, year, month, etc) to make logfile rotating easier + - webfrontend/pgm2 + +- 2006-04-15 (2.1) + - webfrontend/pgm2 changes: + - make it work on Asus dsl-routers (no "use warnings") + - css/readonly configurable + - Formatting for HMS data + - comments can be added to each device (setstate comment:xxx) + - testbed to dry-test functionality (test directory) + - added an empty hull for the KS300 weather module + - added undocumented "exec" function to call arbitrary program parts + for debugging. Example: exec FhzDecode("81xx04xx0101a0011234030011"); + - webfrontend/pgm3, contributed by Martin Haas + - fixed pgm1: changing values should work now + +- 2006-05-20 (2.2) + - FHZ1300 support verified (+ doc changes) + - KS300 support added (with Temperature, Humidity, Wind speed, Rain). + Not verified/undecoded: subzero temp, weak battery, Is-raining flag, + wind speed > 100km/h + - webpgm2 log fix for "offed" FHT devices (with no actuator data) + - webpgm3 upgrade (by Martin Haas, see webpgm/pgm3/docs/CHANGES for details) + - HMS logging/state format changed to make it similar to KS300 + - added HMS100WD (thanks to Sascha Pollok) + - ntfy/logging changed to be able to notify for multiple attributes + arriving in one message + - central FHTcode settable (see commandref.html) + - optionally listen for non-local requests (port global) + - unknown logging + - FAQ + +- 2006-6-22 (2.3) + - CRC checking (i.e. ignoring messages with bad CRC, message on verbose 4) + - contrib/checkmesg.pl added to check message consistency (debugging) + - FHT: unknown_aa, unknown_ba codes added. What they are for? + - Empty modpath / no modpath error messages added (some user think modpath is + superfluous) + - Unparsed messages (verbose 5) now printed as hex + - Try to reattach to the usb device if it disappears: no need to + restart the server if the device is pulled out from the USB socket and + plugged in again (old versions go into a busy loop here). + - Supressing the seldom (ca 1 out of 700) short KS300 messages. + (not sure how to interpret them) + - Added KS300 "israining" status flag. Note: this not always triggers when it + is raining, but there seems to be a correlation. To be evaluated in more + detail. + - notifyon can now execute "private" perl code as well (updated + commandref.html, added the file example/99_PRIV.pm) + - another "perl code" example is logging the data into the database + (with DBI), see the file contrib/91_DbLog.pm. Tested with an Oracle DB. + - logs added to the xmllist + - FHT80b: Fix measured-temp over 25.5 (handling the tempadd messages better) + +- 2006-07-23 (2.4) + - contrib/four2hex (to convert between ELV and our codes) by Peter Stark + - make dist added to set version (it won't work in a released version) + - reload function to reload (private) perl modules + - 20_FHT.pm fix: undef occures once without old data + - "setstate comment" is replaced with the attr command (i.e. attribute). + The corresponding xmllist COMMENT tag is replaced with the ATTR tag. + Devices or logs can have attr definitions. + - webfrontend/pgm2 (fhzweb.pl) updated to handle "room" attributes(showing + only devices in this room). + - version 0.4.2 of webfrontend/pgm3 integrated. + - contrib/ks300avg.pl to compute daily and monthly avarage values. + - the 40_KS300.pm module is computing daily and monthly avarages for the + temp/hum and wind values and sum of the rain. The cum_day and cum_month + state variables are used as helper values. To log the avarage use the + .*avg.* regexp. The regexp for the intraday log will trigger it also. + - Added the contrib file garden.pl as a more complex example: garden + irrigation. The program computes the time for irrigation from the avarage + temperature reported by the ks300-2. + - Enable uppercase hex codes (Bug reported by STefan Mayer) + - Renamed the unknown_XX FHT80b codes to code_XXXXXX, this will produce + "Undefined type" messages when reading the old save file + - RM100-2 added (thanks for the codes from andikt). + +- 2006-08-13 (2.5) + Special thanks to STefan Mayer for a lot of suggestions and bug reports + - If a command is enclosed in {}, then it will be evaluated as a perl + expression, if it is enclosed in "" then it is a shell command, else it is + a "normal" fhz1000 command. + "at" and "notifyon" follow this convention too. + Note: a shell command will always be issued in the background. + - won't die anymore if the at spec contains an unknown command + - rereadcfg added. Sending a HUP should work better now + - escaping % and @ in the notify argument is now possible with %% or @@ + - new command trigger to test notify commands + - where you could specify an fhz command, now you can specify a list of + them, separated by ";". Escape is ;; + - KS300 sometimes reports "negative" rain when it begins to rain. Filter + such values. israining is set when the raincounter changed or the ks300 + israining bit is set. + - sleep command, with millisecond accuracy + - HMS 100MG support by Peter Stark. + - Making FHT and FS20 messages more uniform + - contrib/fs20_holidays.sh by STefan Mayer + (simulate presence while on holiday) + - webfrontends/pgm4 by STefan Mayer: fs20.php + - KS300 avg. monthly values fixed (hopefully) + - deleted undocumented "exec" function (you can write it now as {...}) + +- 2006-09-08 (2.6) + - bugfix: updated the examples (hint from Juergen) + - bugfix: leading and trailing whitespaces in commands are ignored now + - feature: making life easier for perl oneliners: see commandref.html + (motivated by STefans suggestions) + - feature: include command and multiline commands in the configfiles (\) + - bugfix: web/pgm2 KS300 rain plot knows about the avg data + - bugfix: the FHT > 25.5 problem. Needs to be tested. + - feature: log unknown devices (peters idea, see notifyon description) + - feature: HMS wildcard device id for all HMS devices. See the define/HMS + section in the commandref.html for details. + NOTE: the wildcard for RM100-2 changed from 1001 to 1003. + (peters idea) + - feature: rolwzo_no_off.sh contrib file (for those who were already closed + out by automatically closing rollades, by Martin) + - feature: the current version (0.4.5) of the pgm3 from Martin. + +- 2006-09-13 (2.6a) + - bugfix: the FHT > 25.5 problem again. A never ending story. + +- 2006-10-03 (2.7) + - bugfix: Another try on the > 25.5 problem. (Peters suggestion) + - feature: 99_ALARM.pm from Martin (in the contrib directory) + - feature: HMS100TFK von Peter P. + - feature: attribute loglevel + - feature: attribute dummy + - feature: attr command documented + - feature: the current version (0.5a) of the pgm3 from Martin. + +- 2006-11-08 (2.8) + - feature: store oldvalue for triggers. perl only. requested by peter. + - feature: inform cmd. Patch by Martin. There are many Martins around here :-) + - bugfix: XML: fix & and < and co + - bugfix: Accept KS300 negative temperature values + - change: the FS20 msg "rain-msg" is called now "activate" + - feature: start/stop rc script from Stefan (in the contrib directory) + - feature: attribute extra_notify: setting the device will trigger a notify + - feature: optional repeat count for the at command + - feature: skip_next attribute for the at command + - feature: WS300 support by Martin. Check the contrib/ws300 directory. + - bugfix: 91_DbLog.pm: retry if the connection is broken by Peter + - feature: Martin's pgm3-0.5.2 (see the CHANGELOG on his webpage) + - feature: RRD logging example by Peter (in the contrib/rrd directory) + +- 2006-11-19 (2.9) + - bugfix: fhz1000.pl dies at startup if the savefile does not exist + - bugfix: oldvalue hash is not initialized at startup (peter, Nov 09) + - feature: Notify reorganization (requested by juergen and matthias) : + - inform will be notified on both real events and set or trigger commands + - filelogs will additionally be notified on set or trigger commands + - the extra_notify flag is gone: it is default now, there is a + do_not_notify flag for the opposite behaviour. + - feature: at timespec as a function. Example: at +*{sunset()} + commandref.html and examples revisited. + - feature: 99_SUNRISE.pm added to use with the new at functionality + (replaces the old 99_SUNSET.pm) + - feature: webpgm2 "everything" room, at/notify section, arbitrary command + - bugfix: resetting the KS300 + - feature: updated ws300pc (from martin klerx, Nov 08) + - bugfix: parsing timed commands implemented => thermo-off,thermo-on and + activate replaced with timed off-for-timer,on-for-timer and + on-old-for-timer (reported by martin klerx, Nov 08) + - feature: pidfile (requested by peter, Nov 10) + - bugfix: function 81 is not allowed + +- 2006-11-27 (2.9a) + - bugfix: FileLog+Unknown device generates undefined messages + - bugfix: trigger with unknown device generates undefined messages + +- 2006-12-28 (3.0) + - bugfix: KS300: Make the temperature negative, not the humidity + - bugfix: generate correct xmllist even with fhzdev none (Martin, 12.12) + - feature: one set command can handle multiple devices (range and enumeration) + - feature: new FS20 command on-till + - feature: perl: the current state is stored in the %value hash + - feature: perl: sunset renamed to sunset_rel, sunset_abs added (for on-till) + - feature: perl: isday function added + - feature: follow-on-for-timer attribute added to set the state to off + - bugfix: the ws300pc negative-temp bugfix included (from Martin Klerx) + - feature: version 0.6.2 of the webpgm3 included (from Martin Haas) + +- 2007-01-08 (3.1) + - bugfix: delete checks the arg first "exactly", then as a regexp + - bugfix: sun*_rel does not work correctly with offset (Martin) + - feature: FAQ entry on how to install the sunrise stuff. + - feature: the inner core is modified to be able to handle + more than one "IO" device, i.e multiple FHZ at the same time, + or FHZ + FS10 + WS300. Consequences: + - "fhzdev " replaced with "define FHZ " + - "sendraw " replaced with "set raw " + - module function parameters changed (for module developers) + - set FHZ activefor dev + - select instead sleep after sending FHZ commands + - the at timer is more exact (around 1msec instead of 1 sec) + - ignoring FS20 device 0001/00/00 + - feature: contrib/serial.pm to debug serial devices. + - feature: WS300 integrated: no external program needed (Martin) + - feature: updated to pgm3-0.7.0, see the CHANGELOG at Martins site + +- 2007-01-14 (3.2) + - bugfix: example $state changed to $value (remco) + - bugfix: sun*_rel does not work correctly with offset (Sebastian) + - feature: new HMS100TF codes (Sebastian) + - feature: logging unknown HMS with both unique and class ID (Sebastian) + - feature: WS300: "Wetter-Willi-Status", rain_raw/rain_cum added, historic + data (changes by Martin & Markus) + - bugfix: broken rereadcfg / CommandChain after init + (reported by Sebastian and Peter) + - bugfix: sunrise_coord returned "3", which is irritating + +- 2007-01-25 (3.3) + - bugfix: 50_WS300.pm fix from Martin + - bugfix: pidfile does not work as expected (reported by Martin) + - bugfix: %U in the log-filename is wrong (bugreport by Juergen) + - feature: %V added to the log-filename + - feature: KS300 wind calibration possibility added + - feature: (software) filtering repeater messages (suggested by Martin) + - feature: the "client" fhz1000.pl can address another host + - bugfix: empty FHT battery is not reported (by Holger) + - feature: new FHT codes, e.g. month/day/hour/minute setting (by Holger) + +- ==DATE== (3.4) + - bugfix: deny at +{3}... (only +*{3} allowed), reported by Bernd, 25.02 + - bugfix: allow numbers greater then 9 in at +{} + - feature: new 50_WS300.pm from Martin (bugfix + rain statistics, 26.02) + - feature: renamed fhz1000 to fhem diff --git a/FHEM/00_FHZ.pm b/FHEM/00_FHZ.pm new file mode 100755 index 000000000..d6c8e6d8f --- /dev/null +++ b/FHEM/00_FHZ.pm @@ -0,0 +1,574 @@ +############################################## +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use Device::SerialPort; + +sub FHZ_Write($$$); +sub FHZ_Read($); +sub FHZ_ReadAnswer($$); +sub FhzCrc(@); +sub CheckFhzCrc($); + +my $fhzdata = ""; +my $msgstart = pack('H*', "81");# Every msg starts wit this + +my %gets = ( + "init1" => "c9 02011f64", + "init2" => "c9 02011f60", + "init3" => "c9 02011f0a", + "serial" => "04 c90184570208", + "fhtbuf" => "04 c90185", +); +my %sets = ( + "time" => "c9 020161", + "initHMS" => "04 c90186", + "initFS20" => "04 c90196", + "FHTcode" => "04 c901839e0101", + + "activefor"=> "xx xx", + "raw" => "xx xx", +); +my %setnrparam = ( + "time" => 0, + "initHMS" => 0, + "initFS20" => 0, + "FHTcode" => 0, + "activefor"=> 1, + "raw" => 2, +); + +my %codes = ( + "^8501..\$" => "fhtbuf", +); + +my %readings; +my $def; +my %msghist; # Used when more than one FHZ is attached +my $msgcount = 0; + +##################################### +# Note: we are a data provider _and_ a consumer at the same time +sub +FHZ_Initialize($) +{ + my ($hash) = @_; + + + $hash->{Category}= "DEV"; + +# Provider + $hash->{ReadFn} = "FHZ_Read"; + $hash->{WriteFn} = "FHZ_Write"; + $hash->{Clients} = ":FHZ:FS20:FHT:HMS:KS300:"; + +# Consumer + $hash->{Match} = "^81..C9..0102"; + $hash->{DefFn} = "FHZ_Define"; + $hash->{UndefFn} = "FHZ_Undef"; + $hash->{GetFn} = "FHZ_Get"; + $hash->{SetFn} = "FHZ_Set"; + $hash->{StateFn} = "FHZ_SetState"; + $hash->{ListFn} = "FHZ_List"; + $hash->{ParseFn} = "FHZ_Parse"; +} + +##################################### +sub +FHZ_Set($@) +{ + my ($hash, @a) = @_; + + return "Need one to three parameter" if(@a > 4); + return "invalid parameter, use one of:\n " . join("\n ", sort keys %sets) + if(!defined($sets{$a[1]})); + return "Wrong number of parameters, need " . ($setnrparam{$a[1]} + 2) + if(@a != ($setnrparam{$a[1]} + 2)); + + my ($fn, $arg) = split(" ", $sets{$a[1]}); + + my $v = join(" ", @a); + Log GetLogLevel("FHZ"), "FHZ set $v"; + + if($a[1] eq "activefor") { + + my $dhash = $defs{$a[2]}; + return "device $a[2] unknown" if(!defined($dhash)); + + return "Cannot handle $dhash->{TYPE} devices" + if($devmods{FHZ}->{Clients} !~ m/:$dhash->{TYPE}:/); + + $dhash->{IODev} = $hash; + return undef; + + } elsif($a[1] eq "raw") { + + $fn = $a[2]; + $arg = $a[3]; + + } elsif($a[1] eq "time") { + + my @t = localtime; + $arg .= sprintf("%02x%02x%02x%02x%02x", + $t[5]%100, $t[4]+1, $t[3], $t[2], $t[1]); + + } elsif($a[1] eq "FHTcode") { + + return "invalid argument, must be hex" if(!$a[2] || + $a[2] !~ m/^[A-F0-9]{2}$/); + $arg .= $a[2]; + + } + + FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); + return undef; +} + +##################################### +sub +FHZ_Get($@) +{ + my ($hash, @a) = @_; + + return "\"get FHZ\" needs only one parameter" if(@a != 2); + if(!defined($gets{$a[1]})) { + return "Unknown set value $a[1], please specify one of: " . + join(" ", sort(keys %gets)); + } + + my ($fn, $arg) = split(" ", $gets{$a[1]}); + + my $v = join(" ", @a); + Log GetLogLevel("FHZ"), "FHZ get $v"; + + FHZ_Write($hash, $fn, $arg) if(!IsDummy("FHZ")); + + my $msg = FHZ_ReadAnswer($hash, $a[1]); + return $msg if(!$msg || $msg !~ /^81..c9..0102/); + + if($a[1] eq "serial") { + $v = substr($msg, 22, 8) + + } elsif($a[1] eq "fhtbuf") { + $v = substr($msg, 16, 2); + + } else { + $v = substr($msg, 12); + } + $readings{$a[1]}{VAL} = $v; + $readings{$a[1]}{TIM} = TimeNow(); + + return "$a[0] $a[1] => $v"; +} + +##################################### +sub +FHZ_List($) +{ + my ($hash) = @_; + + my $str = ""; + foreach my $m (sort keys %readings) { + $str .= sprintf("%-19s %-15s %s\n", + $readings{$m}{TIM},$m,$readings{$m}{VAL}); + } + return $str; +} + +##################################### +sub +FHZ_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + return "Undefined value $vt" if(!defined($gets{$vt})); + + if(!$readings{$vt} || $readings{$vt}{TIM} lt $tim) { + $readings{$vt}{TIM} = $tim; + $readings{$vt}{VAL} = $val; + } + return undef; +} + + +##################################### +sub +DoInit($) +{ + my $name = shift; + my @init; + push(@init, "get $name init2"); + push(@init, "get $name serial"); + push(@init, "set $name initHMS"); + push(@init, "set $name initFS20"); + push(@init, "set $name time"); + + # Workaround: Sending "set 0001 00 off" after initialization to enable + # the fhz1000 receiver, else we won't get anything reported. + push(@init, "set $name raw 04 01010100010000"); + + CommandChain(3, \@init); +} + +##################################### +sub +FHZ_Define($$) +{ + my ($hash, @a) = @_; + + $hash->{STATE} = "Initialized"; + + delete $hash->{PortObj}; + delete $hash->{FD}; + + my $dev = $a[2]; + if($dev eq "none") { + Log 1, "FHZ device is none, commands will be echoed only"; + return undef; + } + + my $po = new Device::SerialPort ($dev); + return "Can't open $dev: $!\n" if(!$po); + + $po->reset_error(); + $po->baudrate(9600); + $po->databits(8); + $po->parity('none'); + $po->stopbits(1); + $po->handshake('none'); + + $hash->{PortObj} = $po; + $hash->{FD} = $po->FILENO; + $hash->{DeviceName} = $dev; + + DoInit($a[0]); + return undef; +} + +##################################### +sub +FHZ_Undef($$) +{ + my ($hash, $arg) = @_; + foreach my $d (keys %defs) { + if(defined($defs{$d}) && + defined($defs{$d}{IODev}) && + $defs{$d}{IODev} == $hash) + { + Log 4, "deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + $hash->{PortObj}->close(); + return undef; +} + +##################################### +sub +FHZ_Parse($$) +{ + my ($hash,$msg) = @_; + + my $omsg = $msg; + $msg = substr($msg, 12); # The first 12 bytes are not really interesting + + my $type = ""; + foreach my $c (keys %codes) { + if($msg =~ m/$c/) { + $type = $codes{$c}; + last; + } + } + + if(!$type) { + Log 4, "FHZ unknown: $omsg"; + $def->{CHANGED}[0] = "$msg"; + return $hash->{NAME}; + } + + + if($type eq "fhtbuf") { + $msg = substr($msg, 4, 2); + } + + Log 4, "FHZ $type: $msg)"; + $def->{CHANGED}[0] = "$type: $msg"; + return $hash->{NAME}; +} + +##################################### +sub +FhzCrc(@) +{ + my $sum = 0; + map { $sum += $_; } @_; + return $sum & 0xFF; +} + +##################################### +sub +CheckFhzCrc($) +{ + my $msg = shift; + return 0 if(length($msg) < 8); + + my @data; + for(my $i = 8; $i < length($msg); $i += 2) { + push(@data, ord(pack('H*', substr($msg, $i, 2)))); + } + my $crc = hex(substr($msg, 6, 2)); + + # 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); +} + + +##################################### +# This is a direct read for commands like get +sub +FHZ_ReadAnswer($$) +{ + my ($hash,$arg) = @_; + + return undef if(!$hash || !defined($hash->{FD})); + + my ($mfhzdata, $rin) = ("", ''); + + for(;;) { + + vec($rin, $hash->{FD}, 1) = 1; + my $nfound = select($rin, undef, undef, 3); + 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, "FHZ/RAW: " . unpack('H*',$buf); + $mfhzdata .= $buf; + next if(length($mfhzdata) < 2); + + my $len = ord(substr($mfhzdata,1,1)) + 2; + if($len>20) { + Log 1, "Oversized message (" . unpack('H*',$mfhzdata) . + "), dropping it ..."; + return undef; + } + return unpack('H*', $mfhzdata) if(length($mfhzdata) == $len); + } +} + +############## +sub +FHZ_CompleteMsg($$) +{ + my ($fn,$msg) = @_; + my $len = length($msg); + my @data; + 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); +} + +##################################### +sub +FHZ_Write($$$) +{ + my ($hash,$fn,$msg) = @_; + + if(!$hash || !defined($hash->{PortObj})) { + Log 5, "FHZ device $hash->{NAME} is not active, cannot send"; + 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 "04" && substr($msg,0,6) eq "010101") { + my $nmsg = "0101a001" . substr($msg, 6, 6) . "00" . substr($msg, 12); + $msghist{$msgcount}{TIME} = gettimeofday(); + $msghist{$msgcount}{NAME} = $hash->{NAME}; + $msghist{$msgcount}{MSG} = unpack('H*', FHZ_CompleteMsg($fn, $nmsg)); + $msgcount++; + } + + my $bstring = FHZ_CompleteMsg($fn, $msg); + Log 5, "Sending " . unpack('H*', $bstring); + + if(!$hash->{QUEUECNT}) { + $hash->{PortObj}->write($bstring); + ############## + # Write the next buffer not earlier than 0.22 seconds (= 65.6ms + 10ms + + # 65.6ms + 10ms + 65.6ms), else it will be discarded by the FHZ1X00 PC + InternalTimer(gettimeofday()+0.25, "FHZ_HandleWriteQueue", $hash); + } elsif($hash->{QUEUECNT} == 1) { + $hash->{QUEUE} = [ $bstring ]; + } else { + push(@{$hash->{QUEUE}}, $bstring); + } + $hash->{QUEUECNT}++; + + +} + +##################################### +sub +FHZ_HandleWriteQueue($) +{ + my $hash = shift; + my $cnt = --$hash->{QUEUECNT}; + if($cnt > 0) { + my $bstring = shift(@{$hash->{QUEUE}}); + $hash->{PortObj}->write($bstring); + InternalTimer(gettimeofday()+0.25, "FHZ_HandleWriteQueue", $hash); + } +} + +##################################### +sub +FHZ_Read($) +{ + my ($hash) = @_; + + my $buf = $hash->{PortObj}->input(); + my $iohash = $devmods{$hash->{TYPE}}; + 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); + $hash->{PortObj} = new Device::SerialPort($devname); + if($hash->{PortObj}) { + Log 1, "USB device $devname reappeared"; + $hash->{FD} = $hash->{PortObj}->FILENO; + DoInit($name); + return; + } + } + } + + Log 5, "FHZ/RAW: " . unpack('H*',$buf) . + " (Unparsed: " . unpack('H*', $fhzdata) . ")"; + $fhzdata .= $buf; + + while(length($fhzdata) > 2) { + + ################################### + # Skip trash. + my $si = index($fhzdata, $msgstart); + if($si) { + if($si == -1) { + Log(5, "Bogus message received, no start character found"); + $fhzdata = ""; + last; + } else { + Log(5, "Bogus message received, skipping to start character"); + $fhzdata = substr($fhzdata, $si); + } + } + + my $len = ord(substr($fhzdata,1,1)) + 2; + if($len>20) { + Log 4, + "Oversized message (" . unpack('H*',$fhzdata) . "), dropping it ..."; + $fhzdata = ""; + next; + } + + last if(length($fhzdata) < $len); + + my $dmsg = unpack('H*', substr($fhzdata, 0, $len)); + if(CheckFhzCrc($dmsg)) { + + if(substr($fhzdata,2,1) eq $msgstart) { # Skip function 0x81 + $fhzdata = substr($fhzdata, 2); + next; + } + + ############### + # check for duplicate msg from different FHZ's + my $now = gettimeofday(); + my $skip; + my $meetoo = ($attr{$name}{repeater} ? 1 : 0); + + my $to = 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++; + + + my @found; + foreach my $m (sort { $devmods{$a}{ORDER} cmp $devmods{$b}{ORDER} } + keys %devmods) { + next if($iohash->{Clients} !~ m/:$m:/); + next if($dmsg !~ m/$devmods{$m}{Match}/i); + no strict "refs"; + @found = &{$devmods{$m}{ParseFn}}($hash,$dmsg); + use strict "refs"; + 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} = $2; + DoTrigger($d, "$2 $3"); + delete $defs{$d}; + goto NEXTMSG; + } + + foreach my $found (@found) { + DoTrigger($found, undef); + } +NEXTMSG: + $fhzdata = substr($fhzdata, $len); + + } else { + + Log 4, "Bad CRC message, skipping it (Bogus message follows)"; + $fhzdata = substr($fhzdata, 2); + + } + } +} + +1; diff --git a/FHEM/10_FS20.pm b/FHEM/10_FS20.pm new file mode 100755 index 000000000..c84e40caa --- /dev/null +++ b/FHEM/10_FS20.pm @@ -0,0 +1,331 @@ +############################################## +package main; + +use strict; +use warnings; + + +my %codes = ( + "00" => "off", + "01" => "dim06%", + "02" => "dim12%", + "03" => "dim18%", + "04" => "dim25%", + "05" => "dim31%", + "06" => "dim37%", + "07" => "dim43%", + "08" => "dim50%", + "09" => "dim56%", + "0a" => "dim62%", + "0b" => "dim68%", + "0c" => "dim75%", + "0d" => "dim81%", + "0e" => "dim87%", + "0f" => "dim93%", + "10" => "dim100%", + "11" => "on", # Set to previous dim value (before switching it off) + "12" => "toggle", # between off and previous dim val + "13" => "dimup", + "14" => "dimdown", + "15" => "dimupdown", + "16" => "timer", + "17" => "sendstate", + "18" => "off-for-timer", + "19" => "on-for-timer", + "1a" => "on-old-for-timer", + "1b" => "reset", + "1c" => "ramp-on-time", #time to reach the desired dim value on dimmers + "1d" => "ramp-off-time", #time to reach the off state on dimmers +); + +my %readonly = ( + "thermo-on" => 1, + "thermo-off" => 1, +); + +use vars qw(%fs20_c2b); # Peter would like to access it from outside +my %defptr; +my %readings; +my %follow; + +sub +FS20_Initialize($) +{ + my ($hash) = @_; + + foreach my $k (keys %codes) { + $fs20_c2b{$codes{$k}} = $k; + } + $fs20_c2b{"on-till"} = 99; + + $hash->{Category} = "DEV"; + + $hash->{Match} = "^81..(04|0c)..0101a001"; + $hash->{SetFn} = "FS20_Set"; + $hash->{GetFn} = "FS20_Get"; + $hash->{ListFn} = "FS20_List"; + $hash->{StateFn} = "FS20_SetState"; + $hash->{DefFn} = "FS20_Define"; + $hash->{UndefFn} = "FS20_Undef"; + $hash->{ParseFn} = "FS20_Parse"; +} + +################################### +sub +FS20_Get($@) +{ + my ($hash, @a) = @_; + return "No get function implemented"; +} + +################################### +sub +FS20_List($) +{ + my ($hash) = @_; + + my $n = $hash->{NAME}; + if(!defined($readings{$n})) { + return "No information about $n\n"; + } else { + return sprintf("%-19s %s\n", $readings{$n}{TIM}, $readings{$n}{VAL}); + } +} + +##################################### +sub +FS20_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + return "Undefined value $vt" if(!defined($fs20_c2b{$vt})); + + my $name = $hash->{NAME}; + if(!$readings{$name} || $readings{$name}{TIM} lt $tim) { + $readings{$name}{TIM} = $tim; + $readings{$name}{VAL} = $vt; + } + return undef; +} + +############################# +sub +Do_On_Till($@) +{ + my ($hash, @a) = @_; + return "Timespec (HH:MM[:SS]) needed for the on-till command" if(@a != 3); + + my ($err, $hr, $min, $sec, $fn) = GetTimeSpec($a[2]); + return $err if($err); + + my @lt = localtime; + my $hms_till = sprintf("%02d:%02d:%02d", $hr, $min, $sec); + my $hms_now = sprintf("%02d:%02d:%02d", $lt[2], $lt[1], $lt[0]); + if($hms_now ge $hms_till) { + Log 4, "on-till: won't switch as now ($hms_now) is later than $hms_till"; + return ""; + } + + my @b = ($a[0], "on"); + FS20_Set($hash, @b); + CommandAt(undef, "$hms_till set $a[0] off"); +} + + +################################### +sub +FS20_Set($@) +{ + my ($hash, @a) = @_; + my $ret = undef; + my $na = int(@a); + + return "no set value specified" if($na < 2 || $na > 3); + return "Readonly value $a[1]" if(defined($readonly{$a[1]})); + + my $c = $fs20_c2b{$a[1]}; + if(!defined($c)) { + return "Unknown set value $a[1], please specify one of:\n " . + join("\n ", sort(keys %fs20_c2b)); + } + + return Do_On_Till($hash, @a) if($a[1] eq "on-till"); + + return "Bad time spec" if($na == 3 && $a[2] !~ m/^\d*\.?\d+$/); + + my $v = join(" ", @a); + Log GetLogLevel($a[0]), "FS20 set $v"; + (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 { + + $c =~ s/1/3/; # Set the extension bit + + ######################## + # Calculating the time. + LOOP: for(my $i = 0; $i <= 12; $i++) { + for(my $j = 0; $j <= 15; $j++) { + $val = (2**$i)*$j*0.25; + if($val >= $a[2]) { + if($val != $a[2]) { + $ret = "FS20 Setting timeout to $val from $a[2]"; + Log GetLogLevel($a[0]), $ret; + } + $c .= sprintf("%x%x", $i, $j); + last LOOP; + } + } + } + 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])); + + } + + ########################################### + # Set the state of a device to off if on-for-timer is called + if($follow{$a[0]}) { + CommandDelete(undef, "at .*setstate.*$a[0]"); + delete $follow{$a[0]}; + } + if($a[1] eq "on-for-timer" && $na == 3 && + defined($attr{$a[0]}) && defined($attr{$a[0]}{"follow-on-for-timer"})) { + my $to = sprintf("%02d:%02d:%02d", $val/3600, ($val%3600)/60, $val%60); + $follow{$a[0]} = $to; + Log 4, "Follow: +$to setstate $a[0] off"; + CommandAt(undef, "+$to setstate $a[0] off"); + } + + ########################## + # Look for all devices with the same code, and set state, timestamp + my $code = "$hash->{XMIT} $hash->{BTN}"; + my $tn = TimeNow(); + foreach my $n (keys %{ $defptr{$code} }) { + $defptr{$code}{$n}->{CHANGED}[0] = $v; + $defptr{$code}{$n}->{STATE} = $v; + $readings{$n}{TIM} = $tn; + $readings{$n}{VAL} = $v; + } + + + return $ret; +} + +############################# +sub +FS20_Define($@) +{ + my ($hash, @a) = @_; + my $u = + "wrong syntax: define FS20 housecode addr [fg addr] [lm addr] [gm FF]"; + + return $u if(int(@a) < 4); + return "Define $a[0]: wrong housecode format: specify a 4 digit hex value" + if($a[2] !~ m/^[a-f0-9]{4}$/i); + return "Define $a[0]: wrong btn format: specify a 2 digit hex value" + if($a[3] !~ m/^[a-f0-9]{2}$/i); + + $hash->{XMIT} = lc($a[2]); + $hash->{BTN} = lc($a[3]); + + my $code = "$a[2] $a[3]"; + my $ncode = 1; + my $name = $a[0]; + + $hash->{CODE}{$ncode++} = $code; + $defptr{$code}{$name} = $hash; + + for(my $i = 4; $i < int(@a); $i += 2) { + + return "No address specified for $a[$i]" if($i == int(@a)-1); + + $a[$i] = lc($a[$i]); + if($a[$i] eq "fg") { + return "Bad fg address, see the doc" if($a[$i+1] !~ m/^f[a-f0-9]$/); + } elsif($a[$i] eq "lm") { + return "Bad lm address, see the doc" if($a[$i+1] !~ m/^[a-f0-9]f$/); + } elsif($a[$i] eq "gm") { + return "Bad gm address, mus be ff" if($a[$i+1] ne "ff"); + } else { + return $u; + } + + $code = "$a[2] $a[$i+1]"; + $hash->{CODE}{$ncode++} = $code; + $defptr{$code}{$name} = $hash; + } + AssignIoPort($hash); +} + +############################# +sub +FS20_Undef($$) +{ + my ($hash, $name) = @_; + foreach my $c (keys %{ $hash->{CODE} } ) { + delete($defptr{$c}{$name}); + } + return undef; +} + +sub +FS20_Parse($) +{ + my ($hash, $msg) = @_; + + # Msg format: + # 81 0b 04 f7 0101 a001 HHHH 01 00 11 + + my $dev = substr($msg, 16, 4); + my $btn = substr($msg, 20, 2); + my $cde = substr($msg, 24, 2); + + my $def = $defptr{"$dev $btn"}; + + my $dur = 0; + my $cx = hex($cde); + if($cx & 0x20) { + $dur = hex(substr($msg, 26, 2)); + my $i = ($dur & 0xf0) / 16; + my $j = ($dur & 0xf); + $dur = (2**$i)*$j*0.25; + $cde = sprintf("%02x", $cx & ~0x20); + } + + my $v = $codes{$cde}; + $v = "unknown:$cde" if(!defined($v)); + $v .= " $dur" if($dur); + if($def) { + + my @list; + foreach my $n (keys %{ $def }) { + $readings{$n}{TIM} = TimeNow(); + $readings{$n}{VAL} = $v; + $def->{$n}->{CHANGED}[0] = $v; + $def->{$n}->{STATE} = $v; + Log GetLogLevel($n), "FS20 $n $v"; + push(@list, $n); + } + return @list; + + } else { + # Special FHZ initialization parameter. In Multi-FHZ-Mode we receive + # it by the second FHZ + return "" if($dev eq "0001" && $btn eq "00" && $cde eq "00"); + + Log 3, "FS20 Unknown device $dev, Button $btn Code $cde ($v), " . + "please define it"; + return "UNDEFINED FS20 $dev/$btn/$cde"; + } + +} + + +1; diff --git a/FHEM/20_FHT.pm b/FHEM/20_FHT.pm new file mode 100755 index 000000000..5e0146b56 --- /dev/null +++ b/FHEM/20_FHT.pm @@ -0,0 +1,404 @@ +############################################## +package main; + +use strict; +use warnings; + +my %codes = ( + "0000.6" => "actuator", + "00002c" => "synctime", # Not verified + "0100.6" => "actuator1", # Not verified (1-8) + "0200.6" => "actuator2", + "0300.6" => "actuator3", + "0400.6" => "actuator4", + "0500.6" => "actuator5", + "0600.6" => "actuator6", + "0700.6" => "actuator7", + "0800.6" => "actuator8", + "140069" => "mon-from1", + "150069" => "mon-to1", + "160069" => "mon-from2", + "170069" => "mon-to2", + "180069" => "tue-from1", + "190069" => "tue-to1", + "1a0069" => "tue-from2", + "1b0069" => "tue-to2", + "1c0069" => "wed-from1", + "1d0069" => "wed-to1", + "1e0069" => "wed-from2", + "1f0069" => "wed-to2", + "200069" => "thu-from1", + "210069" => "thu-to1", + "220069" => "thu-from2", + "230069" => "thu-to2", + "240069" => "fri-from1", + "250069" => "fri-to1", + "260069" => "fri-from2", + "270069" => "fri-to2", + "280069" => "sat-from1", + "290069" => "sat-to1", + "2a0069" => "sat-from2", + "2b0069" => "sat-to2", + "2c0069" => "sun-from1", + "2d0069" => "sun-to1", + "2e0069" => "sun-from2", + "2f0069" => "sun-to2", + "3e0069" => "mode", + "3f0069" => "holiday1", # Not verified + "400069" => "holiday2", # Not verified + "410069" => "desired-temp", + "XX0069" => "measured-temp", # sum of next. two, never "really" sent + "420069" => "measured-low", + "430069" => "measured-high", + "440069" => "state", + "600069" => "year", + "610069" => "month", + "620069" => "day", + "630069" => "hour", + "640069" => "minute", + "650069" => "init", + "820069" => "day-temp", + "840069" => "night-temp", + "850069" => "unknown_85", + "8a0069" => "windowopen-temp", + + "0000aa" => "code_0000aa", + "0000ba" => "code_0000ba", + "430079" => "code_430079", + "440079" => "code_440079", + "4b0067" => "code_4b004b", + "4b0077" => "code_4b0077", + "7e0067" => "code_7e0067", +); + +my %cantset = ( + "actuator" => 1, + "actuator1" => 1, + "actuator2" => 1, + "actuator3" => 1, + "actuator4" => 1, + "actuator5" => 1, + "actuator6" => 1, + "actuator7" => 1, + "actuator8" => 1, + "synctime" => 1, + "measured-temp" => 1, + "measured-high" => 1, + "measured-low" => 1, + "state" => 1, + "init" => 1, + + "code_0000aa" => 1, + "code_0000ba" => 1, + "code_430079" => 1, + "code_440079" => 1, + "code_4b004b" => 1, + "code_4b0077" => 1, + "code_7e0067" => 1, +); + +my %nosetarg = ( + "help" => 1, + "refreshvalues" => 1, +); + +my %c2m = (0 => "auto", 1 => "manual", 2 => "holiday"); +my %m2c = ("auto" => 0, "manual" => 1, "holiday" => 2); + +my %readings; +my %defptr; +my %c2b; # command->button hash (reverse of codes) +my %c2bset; # Setteable values + + +##################################### +sub +FHT_Initialize($) +{ + my ($hash) = @_; + + foreach my $k (keys %codes) { + my $v = $codes{$k}; + $c2b{$v} = $k; + $c2bset{$v} = substr($k, 0, 2) if(!defined($cantset{$v})); + } + $c2bset{refreshvalues} = "65ff66ff"; + + $hash->{Category} = "DEV"; + +# 810c0426 0909a001 1111 1600 +# 810c04b3 0909a001 1111 44006900 +# 810b0402 83098301 1111 41301d +# 81090421 c409c401 1111 00 + +# 810c0d20 0909a001 3232 7e006724 (NYI) + + $hash->{Match} = "^81..(04|09|0d)..(0909a001|83098301|c409c401).."; + $hash->{SetFn} = "FHT_Set"; + $hash->{GetFn} = "FHT_Get"; + $hash->{StateFn} = "FHT_SetState"; + $hash->{ListFn} = "FHT_List"; + $hash->{DefFn} = "FHT_Define"; + $hash->{UndefFn} = "FHT_Undef"; + $hash->{ParseFn} = "FHT_Parse"; +} + + +##################################### +sub +FHT_Set($@) +{ + my ($hash, @a) = @_; + my $ret = undef; + + return "\"set $a[0]\" needs two parameters" + if(@a != 3 && !(@a == 2 && $nosetarg{$a[1]})); + return "invalid parameter, use one of:\n " . + join("\n ", sort {$c2bset{$a} cmp $c2bset{$b} } keys %c2bset) + if(!defined($c2bset{$a[1]})); + + + Log GetLogLevel($a[0]), "FHT set " . join(" ", @a); + + my $arg = "020183" . $hash->{CODE} . $c2bset{$a[1]}; + + if($a[1] eq "refreshvalues") { + + # This is special. Without the sleep the next FHT won't send its data + if(!IsDummy($a[0])) { + my $havefhz; + $havefhz = 1 if($hash->{IODev} && defined($hash->{IODev}->{FD})); + + IOWrite($hash, "04", $arg); + sleep(1) if($havefhz); + IOWrite($hash, "04", "c90185"); # Check the fht buffer + sleep(1) if($havefhz); + } + return $ret; + + } elsif($a[1] =~ m/-temp/) { + return "Invalid temperature, use NN.N" if($a[2] !~ m/^\d*\.?\d+$/); + my $a = int($a[2]*2); + $arg .= sprintf("%02x", $a); + $ret = "Rounded temperature to " . $a/2 if($a/2 != $a[2]); + + } elsif($a[1] =~ m/-from/ || $a[1] =~ m/-to/) { + return "Invalid timeformat, use HH:MM" if($a[2] !~ m/^([0-2]\d):([0-5]\d)/); + my $a = ($1*6) + ($2/10); + $arg .= sprintf("%02x", $a); + + my $nt = sprintf("%02d:%02d", $1, ($2/10)*10); + $ret = "Rounded time to $nt" if($nt ne $a[2]); + + } elsif($a[1] eq "mode") { + return "Invalid mode, use one of " . join(" ", sort keys %m2c) + if(!defined($m2c{$a[2]})); + $arg .= sprintf("%02x", $m2c{$a[2]}); + + } else { # Holiday1, Holiday2 + $arg .= sprintf("%02x", $a[2]); + + } + + IOWrite($hash, "04", $arg) if(!IsDummy($a[0])); + return $ret; +} + +##################################### +sub +FHT_Get($@) +{ + my ($hash,@a) = @_; + + return "NYI"; +} + +##################################### +sub +FHT_List($) +{ + my ($hash) = @_; + + my $n = $hash->{CODE}; + if(!defined($readings{$n})) { + return "No information about " . $hash->{NAME} . "\n"; + } else { + my $str = ""; + foreach my $m (sort { $c2b{$a} cmp $c2b{$b} } keys %{ $readings{$n} }) { + $str .= sprintf("%-19s %-15s %s\n", + $readings{$n}{$m}{TIM}, $m, $readings{$n}{$m}{VAL}); + } + return $str; + } +} + +##################################### +sub +FHT_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + return "Undefined type $vt" if(!defined($c2b{$vt})); + + my $n = $hash->{CODE}; + + if(!$readings{$n}{$vt} || $readings{$n}{$vt}{TIM} lt $tim) { + $readings{$n}{$vt}{TIM} = $tim; + $readings{$n}{$vt}{VAL} = $val; + } + return undef; +} + + +##################################### +sub +FHT_Define($@) +{ + my ($hash, @a) = @_; + + return "wrong syntax: define FHT CODE" if(int(@a) != 3); + $a[2] = lc($a[2]); + return "Define $a[0]: wrong CODE format: specify a 4 digit hex value" + if($a[2] !~ m/^[a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/i); + + + $hash->{CODE} = $a[2]; + $defptr{$a[2]} = $hash; + + AssignIoPort($hash); + + Log 1, "Asking the FHT device $a[0]/$a[2] to send its data"; + FHT_Set($hash, ($a[0], "refreshvalues")); + + return undef; +} + +##################################### +sub +FHT_Undef($$) +{ + my ($hash, $name) = @_; + delete($defptr{$hash->{CODE}}); + return undef; +} + +##################################### +sub +FHT_Parse($$) +{ + my ($hash,$msg) = @_; + + my $dev = substr($msg, 16, 4); + my $cde = substr($msg, 20, 6); + my $val = substr($msg, 26, 2) if(length($msg) > 26); + + if(!defined($defptr{$dev})) { + Log 3, "FHT Unknown device $dev, please define it"; + return "UNDEFINED FHT $dev"; + } + + + my $def = $defptr{$dev}; + + # Unknown, but don't want report it. Should come with c409c401 + if($cde eq "00") { + return ""; + } + + if(length($cde) < 6) { + Log 4, "FHT Unknown code from $def->{NAME} : $cde"; + $def->{CHANGED}[0] = "unknown code $cde"; + return $def->{NAME}; + } + + + if(!$val) { + # This is a confirmation message. We reformat it so that + # it looks like a real message, and let the rest parse it + Log 4, "FHT $def->{NAME} confirmation: $cde)"; + $val = substr($cde, 2, 2); + $cde = substr($cde, 0, 2) . "0069"; + } + + my $type; + foreach my $c (keys %codes) { + if($cde =~ m/$c/) { + $type = $codes{$c}; + last; + } + } + + $val = hex($val); + + if(!$type) { + Log 4, "FHT $def->{NAME} (Unknown: $cde => $val)"; + $def->{CHANGED}[0] = "unknown $cde: $val"; + return $def->{NAME}; + } + + + my $tn = TimeNow(); + + ########################### + # Reformat the values so they are readable + if($type eq "actuator") { + $val = sprintf("%02d%%", int(100*$val/255 + 0.5)); + + } elsif($cde ge "140069" && $cde le "2f0069") { # Time specs + Log 5, "FHT $def->{NAME} ($type: $val)"; + return "" if($val == 144); # Empty, forget it + my $hour = $val / 6; + my $min = ($val % 6) * 10; + $val = sprintf("%02d:%02d", $hour, $min); + + } elsif($type eq "mode") { + $val = $c2m{$val} if(defined($c2m{$val})); + + } elsif($type eq "measured-low") { + + $readings{$dev}{$type}{TIM} = $tn; + $readings{$dev}{$type}{VAL} = $val; + return ""; + + } elsif($type eq "measured-high") { + + $readings{$dev}{$type}{TIM} = $tn; + $readings{$dev}{$type}{VAL} = $val; + + if(defined($readings{$dev}{"measured-low"}{VAL})) { + + $val = $val*256 + $readings{$dev}{"measured-low"}{VAL}; + $val /= 10; + $val = sprintf("%.1f (Celsius)", $val); + $type = "measured-temp" + + } else { + return ""; + } + + } elsif($type =~ m/.*-temp/) { + $val = sprintf("%.1f (Celsius)", $val / 2) + + } elsif($type eq "state") { + + my $nval; + $nval = "Bat: " . (($val & 1) ? "empty" : "ok"); + $nval .= ", Window: " . (($val & 32) ? "open" : "closed"); + $nval .= ", Fault: " . (($val & 16) ? "yes" : "no"); + $val = $nval; + + } elsif($type =~ m/echo_/) { # Ignore these messages + return ""; + + } + + $readings{$dev}{$type}{TIM} = $tn; + $readings{$dev}{$type}{VAL} = $val; + + Log 4, "FHT $def->{NAME} ($type: $val)"; + $def->{CHANGED}[0] = "$type: $val"; + $def->{STATE} = "$type: $val" if($type eq "measured-temp"); + return $def->{NAME}; +} + +1; diff --git a/FHEM/30_HMS.pm b/FHEM/30_HMS.pm new file mode 100755 index 000000000..63c5b4a1d --- /dev/null +++ b/FHEM/30_HMS.pm @@ -0,0 +1,262 @@ +############################################## +package main; + +use strict; +use warnings; + +my %codes = ( + "0" => "HMS100TF", + "1" => "HMS100T", + "2" => "HMS100WD", + "3" => "RM100-2", + "4" => "HMS100TFK", # Depending on the onboard jumper it is 4 or 5 + "5" => "HMS100TFK", + "6" => "HMS100MG", +); + +my %readings; +my %defptr; + + +##################################### +sub +HMS_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category} = "DEV"; + +# 810e047e0510a001473a000000120233 HMS100TF +# 810e04b90511a0018e63000001100000 HMS100T +# 810e04e80212a001ec46000001000000 HMS100WD +# 810e04d70213a001b16d000003000000 RM100-2 +# 810e047f0214a001a81f000001000000 HMS100TFK +# 810e048f0295a0010155000001000000 HMS100TFK (jumper) +# 810e04330216a001b4c5000001000000 HMS100MG + + $hash->{Match} = "^810e04....(1|5|9)[0-6]a001"; + $hash->{SetFn} = "HMS_Set"; + $hash->{GetFn} = "HMS_Get"; + $hash->{StateFn} = "HMS_SetState"; + $hash->{ListFn} = "HMS_List"; + $hash->{DefFn} = "HMS_Define"; + $hash->{UndefFn} = "HMS_Undef"; + $hash->{ParseFn} = "HMS_Parse"; +} + +################################### +sub +HMS_Set($@) +{ + my ($hash, @a) = @_; + return "No set function implemented"; +} + +################################### +sub +HMS_Get($@) +{ + my ($hash,@a) = @_; + return "No get function implemented"; +} + +##################################### +sub +HMS_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + my $n = $hash->{CODE}; + if(!$readings{$n}{$vt} || $readings{$n}{$vt}{TIM} lt $tim) { + $readings{$n}{$vt}{TIM} = $tim; + $readings{$n}{$vt}{VAL} = $val; + } + return undef; +} + +##################################### +sub +HMS_List($) +{ + my ($hash) = @_; + + my $n = $hash->{CODE}; + if(!defined($readings{$n})) { + return "No information about " . $hash->{NAME} . "\n"; + } else { + my $str = ""; + foreach my $m (keys %{ $readings{$n} }) { + $str .= sprintf("%-19s %-15s %s\n", + $readings{$n}{$m}{TIM}, $m, $readings{$n}{$m}{VAL}); + } + return $str; + } +} + +##################################### +sub +HMS_Define($@) +{ + my ($hash, @a) = @_; + + return "wrong syntax: define HMS CODE" if(int(@a) != 3); + $a[2] = lc($a[2]); + return "Define $a[0]: wrong CODE format: specify a 4 digit hex value" + if($a[2] !~ m/^[a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/); + + + $hash->{CODE} = $a[2]; + $defptr{$a[2]} = $hash; + + return undef; +} + +##################################### +sub +HMS_Undef($$) +{ + my ($hash, $name) = @_; + delete($defptr{$hash->{CODE}}); + return undef; +} + + +##################################### +sub +HMS_Parse($$) +{ + my ($hash, $msg) = @_; + + my $dev = substr($msg, 16, 4); + my $cde = substr($msg, 11, 1); + my $val = substr($msg, 24, 8) if(length($msg) == 32); + + my $type = ""; + foreach my $c (keys %codes) { + if($cde =~ m/$c/) { + $type = $codes{$c}; + last; + } + } + + # As the HMS devices change their id on each battery change, we offer + # a wildcard too for each type: 100, + my $odev = $dev; + if(!defined($defptr{$dev})) { + Log 4, "HMS device $dev not defined, using the wildcard device 100$cde"; + $dev = "100$cde"; + } + + if(!defined($defptr{$dev})) { + Log 3, "Unknown HMS device $dev/$odev, please define it"; + $type = "HMS" if(!$type); + return "UNDEFINED $type $odev"; + } + + my $def = $defptr{$dev}; + + + my (@v, @txt, @sfx); + + if($type eq "HMS100TF") { + + @txt = ( "temperature", "humidity", "battery"); + @sfx = ( "(Celsius)", "(%)", ""); + + # Codierung + my $status = hex(substr($val, 0, 1)); + $v[0] = int(substr($val, 5, 1) . substr($val, 2, 2))/10; + $v[1] = int(substr($val, 6, 2) . substr($val, 4, 1))/10; + $v[2] = "ok"; + if ( $status & 2 ) { $v[2] = "empty"; } + if ( $status & 4 ) { $v[2] = "replaced"; } + if ( $status & 8 ) { $v[0] = -$v[0]; } + + $val = "T: $v[0] H: $v[1] Bat: $v[2]"; + + } elsif ($type eq "HMS100T") { + + @txt = ( "temperature", "battery"); + @sfx = ( "(Celsius)", ""); + + my $status = hex(substr($val, 0, 1)); + $v[0] = int(substr($val, 5, 1) . substr($val, 2, 2))/10; + $v[1] = "ok"; + if ( $status & 2 ) { $v[1] = "empty"; } + if ( $status & 4 ) { $v[1] = "replaced"; } + if ( $status & 8 ) { $v[0] = -$v[0]; } + + $val = "T: $v[0] Bat: $v[1]"; + + } elsif ($type eq "HMS100WD") { + + @txt = ( "water_detect", "battery"); + @sfx = ( "", ""); + + # Battery-low condition detect is not yet properly + # implemented. As soon as my WD's batteries get low + # I am willing to supply a patch ;-) SEP7-RIPE, 2006/05/13 + my $status = hex(substr($val, 1, 1)); + $v[1] = "ok"; + $v[0] = "off"; + if ( $status & 1 ) { $v[0] = "on"; } + $val = "Water Detect: $v[0]"; + + } elsif ($type eq "HMS100TFK") { # By Peter P. + + @txt = ( "switch_detect", "battery"); + @sfx = ( "", ""); + # Battery-low condition detect is not yet properly implemented. + my $status = hex(substr($val, 1, 1)); + $v[0] = ($status ? "on" : "off"); + $v[1] = "off"; + $val = "Switch Detect: $v[0]"; + + } elsif($type eq "RM100-2") { + + @txt = ( "smoke_detect", "battery"); + @sfx = ( "", ""); + + $v[0] = ( hex(substr($val, 1, 1)) != "0" ) ? "on" : "off"; + $v[1] = "unknown"; # Battery-low detect is _NOT_ implemented. + $val = "smoke_detect: $v[0]"; + + } elsif ($type eq "HMS100MG") { # By Peter Stark + + @txt = ( "gas_detect", "battery"); + @sfx = ( "", ""); + + # Battery-low condition detect is not yet properly + # implemented. + my $status = hex(substr($val, 1, 1)); + $v[0] = ($status != "0") ? "on" : "off"; + $v[1] = "off"; + if ($status & 1) { $v[0] = "on"; } + $val = "Gas Detect: $v[0]"; + + } else { + + Log 4, "HMS Device $dev (Unknown type: $type)"; + return ""; + + } + + my $now = TimeNow(); + Log 4, "HMS Device $dev ($type: $val)"; + + my $max = int(@txt); + for( my $i = 0; $i < $max; $i++) { + $readings{$dev}{$txt[$i]}{TIM} = $now; + my $v = "$v[$i] $sfx[$i]"; + $readings{$dev}{$txt[$i]}{VAL} = $v; + $def->{CHANGED}[$i] = "$txt[$i]: $v"; + } + $readings{$dev}{type}{TIM} = $now; + $readings{$dev}{type}{VAL} = $type; + + $def->{STATE} = $val; + $def->{CHANGED}[$max] = $val; + return $def->{NAME}; +} + +1; diff --git a/FHEM/40_KS300.pm b/FHEM/40_KS300.pm new file mode 100755 index 000000000..aaeb12bf3 --- /dev/null +++ b/FHEM/40_KS300.pm @@ -0,0 +1,308 @@ +############################################## +package main; + +use strict; +use warnings; + +my %readings; +my %defptr; +my $negcount = 0; + +###################### +# Note: this is just an empty hull. + +##################################### +sub +KS300_Initialize($) +{ + my ($hash) = @_; + + # Message is like + # 810d04f94027a00171212730000008 + # 81 0d 04 f9 4027a00171 212730000008 + + $hash->{Category} = "DEV"; + + $hash->{Match} = "^810.04..402.a001"; + $hash->{SetFn} = "KS300_Set"; + $hash->{GetFn} = "KS300_Get"; + $hash->{StateFn} = "KS300_SetState"; + $hash->{ListFn} = "KS300_List"; + $hash->{DefFn} = "KS300_Define"; + $hash->{UndefFn} = "KS300_Undef"; + $hash->{ParseFn} = "KS300_Parse"; +} + +################################### +sub +KS300_Set($@) +{ + my ($hash, @a) = @_; + return "No set function implemented"; +} + +################################### +sub +KS300_Get($@) +{ + my ($hash,@a) = @_; + return "No get function implemented"; +} + +##################################### +sub +KS300_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + my $n = $hash->{CODE}; + if(!$readings{$n}{$vt} || $readings{$n}{$vt}{TIM} lt $tim) { + $readings{$n}{$vt}{TIM} = $tim; + $readings{$n}{$vt}{VAL} = $val; + } + return undef; +} + +##################################### +sub +KS300_List($) +{ + my ($hash) = @_; + my $str = ""; + + my $n = $hash->{CODE}; + if(!defined($readings{$n})) { + $str .= "No information about " . $hash->{NAME} . "\n"; + } else { + foreach my $m (keys %{ $readings{$n} }) { + $str .= sprintf("%-19s %-15s %s\n", + $readings{$n}{$m}{TIM}, $m, $readings{$n}{$m}{VAL}); + } + } + return $str; + +} + +##################################### +sub +KS300_Define($@) +{ + my ($hash, @a) = @_; + + return "wrong syntax: define KS300 " . + "[ml/raincounter] [wind-factor]" if(int(@a) < 3 || int(@a) > 5); + $a[2] = lc($a[2]); + return "Define $a[0]: wrong CODE format: specify a 4 digit hex value" + if($a[2] !~ m/^[a-f0-9][a-f0-9][a-f0-9][a-f0-9]$/); + + $hash->{CODE} = $a[2]; + my $rainunit = ((int(@a) > 3) ? $a[3] : 255); + my $windunit = ((int(@a) > 4) ? $a[4] : 1.0); + $hash->{CODE} = $a[2]; + $hash->{RAINUNIT} = $rainunit; + $hash->{WINDUNIT} = $windunit; + $defptr{$a[2]} = $hash; + + return undef; +} + +##################################### +sub +KS300_Undef($$) +{ + my ($hash, $name) = @_; + delete($defptr{$hash->{CODE}}); + return undef; +} + + +##################################### +sub +KS300_Parse($) +{ + my ($hash,$msg) = @_; + + if($msg !~ m/^810d04..4027a001/) { + Log 4, "KS300 unknown message $msg"; + return ""; + } + + ############################### + # 1 2 + #0123456789012345 67890123456789 + # + #810d04f94027a001 71212730000008 + ############################### + my @a = split("", $msg); + + ########################## + # I've seldom (1 out of 700) seen messages of length 10 and 11 with correct + # CRC, they seem to contain partial data (e.g. temp/wind/hum but not rain) + # They are suppressed as of now. + if(hex($a[3]) != 13) { + Log 4, "Strange KS400 message received, wont decode ($msg)"; + return ""; + } + + if(int(keys %defptr)) { + + my @arr = keys(%defptr); # No code is known yet + my $dev = shift(@arr); + my $def = $defptr{$dev}; + my $haverain = 0; + + my @v; + my @txt = ( "rain_raw", "rain", "wind", "humidity", "temperature", + "israining", "unknown1", "unknown2", "unknown3"); + my @sfx = ( "(counter)", "(l/m2)", "(km/h)", "(%)", "(Celsius)", + "(yes/no)", "","",""); + + # The next instr wont work for empty hashes, so we init it now + $readings{$dev}{$txt[0]}{VAL} = 0 if(!$readings{$dev}); + my $r = $readings{$dev}; + + $v[0] = hex("$a[28]$a[27]$a[26]"); + + ############################# + # My KS300 sends a (quite huge) "negative" rain, when the rain begins, + # then the value is "normal" again. So we have to filter neg. rain out. + # But if the KS300 is sending this value more than once, then accept it, + # as the KS300 was probably reset + + if($r->{rain_raw}{VAL}) { + my ($rrv, undef) = split(" ", $r->{rain_raw}{VAL}); + $haverain = 1 if($v[0] != $rrv); + if($v[0] < $rrv) { + if($negcount++ < 3) { + Log 3, "KS300 negative rain, ignoring it"; + $v[0] = $rrv; + } else { + Log 1, "KS300 was probably reset, accepting new rain value"; + } + } else { + $negcount = 0; + } + } + + $v[1] = sprintf("%0.1f", $v[0] * $def->{RAINUNIT} / 1000); + $v[2] = sprintf("%0.1f", ("$a[25]$a[24].$a[23]"+0) * $def->{WINDUNIT}); + $v[3] = "$a[22]$a[21]" + 0; + $v[4] = "$a[20]$a[19].$a[18]" + 0; $v[4] = "-$v[4]" if($a[17] eq "7"); + $v[4] = sprintf("%0.1f", $v[4]); + + $v[5] = ((hex($a[17]) & 0x2) || $haverain) ? "yes" : "no"; + $v[6] = $a[29]; + $v[7] = $a[16]; + $v[8] = $a[17]; + + # Negative temp + $v[4] = -$v[4] if($v[8] & 8); + + my $tm = TimeNow(); + + Log 4, "KS300 $dev: $msg"; + + my $max = int(@v); + + + for(my $i = 0; $i < $max; $i++) { + $r->{$txt[$i]}{TIM} = $tm; + my $val = "$v[$i] $sfx[$i]"; + $r->{$txt[$i]}{VAL} = $val; + $def->{CHANGED}[$i] = "$txt[$i]: $val"; + } + + # For logging/summary + my $val = "T: $v[4] H: $v[3] W: $v[2] R: $v[1] IR: $v[5]"; + $def->{STATE} = $val; + $def->{CHANGED}[$max++] = $val; + + ################################### + # AVG computing + if(!$r->{cum_day}) { + + $r->{cum_day}{VAL} = "$tm T: 0 H: 0 W: 0 R: $v[1]"; + $r->{avg_day}{VAL} = "T: $v[4] H: $v[3] W: $v[2] R: $v[1]"; + + } else { + + my @cv = split(" ", $r->{cum_day}{VAL}); + + my @cd = split("[ :-]", $r->{cum_day}{TIM}); + my $csec = 3600*$cd[3] + 60*$cd[4] + $cd[5]; # Sec of last reading + + my @d = split("[ :-]", $tm); + my $sec = 3600*$d[3] + 60*$d[4] + $d[5]; # Sec now + + my @sd = split("[ :-]", "$cv[0] $cv[1]"); + my $ssec = 3600*$sd[3] + 60*$sd[4] + $sd[5]; # Sec at start of day + + my $difft = $sec - $csec; + $difft += 86400 if($d[2] != $cd[2]); # Sec since last reading + + my $t = $cv[3] + $difft * $v[4]; + my $h = $cv[5] + $difft * $v[3]; + my $w = $cv[7] + $difft * $v[2]; + my $e = $cv[9]; + + $r->{cum_day}{VAL} = "$cv[0] $cv[1] T: $t H: $h W: $w R: $e"; + + $difft = $sec - $ssec; + $difft += 86400 if($d[2] != $sd[2]); # Sec since last reading + + $t /= $difft; $h /= $difft; $w /= $difft; $e = $v[1] - $cv[9]; + $r->{avg_day}{VAL} = + sprintf("T: %.1f H: %d W: %.1f R: %.1f", $t, $h, $w, $e); + + if($d[2] != $sd[2]) { # Day changed, report it + + $def->{CHANGED}[$max++] = "avg_day $r->{avg_day}{VAL}"; + $r->{cum_day}{VAL} = "$tm T: 0 H: 0 W: 0 R: $v[1]"; + + if(!$r->{cum_month}) { # Check the month + + $r->{cum_month}{VAL} = "1 $r->{avg_day}{VAL}"; + $r->{avg_month}{VAL} = $r->{avg_day}{VAL}; + + } else { + + my @cmv = split(" ", $r->{cum_month}{VAL}); + $t += $cmv[2]; $w += $cmv[4]; $h += $cmv[6]; + + $cmv[0]++; + $r->{cum_month}{VAL} = + sprintf("%d T: %.1f H: %d W: %.1f R: %.1f", + $cmv[0], $t, $h, $w, $cmv[8]+$e); + $r->{avg_month}{VAL} = + sprintf("T: %.1f H: %d W: %.1f R: %.1f", + $t/$cmv[0], $h/$cmv[0], $w/$cmv[0], $cmv[8]+$e); + + if($d[1] != $sd[1]) { # Month changed, report it + + $def->{CHANGED}[$max++] = "avg_month $r->{avg_month}{VAL}"; + $r->{cum_month}{VAL} = "0 T: 0 H: 0 W: 0 R: 0"; + + } + + } + $r->{cum_month}{TIM} = $r->{avg_month}{TIM} = $tm; + + } + + } + $r->{cum_day}{TIM} = $r->{avg_day}{TIM} = $tm; + # AVG computing + ################################### + + return $def->{NAME}; + + } else { + + Log 4, "KS300 detected: $msg"; + + } + + return ""; +} + +1; diff --git a/FHEM/50_WS300.pm b/FHEM/50_WS300.pm new file mode 100644 index 000000000..d16628766 --- /dev/null +++ b/FHEM/50_WS300.pm @@ -0,0 +1,698 @@ +################################################################ +# +# Copyright notice +# +# (c) 2007 Copyright: Martin Klerx (Martin at klerx dot de) +# All rights reserved +# +# This script 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! +# +################################################################ +# examples: +# define WS300Device WS300 /dev/ttyUSB1 (fixed name, must be first) +# define ash2200-1 WS300 0 +# define ash2200-2 WS300 1 +# ... +# define ash2200-8 WS300 7 +# define ks300 WS300 8 (always 8) +# define ws300 WS300 9 (always 9) +# set WS300Device +################################################################ + +package main; + +use strict; +use warnings; + +my %readings; +my %defptr; +my $DeviceName=""; +my $inbuf=""; + +my $config; +my $cmd=0x32; +my $errcount=0; +my $ir="no"; +my $willi=0; +my $oldwind=0.0; +my $polling=0; +my $acthour=99; +my $actday=99; +my $actmonth=99; +my $oldrain=0; +my $rain_hour=0; +my $rain_day=0; +my $rain_month=0; +##################################### +sub +WS300_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category} = "DEV"; + + # Provider + $hash->{Clients} = ":WS300:"; + $hash->{ReadFn} = "WS300_Read"; + $hash->{WriteFn} = "WS300_Write"; + $hash->{Type} = "FHZ1000"; + $hash->{Match} = "^WS300.*"; + $hash->{SetFn} = "WS300_Set"; + $hash->{GetFn} = "WS300_Get"; + $hash->{StateFn} = "WS300_SetState"; + $hash->{ListFn} = "WS300_List"; + $hash->{DefFn} = "WS300_Define"; + $hash->{UndefFn} = "WS300_Undef"; + $hash->{ParseFn} = "WS300_Parse"; + $hash->{ReadFn} = "WS300_Read"; +} + +################################### +sub +WS300_Set($@) +{ + my ($hash, @a) = @_; + if($hash->{NAME} eq "WS300Device") + { + return "wrong syntax: set WS300Device " if(int(@a) < 4 || int($a[1]) < 5 || int($a[1]) > 60 || int($a[2]) > 2000); + my $bstring = sprintf("%c%c%c%c%c%c%c%c",0xfe,0x30,(int($a[1])&0xff),((int($a[2])>>8)&0xff),(int($a[2])&0xff),((int($a[3])>>8)&0xff),(int($a[3])&0xff),0xfc); + $hash->{PortObj}->write($bstring); + Log 1,"WS300 synchronization started (".unpack('H*',$bstring).")"; + return "the ws300pc will now synchronize for 10 minutes"; + } + return "No set function implemented"; +} + +################################### +sub +WS300_Get(@) +{ + my ($hash, @a) = @_; + if($hash->{NAME} eq "WS300Device") + { + Log 5,"WS300_Get $a[0] $a[1]"; + WS300_Poll($hash); + return undef; + } + return "No get function implemented"; +} + +##################################### +sub +WS300_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + + return undef if(!defined($hash->{SENSOR})); + + my $n = $hash->{SENSOR}; + if(!$readings{$n}{$vt} || $readings{$n}{$vt}{TIM} lt $tim) { + $readings{$n}{$vt}{TIM} = $tim; + $readings{$n}{$vt}{VAL} = $val; + } + return undef; +} + +##################################### +sub +WS300_List($) +{ + my ($hash) = @_; + my $str = ""; + + return "No information about $hash->{NAME}" if(!defined($hash->{SENSOR})); + my $n = $hash->{SENSOR}; + if(!defined($readings{$n})) + { + $str .= "No information about " . $hash->{NAME} . "\n"; + } + else + { + foreach my $m (keys %{ $readings{$n} }) + { + $str .= sprintf("%-19s %-15s %s\n",$readings{$n}{$m}{TIM}, $m, $readings{$n}{$m}{VAL}); + } + } + return $str; + +} + +##################################### +sub +WS300_Define($@) +{ + my ($hash, @a) = @_; + + if($a[0] eq "WS300Device") + { + return "wrong syntax: define WS300Device WS300 " if(int(@a) < 3); + $DeviceName = $a[2]; + $hash->{STATE} = "Initializing"; + $hash->{SENSOR} = 10; + $readings{10}{WS300Device}{VAL} = "Initializing"; + $readings{10}{WS300Device}{TIM} = TimeNow; + my $po = new Device::SerialPort ($a[2]); + if(!$po) + { + $hash->{STATE} = "error opening device"; + $readings{10}{WS300Device}{VAL} = "error opening device"; + $readings{10}{WS300Device}{TIM} = TimeNow; + Log 1,"Error opening WS300 Device $a[2]"; + return "Can't open $a[2]: $!\n"; + } + $po->reset_error(); + $po->baudrate(19200); + $po->databits(8); + $po->parity('even'); + $po->stopbits(1); + $po->handshake('none'); + $po->rts_active(1); + $po->dtr_active(1); + sleep(1); + $po->rts_active(0); + $hash->{PortObj} = $po; + $hash->{DeviceName} = $a[2]; + $hash->{STATE} = "opened"; + $readings{10}{WS300Device}{VAL} = "opened"; + $readings{10}{WS300Device}{TIM} = TimeNow; + CommandAt($hash,"+*00:00:05 get WS300Device data"); + Log 1,"WS300 Device $a[2] opened"; + return undef; + } + return "wrong syntax: define WS300 \n0-7=ASH2200\n8=KS300\n9=WS300" if(int(@a) < 3); + return "no device: define WS300Device WS300 first" if($DeviceName eq ""); + $a[2] = lc($a[2]); + return "Define $a[0]: wrong sensor number." if($a[2] !~ m/^[0-9]$/); + $hash->{SENSOR} = $a[2]; + $defptr{$a[2]} = $hash; + + return undef; +} + +##################################### +sub +WS300_Undef($$) +{ + my ($hash, $name) = @_; + return undef if(!defined($hash->{SENSOR})); + delete($defptr{$hash->{SENSOR}}); + return undef; +} + + +##################################### +sub +WS300_Parse($) +{ + my $msg = shift; + my $ll = GetLogLevel("WS300Device"); + $ll = 5 if($ll == 2); + + my @c = split("", $config); + my @cmsg = split("",unpack('H*',$config)); + my $dmsg = unpack('H*',$msg); + my @a = split("", $dmsg); + my $val = ""; + my $tm; + my $h; + my $t; + my $b; + my $l; + my $value; + my $offs=0; + my $ref; + my $def; + my $zeit; + my @txt = ( "temperature", "humidity", "wind", "rain_raw", "israining", "battery", "lost_receives", "pressure", "rain_cum", "rain_hour", "rain_day", "rain_month"); + my @sfx = ( "(Celsius)", "(%)", "(km/h)", "(counter)", "(yes/no)", " ", "(counter)", "(hPa)", "(mm)", "(mm)", "(mm)", "(mm)"); + # 1 2 3 4 5 6 7 8 + # 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 + # 3180800001005d4e00000000000000000000000000000000000000000000594a0634001e00f62403f1fc stored + # aaaatttthhtttthhtttthhtttthhtttthhtttthhtttthhtttthhtttthhrrrrwwwwtttthhpppp + # 3300544a0000000000000000000000000000000000000000000057470634002c00f32303ee32fc current + # tttthhtttthhtttthhtttthhtttthhtttthhtttthhtttthhtttthhrrrrwwwwtttthhppppss + # 3210000000000000001005003a0127fc config + # 001122334455667788iihhhhmmmm + $offs = 2 if(hex($a[0].$a[1]) == 0x33); + $offs = 10 if(hex($a[0].$a[1]) == 0x31); + if($offs == 0) + { + Log 1,"WS300 illegal data in WS300_Parse"; + return undef; + } + $zeit = time; + my $wind = hex($a[58+$offs].$a[59+$offs].$a[60+$offs].$a[61+$offs]); + $wind /= 10.0; + if(hex($a[0].$a[1]) == 0x33) + { + return undef if(hex($a[74].$a[75]) == $willi && $wind == $oldwind ); + $willi = hex($a[74].$a[75]); + $ir="no"; + $ir="yes" if(($willi&0x80)); + } + else + { + $zeit -= (hex($a[6].$a[7].$a[8].$a[9])*60); + } + my @lt = localtime($zeit); + $tm = sprintf("%04d-%02d-%02d %02d:%02d:%02d",$lt[5]+1900, $lt[4]+1, $lt[3], $lt[2], $lt[1], $lt[0]); + $oldwind = $wind; + my $press = hex($a[68+$offs].$a[69+$offs].$a[70+$offs].$a[71+$offs]); + my $hpress = hex($cmsg[22].$cmsg[23].$cmsg[24].$cmsg[25]); + $hpress /= 8.5; + $press += $hpress; + $press = sprintf("%.1f",$press); + my $rainc = hex($a[54+$offs].$a[55+$offs].$a[56+$offs].$a[57+$offs]); + my $rain = hex($cmsg[26].$cmsg[27].$cmsg[28].$cmsg[29]); + $rain *= $rainc; + $rain /= 1000; + $rain = sprintf("%.1f",$rain); + for(my $s=0;$s<9;$s++) + { + if((ord($c[$s+1])&0x10)) + { + my $p=($s*6)+$offs; + Log $ll,"Sensor $s vorhanden"; + if(!defined($defptr{$s})) + { + Log(3,"WS300 $s: undefined"); + } + else + { + $readings{$s}{$txt[0]}{VAL} = 0 if(!$readings{$s}); + $ref = $readings{$s}; + $def = $defptr{$s}; + $t = hex($a[$p].$a[$p+1].$a[$p+2].$a[$p+3]); + $t -= 65535 if( $t > 32767 ); + $t /= 10.0; + $h = hex($a[$p+4].$a[$p+5]); + if((ord($c[$s+1])&0xe0)) + { + $b = "Empty" + } + else + { + $b = "Ok" + } + $l = (ord($c[$s+1])&0x0f); + if($s < 8) + { + # state + $val = "T: $t H: $h Bat: $b LR: $l"; + $def->{STATE} = $val; + $def->{CHANGED}[0] = $val; + $def->{CHANGETIME}[0] = $tm; + # temperatur + $ref->{$txt[0]}{TIM} = $tm; + $value = "$t $sfx[0]"; + $ref->{$txt[0]}{VAL} = $value; + $def->{CHANGED}[1] = "$txt[0]: $value"; + $def->{CHANGETIME}[1] = $tm; + # humidity + $ref->{$txt[1]}{TIM} = $tm; + $value = "$h $sfx[1]"; + $ref->{$txt[1]}{VAL} = $value; + $def->{CHANGED}[2] = "$txt[1]: $value"; + $def->{CHANGETIME}[2] = $tm; + # battery + $ref->{$txt[5]}{TIM} = $tm; + $value = "$b $sfx[5]"; + $ref->{$txt[5]}{VAL} = $value; + $def->{CHANGED}[3] = "$txt[5]: $value"; + $def->{CHANGETIME}[3] = $tm; + # lost receives + $ref->{$txt[6]}{TIM} = $tm; + $value = "$l $sfx[6]"; + $ref->{$txt[6]}{VAL} = $value; + $def->{CHANGED}[4] = "$txt[6]: $value"; + $def->{CHANGETIME}[4] = $tm; + + Log 2,"WS300 $def->{NAME}: $val"; + DoTrigger($def->{NAME},undef); + } + else + { + # state + $val = "T: $t H: $h W: $wind R: $rain IR: $ir Bat: $b LR: $l"; + $def->{STATE} = $val; + $def->{CHANGED}[0] = $val; + $def->{CHANGETIME}[0] = $tm; + # temperature + $ref->{$txt[0]}{TIM} = $tm; + $value = "$t $sfx[0]"; + $ref->{$txt[0]}{VAL} = $value; + $def->{CHANGED}[1] = "$txt[0]: $value"; + $def->{CHANGETIME}[1] = $tm; + # humidity + $ref->{$txt[1]}{TIM} = $tm; + $value = "$h $sfx[1]"; + $ref->{$txt[1]}{VAL} = $value; + $def->{CHANGED}[2] = "$txt[1]: $value"; + $def->{CHANGETIME}[2] = $tm; + # wind + $ref->{$txt[2]}{TIM} = $tm; + $value = "$wind $sfx[2]"; + $ref->{$txt[2]}{VAL} = $value; + $def->{CHANGED}[3] = "$txt[2]: $value"; + $def->{CHANGETIME}[3] = $tm; + #rain counter + $ref->{$txt[3]}{TIM} = $tm; + $value = "$rainc $sfx[3]"; + $ref->{$txt[3]}{VAL} = $value; + $def->{CHANGED}[4] = "$txt[3]: $value"; + $def->{CHANGETIME}[4] = $tm; + # is raining + $ref->{$txt[4]}{TIM} = $tm; + $value = "$ir $sfx[4]"; + $ref->{$txt[4]}{VAL} = $value; + $def->{CHANGED}[5] = "$txt[4]: $value"; + $def->{CHANGETIME}[5] = $tm; + # battery + $ref->{$txt[5]}{TIM} = $tm; + $value = "$b $sfx[5]"; + $ref->{$txt[5]}{VAL} = $value; + $def->{CHANGED}[6] = "$txt[5]: $value"; + $def->{CHANGETIME}[6] = $tm; + # lost receives + $ref->{$txt[6]}{TIM} = $tm; + $value = "$l $sfx[6]"; + $ref->{$txt[6]}{VAL} = $value; + $def->{CHANGED}[7] = "$txt[6]: $value"; + $def->{CHANGETIME}[7] = $tm; + # rain cumulative + $ref->{$txt[8]}{TIM} = $tm; + $value = "$rain $sfx[8]"; + $ref->{$txt[8]}{VAL} = $value; + $def->{CHANGED}[8] = "$txt[8]: $value"; + $def->{CHANGETIME}[8] = $tm; + # statistics + if($actday == 99) + { + $oldrain = $rain; + $acthour = $ref->{acthour}{VAL} if(defined($ref->{acthour}{VAL})); + $actday = $ref->{actday}{VAL} if(defined($ref->{actday}{VAL})); + $actmonth = $ref->{actmonth}{VAL} if(defined($ref->{actmonth}{VAL})); + $rain_day = $ref->{rain_day}{VAL} if(defined($ref->{rain_day}{VAL})); + $rain_month = $ref->{rain_month}{VAL} if(defined($ref->{rain_month}{VAL})); + $rain_hour = $ref->{rain_hour}{VAL} if(defined($ref->{rain_hour}{VAL})); + } + if($acthour != $lt[2]) + { + $acthour = $lt[2]; + $rain_hour = sprintf("%.1f",$rain_hour); + $rain_day = sprintf("%.1f",$rain_day); + $rain_month = sprintf("%.1f",$rain_month); + $ref->{acthour}{TIM} = $tm; + $ref->{acthour}{VAL} = "$acthour"; + $ref->{$txt[9]}{TIM} = $tm; + $ref->{$txt[9]}{VAL} = $rain_hour; + $def->{CHANGED}[9] = "$txt[9]: $rain_hour $sfx[9]"; + $def->{CHANGETIME}[9] = $tm; + $ref->{$txt[10]}{TIM} = $tm; + $ref->{$txt[10]}{VAL} = $rain_day; + $def->{CHANGED}[10] = "$txt[10]: $rain_day $sfx[10]"; + $def->{CHANGETIME}[10] = $tm; + $ref->{$txt[11]}{TIM} = $tm; + $ref->{$txt[11]}{VAL} = $rain_month; + $def->{CHANGED}[11] = "$txt[11]: $rain_month $sfx[11]"; + $def->{CHANGETIME}[11] = $tm; + $rain_hour=0; + } + if($actday != $lt[3]) + { + $actday = $lt[3]; + $ref->{actday}{TIM} = $tm; + $ref->{actday}{VAL} = "$actday"; + $rain_day=0; + } + if($actmonth != $lt[4]+1) + { + $actmonth = $lt[4]+1; + $ref->{actmonth}{TIM} = $tm; + $ref->{actmonth}{VAL} = "$actmonth"; + $rain_month=0; + } + if($rain != $oldrain) + { + $rain_hour += ($rain-$oldrain); + $rain_hour = sprintf("%.1f",$rain_hour); + $rain_day += ($rain-$oldrain); + $rain_day = sprintf("%.1f",$rain_day); + $rain_month += ($rain-$oldrain); + $rain_month = sprintf("%.1f",$rain_month); + $oldrain = $rain; + + $ref->{acthour}{TIM} = $tm; + $ref->{acthour}{VAL} = "$acthour"; + $ref->{$txt[9]}{TIM} = $tm; + $ref->{$txt[9]}{VAL} = $rain_hour; + $def->{CHANGED}[9] = "$txt[9]: $rain_hour $sfx[9]"; + $def->{CHANGETIME}[9] = $tm; + $ref->{$txt[10]}{TIM} = $tm; + $ref->{$txt[10]}{VAL} = $rain_day; + $def->{CHANGED}[10] = "$txt[10]: $rain_day $sfx[10]"; + $def->{CHANGETIME}[10] = $tm; + $ref->{$txt[11]}{TIM} = $tm; + $ref->{$txt[11]}{VAL} = $rain_month; + $def->{CHANGED}[11] = "$txt[11]: $rain_month $sfx[11]"; + $def->{CHANGETIME}[11] = $tm; + } + Log 2,"WS300 $def->{NAME}: $val"; + DoTrigger($def->{NAME},undef); + } + } + } + } + if(!defined($defptr{9})) + { + Log(3,"WS300 9: undefined"); + } + else + { + $readings{9}{$txt[0]}{VAL} = 0 if(!$readings{9}); + $ref = $readings{9}; + $def = $defptr{9}; + $t = hex($a[62+$offs].$a[63+$offs].$a[64+$offs].$a[65+$offs]); + $t -= 65535 if( $t > 32767 ); + $t /= 10.0; + $h = hex($a[66+$offs].$a[67+$offs]); + # state + $val = "T: $t H: $h P: $press Willi: $willi"; + $def->{STATE} = $val; + $def->{CHANGED}[0] = $val; + $def->{CHANGETIME}[0] = $tm; + # temperature + $ref->{$txt[0]}{TIM} = $tm; + $value = "$t $sfx[0]"; + $ref->{$txt[0]}{VAL} = $value; + $def->{CHANGED}[1] = "$txt[0]: $value"; + $def->{CHANGETIME}[1] = $tm; + # humidity + $ref->{$txt[1]}{TIM} = $tm; + $value = "$h $sfx[1]"; + $ref->{$txt[1]}{VAL} = $value; + $def->{CHANGED}[2] = "$txt[1]: $value"; + $def->{CHANGETIME}[2] = $tm; + # pressure + $ref->{$txt[7]}{TIM} = $tm; + $value = "$press $sfx[7]"; + $ref->{$txt[7]}{VAL} = $value; + $def->{CHANGED}[3] = "$txt[7]: $value"; + $def->{CHANGETIME}[3] = $tm; + # willi + $ref->{willi}{TIM} = $tm; + $value = "$willi"; + $ref->{willi}{VAL} = $value; + $def->{CHANGED}[4] = "willi: $value"; + $def->{CHANGETIME}[4] = $tm; + + Log 2,"WS300 $def->{NAME}: $val"; + DoTrigger($def->{NAME},undef); + } + return undef; +} +##################################### +sub +WS300_Read($) +{ + my ($hash) = @_; +} +##################################### +sub +WS300_Write($$$) +{ + my ($hash,$fn,$msg) = @_; +} + +##################################### +sub +WS300_Poll($) +{ + my $hash = shift; + my $bstring=" "; + my $count; + my $inchar=''; + my $escape=0; + my $ll = GetLogLevel("WS300Device"); + $ll = 5 if($ll == 2); + + if(!$hash || !defined($hash->{PortObj})) + { + return; + } + return if($polling); + $polling=1; +NEXTPOLL: + $inbuf = $hash->{PortObj}->input(); + $bstring = sprintf("%c%c%c",0xfe,$cmd,0xfc); + + my $ret = $hash->{PortObj}->write($bstring); + if($ret <= 0) + { + + my $devname = $hash->{DeviceName}; + Log 1, "USB device $devname disconnected, waiting to reappear"; + $hash->{PortObj}->close(); + $hash->{STATE} = "disconnected"; + $readings{10}{WS300Device}{VAL} = "disconnected"; + $readings{10}{WS300Device}{TIM} = TimeNow; + sleep(1); + my $po = new Device::SerialPort($devname); + if($po) + { + $po->reset_error(); + $po->baudrate(19200); + $po->databits(8); + $po->parity('even'); + $po->stopbits(1); + $po->handshake('none'); + $po->rts_active(1); + $po->dtr_active(1); + sleep(1); + $po->rts_active(0); + Log 1, "USB device $devname reappeared"; + $hash->{PortObj} = $po; + $hash->{STATE} = "opened"; + $readings{10}{WS300Device}{VAL} = "opened"; + $readings{10}{WS300Device}{TIM} = TimeNow; + $polling=0; + return; + } + } + $inbuf = ""; + my $start=0; + my $tout=time(); + my $rcount=0; + my $ic=0; + + for(;;) + { + ($count,$inchar) = $hash->{PortObj}->read(1); + if($count == 0) + { + last if($tout < time()); + } + else + { + $ic = hex(unpack('H*',$inchar)); + if(!$start) + { + if($ic == 0xfe) + { + $start = 1; + } + } + else + { + if($ic == 0xf8) + { + $escape = 1; + $count = 0; + } + else + { + if($escape) + { + $ic--; + $inbuf .= chr($ic); + $escape = 0; + } + else + { + $inbuf .= $inchar; + last if($ic == 0xfc); + } + } + } + $rcount += $count; + $tout=time(); + } + } + + Log($ll,"WS300/RAW: ".$rcount." ".unpack('H*',$inbuf)); + if($ic != 0xfc) + { + $errcount++ if($errcount < 10); + if($errcount == 10) + { + $hash->{STATE} = "timeout"; + $readings{10}{WS300Device}{VAL} = "timeout"; + $readings{10}{WS300Device}{TIM} = TimeNow; + $errcount++; + } + Log 1,"WS300: no data" if($rcount == 0); + Log 1,"WS300: wrong data ".unpack('H*',$inbuf) if($rcount > 0); + $polling=0; + return; + } + if($hash->{STATE} ne "connected" && $errcount > 10) + { + $hash->{STATE} = "connected"; + $readings{10}{WS300Device}{VAL} = "connected"; + $readings{10}{WS300Device}{TIM} = TimeNow; + } + $errcount = 0; + $ic = ord(substr($inbuf,0,1)); + if($ic == 0x32) + { + $config = $inbuf if($rcount == 16); + $cmd=0x31; + goto NEXTPOLL; + } + if($ic == 0x31) + { + if($rcount == 42) + { + WS300_Parse($inbuf); + goto NEXTPOLL; + } + else + { + $cmd=0x33; + goto NEXTPOLL; + } + } + if($ic == 0x33) + { + WS300_Parse($inbuf) if($rcount == 39); + $cmd=0x32; + } + $polling=0; +} + +1; diff --git a/FHEM/90_FileLog.pm b/FHEM/90_FileLog.pm new file mode 100755 index 000000000..6ddfec9a9 --- /dev/null +++ b/FHEM/90_FileLog.pm @@ -0,0 +1,97 @@ +############################################## +package main; + +use strict; +use warnings; +use IO::File; + +##################################### +sub +FileLog_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category}= "LOG"; + + $hash->{DefFn} = "FileLog_Define"; + $hash->{UndefFn} = "FileLog_Undef"; + $hash->{LogFn} = "FileLog_Log"; +} + + +##################################### +sub +FileLog_Define($@) +{ + my ($hash, @a) = @_; + my $fh; + + return "wrong syntax: define FileLog filename regexp" if(int(@a) != 4); + + eval { "Hallo" =~ m/^$a[3]$/ }; + return "Bad regexp: $@" if($@); + + my @t = localtime; + my $f = ResolveDateWildcards($a[2], @t); + $fh = new IO::File ">>$f"; + return "Can't open $f" if(!defined($fh)); + + $hash->{FH} = $fh; + $hash->{REGEXP} = $a[3]; + $hash->{FILENAME} = $a[2]; + $hash->{CURRENT} = $f; + + return undef; +} + +##################################### +sub +FileLog_Undef($$) +{ + my ($hash, $name) = @_; + close($hash->{FH}); + return undef; +} + + +##################################### +sub +FileLog_Log($$) +{ + my ($log, $dev) = @_; + + my $n = $dev->{NAME}; + my $re = $log->{REGEXP}; + my $max = int(@{$dev->{CHANGED}}); + + for (my $i = 0; $i < $max; $i++) { + my $s = $dev->{CHANGED}[$i]; + $s = "" if(!defined($s)); + if($n =~ m/^$re$/ || "$n:$s" =~ m/^$re$/) { + my $t = TimeNow(); + $t = $dev->{CHANGETIME}[$i] if(defined($dev->{CHANGETIME}[$i])); + $t =~ s/ /_/; + + my $fh = $log->{FH}; + my @t = localtime; + my $cn = ResolveDateWildcards($log->{FILENAME}, @t); + + if($cn ne $log->{CURRENT}) { # New logfile + $fh->close(); + $fh = new IO::File ">>$cn"; + if(!defined($fh)) { + Log(0, "Can't open $cn"); + return; + } + $log->{CURRENT} = $cn; + $log->{FH} = $fh; + } + + print $fh "$t $n $s\n"; + $fh->flush; + $fh->sync; + } + } +} + +1; diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..c8ca87c2b --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +BINDIR=/usr/local/bin +MODDIR=/usr/local/lib + +VERS=3.3 +DATE=2006-01-25 +DIR=fhem-$(VERS) + +all: + @echo Nothing to do for all. + @echo To install, check the Makefile, and then \'make install\' + +install: + cp fhem.pl $(BINDIR) + cp -rp FHEM $(MODDIR) + perl -pi -e 's,modpath .,modpath $(MODDIR),' examples/* + +dist: + @echo Version is $(VERS), Date is $(DATE) + mkdir .f + cp -rp * .f + find .f -name \*.orig -print | xargs rm -f + find .f -type f -print |\ + xargs perl -pi -e 's/=VERS=/$(VERS)/g;s/=DATE=/$(DATE)/g' + mv .f fhem-$(VERS) + tar cf - fhem-$(VERS) | gzip > fhem-$(VERS).tar.gz + mv fhem-$(VERS)/docs/*.html . + rm -rf fhem-$(VERS) diff --git a/contrib/91_DbLog.pm b/contrib/91_DbLog.pm new file mode 100755 index 000000000..2aaa4794a --- /dev/null +++ b/contrib/91_DbLog.pm @@ -0,0 +1,99 @@ +############################################## +# Example for logging KS300 data into a DB. +# +# Prerequisites: +# - The DBI and the DBD:: modules must be installed. +# - a Database is created/configured +# - a db table: create table FHZLOG (TIMESTAMP varchar(20), TEMP varchar(5), +# HUM varchar(3), WIND varchar(4), RAIN varchar(8)); +# - Change the content of the dbconn variable below +# - extend your FHEM config file with +# notify .*H:.* {DbLog("@","%")} +# - copy this file into the /FHEM and restart fhem.pl +# +# If you want to change this setup, your starting point is the DbLog function + +my $dbconn = "Oracle:DBNAME:user:password"; + +package main; +use strict; +use warnings; +use DBI; + +my $dbh; + +sub DbDo($); +sub DbConnect(); + + +################################################################ +sub +DbLog_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category} = "none"; + + # Lets connect here, so we see the error at startup + DbConnect(); +} + +################################################################ +sub +DbLog($$) +{ + my ($a1, $a2) = @_; + + # a2 is like "T: 21.2 H: 37 W: 0.0 R: 0.0 IR: no" + my @a = split(" ", $a2); + my $tm = TimeNow(); + + DbDo("insert into FHZLOG (TIMESTAMP, TEMP, HUM, WIND, RAIN) values " . + "('$tm', '$a[1]', '$a[3]', '$a[5]', '$a[7]')"); +} + + +################################################################ +sub +DbConnect() +{ + return 1 if($dbh); + Log 5, "Connecting to database $dbconn"; + my @a = split(":", $dbconn); + $dbh = DBI->connect("dbi:$a[0]:$a[1]", $a[2], $a[3]); + if(!$dbh) { + Log 1, "Can't connect to $a[1]: $DBI::errstr"; + return 0; + } + Log 5, "Connection to db $a[1] established"; + return 1; +} + +################################################################ +sub +DbDo($) +{ + my $str = shift; + + return 0 if(!DbConnect()); + Log 5, "Executing $str"; + my $sth = $dbh->do($str); + if(!$sth) { + Log 2, "DB: " . $DBI::errstr; + $dbh->disconnect; + $dbh = 0; + return 0 if(!DbConnect()); +#retry + $sth = $dbh->do($str); + if($sth) + { + Log 2, "Retry ok: $str"; + return 1; + } +# + return 0; + } + return 1; +} + +1; diff --git a/contrib/99_ALARM.pm b/contrib/99_ALARM.pm new file mode 100755 index 000000000..3fbb3470f --- /dev/null +++ b/contrib/99_ALARM.pm @@ -0,0 +1,153 @@ +############################################## +# Low Budget ALARM System +############################################## +# ATTENTION! This is more a toy than a real alarm system! You must know what you do! +############################################## +# +# Concept: +# 1x Signal Light (FS20 allight) to show the status (activated/deactivated) +# 1x Sirene (FS20 alsir1) +# 2x PIRI-2 (FS20 piriu pirio) +# 1x Sender (FS20 alsw) to activate/deactivate the system. +# Tip: use the KeyMatic CAC with pin code +# optional a normal sender (not a Keymatic CAC) FS20 alsw2 +# +# Add something like the following lines to the configuration file : +# notifyon alsw {MyAlsw()} +# notifyon alsw2 {MyAlswNoPin()} +# notifyon piriu {MyAlarm()} +# notifyon pirio {MyAlarm()} +# and put this file in the /FHEM directory. +# +# Martin Haas +############################################## + + +package main; +use strict; +use warnings; + +sub +ALARM_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category} = "none"; +} + + +############################################## +# Switching Alarm System on or off +sub +MyAlsw() +{ + my $ON="set allight on; setstate alsw on"; + my $OFF1="set allight off"; + my $OFF2="set alsir1 off"; + my $OFF3="setstate alsw off"; + + if ( -e "/var/tmp/alertsystem") + { + unlink "/var/tmp/alertsystem"; + for (my $i = 0; $i < 2; $i++ ) + { + fhz "$OFF1"; + fhz "$OFF2"; + fhz "$OFF3"; + }; + Log 2, "alarm system is OFF"; + } else { + system "touch /var/tmp/alertsystem"; + for (my $i = 0; $i < 2; $i++ ) + { + fhz "$ON" + } + Log 2, "alarm system is ON"; + }; +} + + +############################################## +# If you have no Keymatic then use this workaround: +# After 4x pushing a fs20-button within some seconds it will activate/deactivate the alarm system. +sub +MyAlswNoPin() +{ + +my $timedout=5; + +## first time + if ( ! -e "/var/tmp/alontest1") + { + for (my $i = 1; $i < 4; $i++ ) + { + system "touch /var/tmp/alontest$i"; + system "touch -t 200601010101 /var/tmp/alontest$i"; + } + } + + +## test 4 times + my $now= `date +%s`; + for (my $i = 1; $i < 4; $i++ ) + { + my $tagx=`date -r /var/tmp/alontest$i +%s`; + my $testx=$now-$tagx; + + if ( $testx > $timedout ) + { + system "touch /var/tmp/alontest$i"; + Log 2, "test$i: more than $timedout sec\n"; + die; + } + } + system "touch -t 200601010101 /var/tmp/alontest*"; + Log 2, "ok, let's switch the alarm system..."; + +#if you only allow to activate (and not deactivate) with this script: +# if ( -e "/var/tmp/alertsystem") { die; }; + + MyAlsw(); +} + + + + +############################################## +# ALARM! Do what you want! +sub +MyAlarm() +{ + + #alarm-system activated?? + if ( -e "/var/tmp/alertsystem") + { + + + my $timer=180; # time until the sirene will be quit + my $ON1="set alsir1 on-for-timer $timer"; + + + #Paranoia + for (my $i = 1; $i < 3; $i++ ) + { + fhz "$ON1"; + } + Log 2, "ALARM! $ON1" ; + + + # have fun + my @lights=("stuwz1", "stuwz2", "stunacht", "stonacht", "stoliba"); + my @rollos=("rolu4", "rolu5", "roloadi", "rololeo", "roloco", "rolowz", "rolunik1", "rolunik2"); + + foreach my $light (@lights) { + fhz "set $light on" + } + + foreach my $rollo (@rollos) { + fhz "set $rollo on" + } + } +} + +1; diff --git a/contrib/99_PRIV.pm b/contrib/99_PRIV.pm new file mode 100755 index 000000000..9a9973052 --- /dev/null +++ b/contrib/99_PRIV.pm @@ -0,0 +1,33 @@ +############################################## +# Example for notifying with perl-code, in a proper file, not inline. +# Add the following line to the configuration file (02_fs20): +# notifyon btn3 {MyFunc("@", "%")} +# and put this file in the /FHEM directory. + +package main; +use strict; +use warnings; + +sub +PRIV_Initialize($$) +{ + my ($hash, $init) = @_; + $hash->{Category} = "none"; +} + +sub +MyFunc($$) +{ + my ($a1, $a2) = @_; + + Log 2, "Device $a1 was set to $a2 (type: $defs{$a1}{TYPE})"; + if($a2 eq "on") { + fhz "roll1 on-for-timer 10"; + fhz "roll2 on-for-timer 16"); + } else { + fhz "roll1 off"; + fhz "roll2 off"; + } +} + +1; diff --git a/contrib/99_SUNRISE.pm b/contrib/99_SUNRISE.pm new file mode 100755 index 000000000..f26c22b06 --- /dev/null +++ b/contrib/99_SUNRISE.pm @@ -0,0 +1,90 @@ +############################################## +# - Be aware: Installing the DateTime modules might be tedious, one way is: +# perl -MCPAN -e shell +# cpan> install DateTime::Event::Sunrise +# - Please call sunrise_coord before using this module, else you'll get times +# for frankfurt am main (germany). See the "at" entry in commandref.html + +package main; +use strict; +use warnings; + +use DateTime; +use DateTime::Event::Sunrise; + +sub sr($$$$); +sub sunrise_rel(@); +sub sunset_rel(@); +sub sunrise_abs(@); +sub sunset_abs(@); +sub isday(); +sub sunrise_coord($$$); +sub SUNRISE_Initialize($); + +# See perldoc DateTime::Event::Sunrise for details +my $long = "8.686"; +my $lat = "50.112"; +my $tz = "Europe/Berlin"; + +sub +SUNRISE_Initialize($) +{ + my ($hash) = @_; + + $hash->{Category} = "none"; +} + + +########################## +# Compute: +# rise: 1: event is sunrise (else sunset) +# isrel: 1: _relative_ times until the next event (else absolute for today) +# seconds: second offset to event +# daycheck: if set, then return 1 if the sun is visible, 0 else +sub +sr($$$$) +{ + my ($rise, $seconds, $isrel, $daycheck) = @_; + + my $sunrise = DateTime::Event::Sunrise ->new( + longitude => $long, + latitude => $lat, + altitude => '-6', # Civil twilight + iteration => '3'); + my $now = DateTime->now(time_zone => $tz); + my $stm = ($rise ? $sunrise->sunrise_datetime( $now ) : + $sunrise->sunset_datetime( $now )); + + if($daycheck) { + return 0 if(DateTime->compare($now, $stm) < 0); + $stm = $sunrise->sunset_datetime( $now ); + return 0 if(DateTime->compare($now, $stm) > 0); + return 1; + } + + if(!$isrel) { + $stm = $stm->add(seconds => $seconds) if($seconds); + return $stm->hms(); + } + + $stm = $stm->add(seconds => $seconds) if($seconds); + + if(DateTime->compare($now, $stm) >= 0) { + my $tom = DateTime->now(time_zone => $tz)->add(days => 1); + $stm = ($rise ? $sunrise->sunrise_datetime( $tom ) : + $sunrise->sunset_datetime( $tom )); + $stm = $stm->add(seconds => $seconds) if($seconds); + } + + my $diff = $stm->epoch - $now->epoch; + return sprintf("%02d:%02d:%02d", $diff/3600, ($diff/60)%60, $diff%60); +} + +sub sunrise_rel(@) { return sr(1, shift, 1, 0) } +sub sunset_rel(@) { return sr(0, shift, 1, 0) } +sub sunrise_abs(@) { return sr(1, shift, 0, 0) } +sub sunset_abs(@) { return sr(0, shift, 0, 0) } +sub isday() { return sr(1, 0, 0, 1) } +sub sunrise_coord($$$) { ($long, $lat, $tz) = @_; return undef; } + +1; diff --git a/contrib/README b/contrib/README new file mode 100755 index 000000000..cec8ef0fd --- /dev/null +++ b/contrib/README @@ -0,0 +1,21 @@ +- 91_DbLog.pm + Example to log data in a (DBI supported) database (MySQL, Oracle, etc) +- 99_ALARM.pm + Example for a Low Budget ALARM System by Martin +- checkmsg.pl + Check cwthe CRC of an FS20 hex message +- fhem + RC script by Stefan to be put into /etc/init.d and then symlinked + to /etc/rc3.d or similar. +- four2hex + Convert housecode from ELV notation (4) to fhem.pl notation (hex) +- fs20_holidays.sh + STefan's "presence simulator" for holidays +- garden.pl + Garden irrigation regulator with weather dependency (KS300 temp + rain) +- ks300avg.pl + Computing daily/monthly avarage values from a KS300 log +- rolwzo_not_off.sh + Martin's "don't lock me out" program: look at the comment +- rrd + Peter's RRD support. See the HOWTO diff --git a/contrib/checkmsg.pl b/contrib/checkmsg.pl new file mode 100755 index 000000000..40574c4e6 --- /dev/null +++ b/contrib/checkmsg.pl @@ -0,0 +1,26 @@ +die("Usage: checkmsg HEX-FHZ-MESSAGE\n") if(int(@ARGV) != 1); +my $msg = $ARGV[0]; + +die("Bad prefix (not 0x81)\n") if($msg !~ m/^81/); +print("Prefix is ok (0x81)\n"); + +my $l = hex(substr($msg, 2, 2)); +my $rl = length($msg)/2-2; +die("Bad length $l (should be $rl)\n") if($rl != $l); +print("Length is ok ($l)\n"); + +my @data; +for(my $i = 8; $i < length($msg); $i += 2) { + push(@data, ord(pack('H*', substr($msg, $i, 2)))); +} + +my $rcrc = 0; +map { $rcrc += $_; } @data; +$rcrc &= 0xFF; + +my $crc = hex(substr($msg, 6, 2)); +my $str = sprintf("Bad CRC 0x%02x (should be 0x%02x)\n", $crc, $rcrc); +die($str) if($crc ne $rcrc); +printf("CRC is ok (0x%02x)\n", $crc); + +exit(0); diff --git a/contrib/crc.pl b/contrib/crc.pl new file mode 100755 index 000000000..7066c2811 --- /dev/null +++ b/contrib/crc.pl @@ -0,0 +1,33 @@ +die("Usage: crc HEX-MESSAGE\n") if(int(@ARGV) != 2); +my $msg = $ARGV[0]; +$msg =~ s/ //g; + +my $des = $ARGV[1]; +$des =~ s/ //g; + +# FFFF: 77 72 statt 2c 7f +# FFFF: 5C AC statt DC D9 + + +#for(my $ic = 0; $ic < 65536; $ic++) { +for(my $ic = 0; $ic < 2; $ic++) { + my $crc = ($ic == 0?0:0xffffffff); + for(my $i = 0; $i < length($msg); $i += 2) { + my $n = ord(pack('H*', substr($msg, $i, 2))); + + my $od = $n; + for my $b (0..7) { + my $crcbit = ($crc & 0x80000000) ? 1 : 0; + my $databit = ($n & 0x80) ? 1 : 0; + $crc <<= 1; + $n <<= 1; + $crc ^= 0x04C11DB7 if($crcbit != $databit); +# printf("%3d.%d %02x CRC %x ($crcbit $databit)\n", $i/2, $b, $n, $crc); + } +# printf("%3d %02x CRC %02x %02x\n", $i/2, $od, ($crc&0xff00)>>8, $crc&0xff); + } +# print "$ic\n" if($ic % 10000 == 0); + printf("%02x %02x\n",($crc&0xff00)>>8,$crc&0xff); + print "got $ic\n" + if(sprintf("%02x%02x",($crc&0xff00)>>8,$crc&0xff) eq $des); +} diff --git a/contrib/fht.gnuplot b/contrib/fht.gnuplot new file mode 100644 index 000000000..c97033dd7 --- /dev/null +++ b/contrib/fht.gnuplot @@ -0,0 +1,33 @@ +############################ +# Display the measured temperature and actuator data logged +# as described in the 04_log config file. +# Copy your logfile to fht.log and then call +# gnuplot fht.gnuplot +# (i.e. this file) +# Note: The webfrontend pgm2 and pgm3 does this for you. + + +########################### +# Uncomment the following if you want to create a postscript file +# and comment out the pause at the end +#set terminal postscript color "Helvetica" 11 +#set output 'saplogins-lrn.ps' + +set xdata time +set timefmt "%Y-%m-%d_%H:%M:%S" +set xlabel " " + +set ylabel "Temperature (Celsius)" +set y2label "Actuator (%)" +set ytics nomirror +set y2tics +set y2label "Actuator (%)" + +set title 'FHT log' +plot \ + "< awk '/measured/{print $1, $4}' fht.log"\ + using 1:2 axes x1y1 title 'Measured temperature' with lines,\ + "< awk '/actuator/{print $1, $4+0}' fht.log"\ + using 1:2 axes x1y2 title 'Actuator (%)' with lines\ + +pause 100000 diff --git a/contrib/four2hex/Makefile b/contrib/four2hex/Makefile new file mode 100644 index 000000000..9bf17bbb7 --- /dev/null +++ b/contrib/four2hex/Makefile @@ -0,0 +1,6 @@ +CC=gcc + +four2hex : four2hex.c + +install : four2hex + install -m 0755 four2hex /usr/local/bin/four2hex diff --git a/contrib/four2hex/README b/contrib/four2hex/README new file mode 100644 index 000000000..a25edab64 --- /dev/null +++ b/contrib/four2hex/README @@ -0,0 +1,23 @@ +Four2hex was written to convert the housecode based on digits ranging from 1 +to 4 into hex code and vica versa. +Four2hex is freeware based on the GNU Public license. + +To built it: +$ make four2hex + +Install it to /usr/local/bin: +$ su +# make install + + +Here an example from "four"-based to hex: +$ four2hex 12341234 +1b1b + +Here an example in the other (reverse) direction: +$ four2hex -r 1b1b +12341234 + +Enjoy. +Peter Stark, (Peter dot stark at t-online dot de) + diff --git a/contrib/four2hex/four2hex b/contrib/four2hex/four2hex new file mode 100755 index 000000000..57236bafe Binary files /dev/null and b/contrib/four2hex/four2hex differ diff --git a/contrib/four2hex/four2hex.c b/contrib/four2hex/four2hex.c new file mode 100644 index 000000000..a60349014 --- /dev/null +++ b/contrib/four2hex/four2hex.c @@ -0,0 +1,94 @@ +/* +Four2hex was written to convert the housecode based on digits ranging from 1 +to 4 into hex code and vica versa. +Four2hex is freeware based on the GNU Public license. + +To built it: +$ make four2hex + +Install it to /usr/local/bin: +$ su +# make install + + +Here an example from "four"-based to hex: +$ four2hex 12341234 +1b1b + +Here an example in the other (reverse) direction: +$ four2hex -r 1b1b +12341234 + +Enjoy. +Peter Stark, (Peter dot stark at t-online dot de) + +*/ + +#include +#include + +int atoh (const char c) +{ + int ret=0; + + ret = (int) (c - '0'); + if (ret > 9) { + ret = (int) (c - 'a' + 10); + } + return ret; +} + +int strlen(const char *); + +main (int argc, char **argv) +{ + char c, *s, *four; + long int result; + int b, i, h; + + if (argc < 2 || argc >3) { + fprintf (stderr, "usage: four2hex four-string\n"); + fprintf (stderr, " or: four2hex -r hex-string\n"); + return (1); + } + result = 0L; + if (strcmp(argv[1], "-r") == 0) { + /* reverse (hex->4) */ + for (s = argv[2]; *s != '\0'; s++) { + c = tolower(*s); + b = atoh(c); + for (i = 0; i < 2; i++) { + h = ((b & 0xc) >> 2) + 1; + b = (b & 0x3) << 2; + printf ("%d", h); + } + } + printf ("\n"); + } else { + /* normal (4->hex) */ + four = argv[1]; + if (strlen(four) == 4 || strlen(four) == 8) { + for (s = four; *s != '\0'; s++) { + result = result << 2; + switch (*s) { + case '1' : result = result + 0; break; + case '2' : result = result + 1; break; + case '3' : result = result + 2; break; + case '4' : result = result + 3; break; + default : + fprintf (stderr, "four-string may contain '1' to '4' only\n"); + break; + } + } + if (strlen(four) == 8) { + printf ("%04x\n", result); + } else { + printf ("%02x\n", result); + } + } else { + fprintf (stderr, "four-string must be of length 4 or 8\n"); + return (1); + } + } + return (0); +} diff --git a/contrib/fs20_holidays.sh b/contrib/fs20_holidays.sh new file mode 100755 index 000000000..e8ab01ba5 --- /dev/null +++ b/contrib/fs20_holidays.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# +# script to generate a random number of on/off events to simulate presence eg. +# while on holidays. normally this script would be executed by an event like a +# dawn-sensor (you wouldn't want light during the day...:-) +# +# Copyright STefan Mayer + +################## configuration ########################### +#number of events (min - max) +event_min=5 +event_max=20 + +#maximum delay in minutes +delay_max=240 + +#minimum and maximum ontime in minutes +ontime_min=5 +ontime_max=60 + +#devices to consider +declare -a devices='("dg.gang" "dg.wand" "dg.dusche" "dg.bad" "dg.reduit")' + +#output variant [oft|onoff] +#oft: use one at with on-for-timer of system +#onoff: use two at, one for on one for off +variant="onoff" + +#command to execute +#command_start="/opt/fhem/fhem.pl 7072 \"" +command_start="echo /opt/fhem/fhem.pl 7072 \"" +command_end="\"" + + +##################### Shouldnt need any changes below here ##################### + +# count number of devices +count=0 +for i in ${devices[*]} +do + ((count++)) +done +# echo $count + +# maximum random in bash: 32768 +random_max=32768 + +#number of events +event=$(($RANDOM * (($event_max - $event_min)) / $random_max +$event_min)) + +#initialize command +command=$command_start + +for ((i=0; i<$event; i++)) +do + #calculate starttime + starttime=$(($RANDOM * $delay_max / $random_max)) + hour=$(($starttime / 60)) + minute=$(($starttime % 60)) + second=$(($RANDOM * 60 / $random_max)) + + #calculate ontime + ontime=$(($RANDOM * (($ontime_max - $ontime_min)) / $random_max +$ontime_min)) + + #choose device + dev=$(($RANDOM * $count / $random_max)) + + case $variant in + oft) + printf "event %02d: at +%02d:%02d:%02d set %s on-for-timer %d\n" $i $hour $minute $second ${devices[$dev]} $ontime + command=`printf "$command at +%02d:%02d:%02d set %s on-for-timer %d;;" $hour $minute $second ${devices[$dev]} $ontime` + ;; + onoff) + offtime=$(($starttime + $ontime)) + hour_off=$(($offtime / 60)) + minute_off=$(($offtime % 60)) + second_off=$(($RANDOM * 60 / $random_max)) + printf "event %02d/on : at +%02d:%02d:%02d set %s on\n" $i $hour $minute $second ${devices[$dev]} + printf "event %02d/off: at +%02d:%02d:%02d set %s off\n" $i $hour_off $minute_off $second_off ${devices[$dev]} + command=`printf "$command at +%02d:%02d:%02d set %s on;;" $hour $minute $second ${devices[$dev]}` + command=`printf "$command at +%02d:%02d:%02d set %s off;;" $hour_off $minute_off $second_off ${devices[$dev]}` + ;; + *) + echo "no variant specifieno variant specified!!" + ;; + esac + +done +command="$command $command_end" + +#execute command +eval "$command" + + diff --git a/contrib/garden.pl b/contrib/garden.pl new file mode 100755 index 000000000..f9473c1d1 --- /dev/null +++ b/contrib/garden.pl @@ -0,0 +1,212 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use IO::Socket::INET; +use IO::Handle; + +STDOUT->autoflush(1); + +################# +# Formula: +# Compute for the last days + today the avarage temperature and the +# sum of rain, then compute the multiplier: (temp/20)^2 - rain/5 +# Now multiply the duration of each vent with this multiplier +# If the value is less than a minimum, then store the value and add it +# the next day +################# + +my $test = 0; # Test only, do not switch anything +my $fhzport = 7072; # Where to contact it +my $avg = "/home/rudi/log/avg.log"; # KS300 avarage log file +my $navg = 2; # Number of last avg_days to consider +my $min = 300; # If the duration is < min (sec) then collect +my $col = "/home/rudi/log/gardencoll.log"; # File where it will be collected +my $pmp = "GPumpe"; # Name of the water pump, will be switched in first +my $maxmult = 4; # Maximum factor (corresponds to 40 degree avg. + # temp over $navg days, no rain) + +if(@ARGV) { + if($ARGV[0] eq "test") { + $test = 1; + } else { + print "Usage: garden.pl [test]\n"; + exit(1); + } +} + +my %list = ( + GVent1 => { Nr => 1, Dur => 720 }, + GVent2 => { Nr => 2, Dur => 480 }, + GVent3 => { Nr => 3, Dur => 720 }, + GVent4 => { Nr => 4, Dur => 720 }, + GVent6 => { Nr => 5, Dur => 720 }, + GVent7 => { Nr => 6, Dur => 480 }, + GVent8 => { Nr => 7, Dur => 480 }, +); + +############################## +# End of config + +sub fhzcommand($); +sub doswitch($$); +sub donext($$); + +my ($nlines, $temp, $rain) = (0, 0, 0); +my ($KS300name, $server, $last); + +my @t = localtime; +printf("%04d-%02d-%02d %02d:%02d:%02d\n", + $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); + +########################### +# First read in the last avg_days +open(FH, $avg) || die("$avg: $!\n"); +my @avg = ; +close(FH); + +my @tarr; # Want the printout in the right order +while(my $l = pop(@avg)) { + next if($l !~ m/avg_day/); + my @v = split(" ", $l); + push(@tarr, "$v[0]: T: $v[4], R: $v[10]") if($test); + $temp += $v[4]; $rain += $v[10]; + $KS300name = $v[1]; + $nlines++; + last if($nlines >= $navg); +} + +########################### +# Now get the current day +foreach my $l (split("\n", fhzcommand("list $KS300name"))) { + next if($l !~ m/avg_day/); + my @v = split(" ", $l); + print("$v[0] $v[1]: T: $v[4], R: $v[10]\n") if($test); + $temp += $v[4]; $rain += $v[10]; + $nlines++; + last; +} + +if($test) { + foreach my $l (@tarr) { + print "$l\n"; + } +} + + +########################### +# the collected data +my %coll; +if(open(FH, $col)) { + while(my $l = ) { + my ($k, $v) = split("[ \n]", $l); + $coll{$k} = $v; + } + close(FH); +} + +########################### +# The formula +$temp /= $nlines; +$rain /= $nlines; + +# safety measures +$rain = 0 if($rain < 0); +$temp = 0 if($temp < 0); +$temp = 40 if($temp > 40); +my $mult = exp( 2.0 * log( $temp / 20 )) - $rain/5; +$mult = $maxmult if($mult > $maxmult); + +if($mult <= 0) { + print("Multiplier is not positive ($mult), exiting\n"); + exit(0); +} + +printf("Multiplier is %.2f (T: $temp, R: $rain)\n", $mult, $temp, $rain); + +my $have = 0; +if(!$test) { + open(FH, ">$col") || die("Can't open $col: $!\n"); +} +foreach my $a (sort { $list{$a}{Nr} <=> $list{$b}{Nr} } keys %list) { + my $dur = int($list{$a}{Dur} * $mult); + + if(defined($coll{$a})) { + $dur += $coll{$a}; + printf(" $a: $dur ($coll{$a})\n"); + } else { + printf(" $a: $dur\n"); + } + + if($dur > $min) { + $list{$a}{Act} = $dur; + $have += $dur; + } else { + print FH "$a $dur\n" if(!$test); + } +} + +print("Total time is $have\n"); +exit(0) if($test); +close(FH); + +if($have) { + doswitch($pmp, "on") if($pmp); + sleep(3) if(!$test); + foreach my $a (sort { $list{$a}{Nr} <=> $list{$b}{Nr} } keys %list) { + next if(!$list{$a}{Act}); + donext($a, $list{$a}{Act}); + } + donext("", 0); + doswitch($pmp, "off") if($pmp); +} + +########################### +# Switch the next dev on and the last one off +sub +donext($$) +{ + my ($dev, $sl) = @_; + doswitch($dev, "on"); + doswitch($last, "off"); + $last = $dev; + if($test) { + print "sleeping $sl\n"; + } else { + sleep($sl); + } +} + +########################### +# Paranoid setting. +sub +doswitch($$) +{ + my ($dev, $how) = @_; + return if(!$dev || !$how); + + if($test) { + print "set $dev $how\n"; + return; + } + fhzcommand("set $dev $how"); + sleep(1); + fhzcommand("set $dev $how"); +} + +########################### +sub +fhzcommand($) +{ + my $cmd = shift; + + my ($ret, $buf) = ("", ""); + $server = IO::Socket::INET->new(PeerAddr => "localhost:$fhzport"); + die "Can't connect to the server at port $fhzport\n" if(!$server); + syswrite($server, "$cmd;quit\n"); + while(sysread($server, $buf, 256) > 0) { + $ret .= $buf; + } + close($server); + return $ret; +} diff --git a/contrib/init-scripts/fhem.1 b/contrib/init-scripts/fhem.1 new file mode 100755 index 000000000..c691d9a0f --- /dev/null +++ b/contrib/init-scripts/fhem.1 @@ -0,0 +1,46 @@ +#! /bin/sh -e +# +# +# +# Written by Stefan Manteuffel + +PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin +DAEMON=/usr/local/bin/fhem.pl +PIDFILE=/var/run/fhem.pid + +# Arguments to atd +# +ARGS="/etc/FHZ/fhem.cfg" + +test -x $DAEMON || exit 0 + +. /lib/lsb/init-functions + +case "$1" in + start) + echo "Starting deferred execution scheduler..." + start-stop-daemon -b --start --quiet --pidfile $PIDFILE --startas $DAEMON -- $ARGS + log_end_msg $? + ;; + stop) + log_begin_msg "Stopping deferred execution scheduler..." + start-stop-daemon --oknodo --stop --quiet --retry 30 --pidfile $PIDFILE --name fhem.pl + + log_end_msg $? + ;; + force-reload|restart) + log_begin_msg "Restarting deferred execution scheduler..." + if start-stop-daemon --stop --quiet --retry 30 --pidfile $PIDFILE --name fhem.pl; then + start-stop-daemon -b --start --quiet --pidfile $PIDFILE --startas $DAEMON -- $ARGS + log_end_msg $? + else + log_end_msg 1 + fi + ;; + *) + echo "Usage: /etc/init.d/fhem.pl {start|stop|restart|force-reload|reload}" + exit 1 + ;; +esac + +exit 0 diff --git a/contrib/init-scripts/fhem.2 b/contrib/init-scripts/fhem.2 new file mode 100755 index 000000000..2de44f8e3 --- /dev/null +++ b/contrib/init-scripts/fhem.2 @@ -0,0 +1,25 @@ +#!/bin/sh +# by Matthias Bauer + +case "$1" in + start) + echo "Starting $0" + fhem.pl /etc/fhem/fhem.conf + ;; + stop) + echo "Stopping $0" + killall fhem.pl + ;; + status) + cnt=`ps -ef | grep "fhem.pl" | grep -v grep | wc -l` + if [ "$cnt" -eq "0" ] ; then + echo "$0 is not running" + else + echo "$0 is running" + fi + ;; + *) + echo "Usage: $0 {start|stop|status}" + exit 1 +esac +exit 0 diff --git a/contrib/ks300avg.pl b/contrib/ks300avg.pl new file mode 100755 index 000000000..943012b44 --- /dev/null +++ b/contrib/ks300avg.pl @@ -0,0 +1,77 @@ +#!/usr/bin/perl + +# Compute Daily and monthly avarage temp/hum/wind and cumulative rain values +# from the "standard" KS300 logs. +# Best to concatenate all KS300-logs into one big file (cat out*.log > big.log) +# and then start the program with ks300avg.pl big.log +# Note: the program assumes that there are no "holes" in the logs. + +use strict; +use warnings; + +if(@ARGV != 1) { + print "Usage: ks300avg.pl KS300-logfile\n"; + exit(1); +} + +open(FH, $ARGV[0]) || die("$ARGV[0]: $!\n"); + +my ($mt, $mh, $mw, $md) = (0,0,0,0); +my ($t, $h, $w) = (0,0,0); +my (@ld, $lsec, $lr, $mr, $ldsec); +my ($dt, $dev, $sec, @a); + +while(my $l = ) { + next if($l =~ m/avg/); + + chomp $l; + @a = split(" ", $l); + $dev = $a[1]; + $dt = $a[0]; + my @d = split("[_:-]", $a[0]); + $sec = $d[3]*3600+$d[4]*60+$d[5]; + + if(!$lsec) { + @ld = @d; + $lr = $a[9]; + $mr = $a[9]; + $lsec = $ldsec = $sec; + next; + } + + my $difft = $sec - $lsec; + $difft += 86400 if($d[2] != $ld[2]); + + $lsec = $sec; + $t += $difft * $a[3]; + $h += $difft * $a[5]; + $w += $difft * $a[7]; + + $l = ; + + if($d[2] != $ld[2]) { # Day changed + my $diff = ($sec - $ldsec) + 86400; + $t /= $diff; $h /= $diff; $w /= $diff; + printf("$dt $dev avg_day T: %.1f H: %d W: %0.1f R: %.1f\n", + $t, $h, $w, $a[9]-$lr); + $lr = $a[9]; + $md++; + $mt += $t; $mh += $h; $mw += $w; + $t = $h = $w = 0; + $ldsec = $sec; + } + + if($d[1] != $ld[1]) { # Month changed + printf("$dt $dev avg_month T: %.1f H: %d W: %0.1f R: %.1f\n", + $mt/$md, $mh/$md, $mw/$md, $a[9]-$mr); + $mr = $a[9]; + $mt = $mh = $mw = $md = 0; + } + + @ld = @d; +} + +printf("$dt $dev avg_day T: %.1f H: %d W: %0.1f R: %.1f\n", + $t/$sec, $h/$sec, $w/$sec, $a[9]-$lr); +printf("$dt $dev avg_month T: %.1f H: %d W: %0.1f R: %.1f\n", + $mt/$md, $mh/$md, $mw/$md, $a[9]-$mr); diff --git a/contrib/rolwzo_not_off.sh b/contrib/rolwzo_not_off.sh new file mode 100755 index 000000000..bc5b43354 --- /dev/null +++ b/contrib/rolwzo_not_off.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# What do I want: If I'am sitting e.g. outside I don't want that the rollo goes +# down. solution: if the button for e.g. rollo will be pressed after 16:59 +# o'clock then the at-job for going down by sunset will be deleted + +# put something like the following into your fhz100.cfg: +# notifyon rolwzo /usr/local/bin/rolwzo_not_off.sh + + +FHZ="/usr/local/bin/fhem.pl 7072" +order="delete at set rolwzo off" + +DATESTRING=`date +"%H"` +[[ $DATESTRING > 16 ]] && $FHZ "$order" diff --git a/contrib/rrd/98_RRD.pm b/contrib/rrd/98_RRD.pm new file mode 100644 index 000000000..138787d68 --- /dev/null +++ b/contrib/rrd/98_RRD.pm @@ -0,0 +1,76 @@ +############################################## +# Example for writing to RRD. +# notify .*T:.* {RRDUpdateTemp("@","%")} +# and put this file in the /FHEM directory. + +package main; +use strict; +use warnings; +use RRDs; + +my $DB = "/var/lib/collectd/temperature.rrd"; + +sub +RRD_Initialize($$) +{ +my ($hash, $init) = @_; +$hash->{Type} = "none"; + +if(! -f $DB) + { + Log 3, "***RRD Init"; + RRDs::create($DB, "--step=300", + "DS:innen:GAUGE:1800:-30.0:70.0", + "DS:bad:GAUGE:1800:-30.0:70.0", + "DS:wasser:GAUGE:1800:-30.0:70.0", + "RRA:AVERAGE:0.5:1:288", + "RRA:MAX:0.5:12:168", + "RRA:MIN:0.5:12:168", + "RRA:AVERAGE:0.5:288:365") or die "Create error: ($RRDs::error)"; + } +} + +### FHT80 ### +sub +RRDUpdateInnen($$) +{ +my ($a1, $a2) = @_; +my @a = split(" ", $a2); +my $tm = TimeNow(); + +my $value = $a[1]; +Log 5, "Device $a1 was set to $a2 (type: $defs{$a1}{TYPE})"; + +Log 2, "***InnenTemp:$value um $tm RRD"; +RRDs::update($DB, "--template", "innen", "N:$value") or + die "Update error: ($RRDs::error)"; +} + +### HMS ### +sub +RRDUpdateTemp($$) +{ +my ($a1, $a2) = @_; +# a2 is like "T: 21.2 H: 37 " +my @a = split(" ", $a2); +my $tm = TimeNow(); +my $value = $a[1]; + +Log 5, "Device $a1 was set to $a2 (type: $defs{$a1}{TYPE})"; + +if($a1 eq "viebadtemp") + { + Log 2, "***BadTemp:$value um $tm RRD"; + RRDs::update($DB, "--template", "bad", "N:$value") or + die "Update error: ($RRDs::error)"; + } + +if($a1 eq "viewassertemp") + { + Log 2, "***WasserTemp:$value um $tm RRD"; + RRDs::update($DB, "--template", "wasser", "N:$value") or + die "Update error: ($RRDs::error)"; + } +} + +1; diff --git a/contrib/rrd/fhz1000-rrd-howto.txt b/contrib/rrd/fhz1000-rrd-howto.txt new file mode 100644 index 000000000..76bac9597 --- /dev/null +++ b/contrib/rrd/fhz1000-rrd-howto.txt @@ -0,0 +1,52 @@ +apt-get install rrdtool + +if RRDs.pm is missing apt-get install librrds-perl +apt-get collectd + +Zum weiterlesen +Messdaten mit RRDtool und Perl verwalten http://www.linux-magazin.de/Artikel/ausgabe/2004/06/perl/perl.html + +my example: + + RRDs::create($DB, "--step=300", #300 = every 5 min + "DS:innen:GAUGE:1800:-30.0:70.0", #FHT80 room temperature + "DS:bad:GAUGE:1800:-30.0:70.0", #HMS100TF bath temperature + "DS:wasser:GAUGE:1800:-30.0:70.0", #HMS100T water temperature + "RRA:AVERAGE:0.5:1:288", + "RRA:MAX:0.5:12:168", + "RRA:MIN:0.5:12:168", + "RRA:AVERAGE:0.5:288:365") or die "Create error: ($RRDs::error)"; + +Modification on collection.cgi + + temperature => [ + 'DEF:temp_avg={file}:bad:AVERAGE', + 'DEF:temp_min={file}:bad:MIN', + 'DEF:temp_max={file}:bad:MAX', + 'DEF:i_avg={file}:innen:AVERAGE', + 'DEF:i_min={file}:innen:MIN', + 'DEF:i_max={file}:innen:MAX', + 'DEF:w_avg={file}:wasser:AVERAGE', + 'DEF:w_min={file}:wasser:MIN', + 'DEF:w_max={file}:wasser:MAX', + "AREA:temp_max#$HalfBlue", + "AREA:temp_min#$Canvas", + "LINE1:temp_avg#$FullRed:Bad", + 'GPRINT:temp_min:MIN:%4.1lf Min,', + 'GPRINT:temp_avg:AVERAGE:%4.1lf Avg,', + 'GPRINT:temp_max:MAX:%4.1lf Max,', + 'GPRINT:temp_avg:LAST:%4.1lf zuletzt\l', + "LINE2:i_avg#$FullGreen:Innen", + 'GPRINT:i_min:MIN:%4.1lf Min,', + 'GPRINT:i_avg:AVERAGE:%4.1lf Avg,', + 'GPRINT:i_max:MAX:%4.1lf Max,', + 'GPRINT:i_avg:LAST:%4.1lf zuletzt\l', + "LINE3:w_avg#$FullBlue:Wasser", + 'GPRINT:w_min:MIN:%4.1lf Min,', + 'GPRINT:w_avg:AVERAGE:%4.1lf Avg,', + 'GPRINT:w_max:MAX:%4.1lf Max,', + 'GPRINT:w_avg:LAST:%4.1lf zuletzt\l' + + ], + + diff --git a/contrib/serial.pl b/contrib/serial.pl new file mode 100755 index 000000000..47e01f82f --- /dev/null +++ b/contrib/serial.pl @@ -0,0 +1,89 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Device::SerialPort; +use Time::HiRes qw(gettimeofday); +sub pp($$); + +if(@ARGV != 3) { + printf("Usage: perl serial.pl serial-device outfile initial-hex-msg\n"); + exit(1); +} +my $ser = $ARGV[0]; +my $fil = $ARGV[1]; +my $hm = $ARGV[2]; + +my $fd; +open($fd, ">$fil") || die("Can't open $fil for writing\n"); +select $fd; +$| = 1; + +my $serport = new Device::SerialPort ($ser); +die "Can't open $ser: $!\n" if(!$serport); +$serport->reset_error(); +$serport->baudrate(38400); +$serport->databits(8); +$serport->parity('none'); +$serport->stopbits(1); +$serport->handshake('none'); + +my $interval = 2.0; # Seconds + +my $nto = gettimeofday(); +my $nfound; + +$hm=~ s/ //g; +$hm = pack('H*', $hm); + +while (1) { + my ($rout, $rin) = ('', ''); + vec($rin, 0, 1) = 1; # stdin + vec($rin, $serport->FILENO, 1) = 1; + + my $to = $nto - gettimeofday(); + if($to > 0) { + $nfound = select($rout=$rin, undef, undef, $to); + die("Select error $nfound / $!\n") if($nfound < 0); + } + + if($to <= 0 || $nfound == 0) { # Timeout + $serport->write($hm); + pp("S>", $hm); + $nto = gettimeofday() + $interval; + } + + if(vec($rout, 0, 1)) { + my $buf = ; + die "EOF on STDIN\n" if(!defined($buf) || length($buf) == 0); + $buf=~ s/[ \r\n]//g; + $buf = pack('H*', $buf); + $serport->write($buf); + pp("X>", $buf); + } + if(vec($rout, $serport->FILENO, 1)) { + my $buf = $serport->input(); + die "EOF on $ser\n" if(!defined($buf) || length($buf) == 0); + pp("S<", $buf); + } +} + +sub +pp($$) { + my ($prompt, $txt) = @_; + + my ($s, $ms) = gettimeofday(); + my @t = localtime($s); + my $tim = sprintf("%02d:%02d:%02d.%03d", $t[2],$t[1],$t[0], $ms/1000); + + for(my $i = 0; $i < length($txt); $i += 16) { + my $a = substr($txt, $i, 16); + my $h = unpack("H*", $a); + $a =~ s/[\r\n]/./g; + $a =~ s/\P{IsPrint}/\./g; + $h =~ s/(....)/$1 /g; + printf $fd "%s %s %04d %-40s %s\n", $prompt, $tim, $i, $h, $a; + } + print $fd "\n"; + +} diff --git a/docs/README b/docs/README new file mode 100644 index 000000000..09f1a1c70 --- /dev/null +++ b/docs/README @@ -0,0 +1,6 @@ +The snippet files are here just for reference, some of them were created by me, +some of them were sent to me by fellow users of the FHZ1000, others found in +the newsgroups. + +Do not assume that they were verified, they are by no means thought as an +official documentation. diff --git a/docs/commandref.html b/docs/commandref.html new file mode 100644 index 000000000..465e420e9 --- /dev/null +++ b/docs/commandref.html @@ -0,0 +1,1176 @@ + + + + + + fhem.pl command reference + + + + +

fhem.pl command reference

+ +You can use all of the following commands in in two ways: +
    +
  • In the configuration file, which must be specified if you startup the + server. Example:
    +
      + fhem.pl ~/.fhem +
    +
    + A minimal configuration file would look like: +
    +      logfile /tmp/fhem.log
    +      savefile /tmp/fhem.save  
    +      verbose 3                   
    +      port 7072                   
    +      modpath .                   
    +      define FHZ FHZ /dev/tts/USB0        
    +
    +      define lamp FS20 8765 01
    + For other configuration files see the examples subdirectory.
    +
    +
  • + +
  • Through the TCP/IP connection, which you can either use in a "session" + (via telnet) or single client command (via fhem.pl). Example: +
      + telnet localhost 7072
      + <NL>
      (This newline switches into "prompt" mode)
      + <command>...
      + quit

      +
    + or +
      + fhem.pl 7072 "set lamp off" +
    +
  • +
+ +There are three types of commands: "fhz" commands (described in this document), +shell commands (they must be enclosed in double quotes ") and perl expressions +(enclosed in curly brackets {}). shell commands or perl expressions are needed for +complex at or notifyon arguments.

+ +Shell commands will be executed in the +background, the perl program and the fhz commands will be executed in the main +"thread". +In order to make perl expressions easier to write, some special functions and +variables are available. See the section Perl special for a +description. +To trigger "fhz" commands from a shell script, use the client form of +fhem.pl (described above).

+ +Multiple fhz commands are separated by semicolon (;). In order to use semicolon +in perl code or shell programs, they have to be escaped by the double semicolon +(;;).

+ +Commands can be either typed in plain, or read from a file (e.g. the +configuration file at startup). The commands are either executed directly, or +later if they are arguments to the at and notifyon fhz commands.

+ +If commands are read from a file, then a line ending with \ will be +concatenated with the next one, so long lines (e.g. perl oneliners) can be +split in multiple lines

+ + + +

?, help

+
    + ?
    + help
    +
    + Get a list of all commands and short description for each one +
+ + + +

at

+
    + at <timespec> <command>
    +
    + Start an arbitrary fhem.pl command at a later time. + <timespec> format: [+][*]HH:MM:SS
    + The optional + indicates that the specification is + relative(i.e. it will be added to the current time).
    + The optional * indicates that the command should be + executed repeatedly.
    + The optional {N} after the * indicates,that the command + should be repeated N-times only.
    +
    + + Examples: +
    +  # absolute ones:
    +  at 17:00:00 set lamp on                            # fhz command
    +  at 17:00:00 { Log 1, "Teetime" }                   # Perl command
    +  at 17:00:00 "/bin/echo "Teetime" > /dev/console"   # shell command
    +  at *17:00:00 set lamp on                           # repeat every day
    +
    +  # relative ones
    +  at +00:00:10 set lamp on                  # switch the lamp on in 10 seconds
    +  at +00:00:02 set lamp on-for-timer 1      # Blink once in 2 seconds
    +  at +*{3}00:00:02 set lamp on-for-timer 1   # Blink 3 times
    +
    +  # Blink 3 times if the piri sends a command
    +  notify piri:on.* at +*{3}00:00:02 set lamp on-for-timer 1
    +
    +  # Switch the lamp on from sunset to 11 PM 
    +  # Copy 99_SUNRISE.pm in the FHEM directory to have sunset_rel()
    +  { sunrise_coord("8.686", "50.112", "Europe/Berlin") }
    +  at +*{sunset_rel()} set lamp on
    +  at *23:00:00 set lamp off
    +
    +  # More elegant version, works for sunset > 23:00 too
    +  at +*{sunset_rel()} set lamp on-till 23:00
    +
    +  # Only do this on weekend
    +  at +*{sunset_rel()} { fhz("set lamp on-till 23:00") if($we) }
    +
    +  # Switch lamp1 and lamp2 on from 7:00 till 10 minutes after sunrise
    +  at *07:00 set lamp1,lamp2 on-till {sunrise_abs(+600)}
    +
    +  # Switch the lamp off 2 minutes after sunrise each day
    +  at +*{sunrise_rel(+120)} set lamp on
    +  
    + + Notes:
    +
      +
    • if no * is specified, then a command will be executed + only once, and then the at entry will be deleted.
    • +
    • if the current time is greater then the time specified, then the + command will be executed tomorrow. This is why the relative forms + of the sunset/sunrise functions should be used with the relative + (+) flag
    • +
    • the delete argument for at is the + complete line as it appears in list + (with spaces), but you can use regexps.
    • +
    • In order to use the sunrise_rel()/sunset_rel() functions, copy the + 99_SUNRISE.pm file from the contrib into the modules (FHEM) + directory, and put { sunrise_coord(long, lat, tz) } into your config + file, as in the above example. If you are not using sunrise_coord, then + the coordinates for Frankfurt am Main, Germany will be used. + You also have to install the Datetime::Event::Sunrise perl module. +
    • +
    • do not place at commands in the config file if you + specified a savefile, as all + at commands will be saved there too, and then defined + twice at startup. at is intended to be inserted by + hand or by cronjobs.
    • +
    • For more complex date handling you either have to call fhem from + cron or filter the date in a perl expression, see the last example and + the section Perl special. +
    • +
+ + +

attr

+
    + attr <devname> <attrname> [<value>]
    + or
    + attr at <at-spec-regexp> <attribute>
    + +
    Set a device,log or at attribute. There are some special attributes used + by the fhem.pl itself or the web-frontends, but you can define your own to + use them in other applications.

    + + Recognized attributes:
    +
      +
    • room
      + Filter/group devices. Recognized by web-pgm2 and web-pgm3. + Devices in the room hidden will not appear in the web output.
    • +
    • loglevel
      + Set the device loglevel to e.g. 6 if you do not wish messages from a + given device to appear in the global logfile (FHZ/FS20/FHT). E.g. to + set the FHT time, you should schedule "set FHZ time" every minute, but + this in turn makes your logfile unreadable. These messages will not be + generated if the FHZ attribute loglevel is set to 6.
    • +
    • dummy
      + Set the device attribute dummy to define devices which should not + output any radio signals. Associated notifyons will be executed if + the signal is received. Used e.g. to react to a code from a sender, but + it will not emit radio signal if triggered in the web frontend. + Implemented for FS20 and FHT devices.
    • +
    • do_not_notify
      + Disable FileLog/notify/inform notification for a device. This affects + the received signal, the set and trigger commands.
    • +
    • skip_next
      + Used for at commands: skip the execution of the command the next time. +
    • +
    • follow-on-for-timer
      + the program automatically schedules a "setstate off" for the time + specified as argument to the on-for-timer command (for the specified + device only). +
    • +
    • repeater
      + Set the attribute "repeater" for an FHZ device to 1 to ignore events + received from a FS20 repeater. In fact we are not sure that they are + repeater messages, we just ignore messages which were sent out by our + device for the next 3 seconds (see the next attribute) +
    • +
    • filtertimeout
      + Ignore duplicate messages for this amount of time. The time is in + seconds, fractions are allowed. It affects installations with more then + one FHZ device or repeater, see the entry above. +
    • +
    + + Examples: +
      + attr lamp room kitchen
      + attr lamp dummy
      + attr lamp loglevel 6
      + del attr lamp
      + at *23:00:10 set lamp off
      + attr at lamp.off skip_next
      + +
    +
    + + Notes:
    +
      +
    • There is no way to delete a single attribute.
    • +
+ + + + +

define

+
    + define <name> <type> <type-specific>
    +
    + Define a device. You need devices if you want to manipulate them (e.g. + set on/off), and the logfile is also more readable if it contains e.g. + "lamp off" instead of "Device 5673, Button 00, Code 00 (off)".
    +
    + + Type FHZ: +
      + define <name> FHZ <serial-device>
      +
      + Specifies the serial port to communicate with the FHZ 1000 PC. The name(s) + of the serial-device(s) depends on your distribution. The program can + service multiple devices, FS20 and FHT device commands will be sent out + through the last FHZ device defined before the definition of the FS20/FHT + device. To change the association, use the set command + activefor. Important: this definition must occur after the + modpath command but before any other (FHZ related) device definition, + else you'll see "no I/O device found" messages.
      + If the serial-device is called none, then no device will be opened, so you + can experiment without hardware attached.
      + + Set the attribute "repeater" for this device to 1 to ignore events received + from a FS20 repeater. In fact we are not sure that they are repeater + messages, we just ignore messages which were sent out by our device for the + next 3 seconds (or configured otherwise by filtertimeout). + +
    +
    + + Type FS20: +
      + define <name> FS20 <housecode> <button> + [fg <fgaddr>] [lm <lmaddr>] [gm FF] +

      + + <housecode> is a four digit hex number, + corresponding to the housecode address, <button> is + a two digit hex nunmber, corresponding to a button of the transmitter.
      +
    • The optional fg specifies the function group, the first digit of the 2 + digit adress must be F.
    • +
    • The optional lm specifies the local master, the last digit of the 2 + digit adress must be F.
    • +
    • The optional gm specifies the global master, the adress must be FF.
    • +
      + + Examples: +
        + define lamp FS20 7777 00 fg F1
        + define roll1 FS20 7777 01 +
      +
    +
    + + Type FHT: +
      + define <name> FHT <housecode> +

      + + <housecode> is a four digit hex number, + corresponding to the adress of the FHT80b device. +
      + + Examples: +
        + define wz FHT 3232
        +
      +
      + See the section set for more. +
    +
    + + Type HMS: +
      + define <name> HMS <housecode> +

      + + <housecode> is a four digit hex number, + corresponding to the adress of the HMS device. +
      + + Examples: +
        + define temp HMS 1234
        +
      + Notes:
      +
        +
      • There is _NO_ guarantee that the code will work as expected in all + circumstances, the authors are not liable for any damage occuring as a + result of incomplete or buggy code
      • + +
      • Currently supported devices are the HMS100T, HMS100TF, HMS100WD and + the RM100-2.
      • + +
      • The housecode of the HMS devices may change if the battery is renewed. + In order to make life easier, you can define a "wildcard" device for each + type of HMS device. First the real device-id will be checked, then the + wildcard device id. The wildcards are: +
          +
        • 1000 for the HMS100TF
        • +
        • 1001 for the HMS100T
        • +
        • 1002 for the HMS100WD
        • +
        • 1003 for the RM100-2
        • +
        • 1006 for the HMS100MG
        • +
        +
      • + +
      • Some battery low notifications are not yet implemented (RM100, HMS100WD).
      • +
      • Please test your installation before relying on the functionality.
      • +
      +
      +
    +
    + + Type KS300: +
      + define <name> KS300 <housecode> [ml/raincounter [wind-factor]] +

      + + <housecode> is a four digit hex number, + corresponding to the adress of the KS300 device, right now it is ignored. + The ml/raincounter defaults to 255 ml, but it must be specified if you wish + to set the wind factor, which defaults to 1.0. + +
      + + Examples: +
        + define ks1 KS300 1234
        +
      +
      +
    +
    + + Type WS300: +
      + define WS300Device WS300 <serial device>
      + or
      + define <devname> WS300 [0-9]
      +
      + The first line is mandatory if you have a WS300 device: it defines the + input device with its USB port. The name of this device is fixed and must + be WS300Device. It must be the first defined WS300 device.
      + + For each additional device (with number 0 to 9) you have to define another + WS300 device, with an arbitrary name. The WS300 device which reports the + readings will be defined with the port number 9, an optional KS300 with the + port number 8.

      + + Examples: +
      +      define WS300Device  WS300   /dev/ttyUSB1
      +      define ash2200-1    WS300   0
      +      define ks300        WS300   8
      +      define ws300        WS300   9
      +    
      +
    +
    + + + Type FileLog: +
      + define <name> FileLog <filename> <regexp> +

      + + Log events to <filename>. The log format is +
      +      YYYY:MM:DD_HH:MM:SS <device> <event>
      + The regexp will be checked against the (complete!) device name + or against the (complete!) devicename:event combination. +
      + <filename> may contain one or more of the following + wildcards (a subset of the Unix date command arguments): +
        +
      • %d day of month (01..31)
      • +
      • %m month (01..12)
      • +
      • %Y year (1970...) +
      • %w day of week (0..6); 0 represents Sunday +
      • %j day of year (001..366) +
      • %U week number of year with Sunday as first day of week (00..53) +
      • %V week number of year with Monday as first day of week (01..53) +
      + + Examples: +
        + define lamplog FileLog /var/tmp/lamp.log lamp
        + define wzlog FileLog /var/tmp/wz-%Y-%U.log + wz:(measured-temp|actuator).*
        +
      +
      +
    +
+ + + +

delete

+
    + delete {def|ntfy|at} <name>
    +
    + Delete a definition, a + notifyon setting or an at command.
    + The <name> argument has to be the first column of + the list output (as in case of at least at + it is not obvious). +

    + + Examples: +
      + delete def lamp
      + delete at 22:15:00 set lamp off +
    +
    + + Notes: +
      +
    • The program first tries to find the exact <name>, if + it is not found, then it will try it as a regexp, so you can also specify + delete at .*lamp.* to delete the at command above.
    • + +
    • Do not use doubleqoutes ("e;) in <name>, only if + you used them in your definition. +
    +
+ + +

get

+
    + get <name> <type-specific> +

    + Ask a value directly from the device, and wait for an answer. In general, you + can get a list of possible commands by
    get <device> + help +
    + Right now only the FHZ module supports this function. + +

    Type FHZ:

    +
      + get FHZ <value> +

      + where value is one of:
      +
      +      init1
      +      init2
      +      init3
      +      serial
      +      fhtbuf
      + Notes: +
        +
      • There is only one FHZ device (called FHZ), it is created automatically + at startup.
      • +
      • The mentioned codes are needed for initializing the FHZ1000
      • +
      • The answer for a command is also displayed by list FHZ +
      • +
      • + fhtbuf should be incorporated later in the FHT + module:
        the FHZ1000PC has a message buffer for the FHT, as it + only can send messages to it every 2 (or so) minutes. If the buffer + is full, then newly issued ones will be dropped. fhtbuf + returns the free memory in this buffer (in hex), my maximum is 2c (42 + bytes).
      • + +
      +
    +
+ + + +

include

+
    + include <filename>
    +
    + Read in the file, and process every line as a fhem command. Makes + configuration files more modular and enables to reread them. +
    +
+ + +

inform

+
    + inform [on|off]
    +
    + If set to on, and a device state changes, send a notification to the current + client. This command can be used by other programs/modules to receive a + notification. +
    +
+ + +

list

+
    + list [name] +

    + Output a list of all definitions, all notifyon settings and all at + entries. This is one of the few commands which return a string in a + normal case. +

    + Example: +
      FHZ> list
    +
    +  Type list  for detailed info.
    +
    +  FHZ devices:
    +    FHZ                  (Last msg: Initialized)
    +
    +  FS20 devices:
    +    Btn3                 (Last msg: off)
    +    Roll1                (Last msg: on-for-timer 11)
    +    Stehlampe            (Last msg: off)
    +
    +  FHT devices:
    +    fl                   (Last msg: state: Bat: ok, Window: closed)
    +    wz                   (Last msg: actuator: 07%)
    +
    +  NotifyOn:
    +    Btn3                 /usr/local/bin/setroll %
    +
    +  At:
    +    +*{sunrise_rel(+10)} set Roll1 on-for-timer 10 (07:43:56)
    +
    +
    +  Logs:
    +    wzlog FileLog /var/tmp/wz.log wz:.*(temp|actuator).*
    +  
    + If specifying name, then a detailed status for name + will be displayed, e.g.: +
      FHZ> list wz
    +
    +  2006-01-03 18:28:27   actuator        07%
    +  2006-01-03 18:26:32   mon-from1       06:00
    +  2006-01-03 18:26:33   mon-to1         23:00
    +  2006-01-03 18:26:34   tue-from1       06:00
    +  2006-01-03 18:26:34   tue-to1         23:00
    +  2006-01-03 18:26:35   wed-from1       06:00
    +  2006-01-03 18:26:35   wed-to1         23:00
    +  2006-01-03 18:26:37   thu-from1       06:00
    +  2006-01-03 18:26:37   thu-to1         23:00
    +  2006-01-03 18:26:38   fri-from1       06:00
    +  2006-01-03 18:26:38   fri-to1         23:00
    +  2006-01-03 18:26:39   sat-from1       06:00
    +  2006-01-03 18:26:40   sat-to1         23:50
    +  2006-01-03 18:26:41   sun-from1       06:00
    +  2006-01-03 18:26:41   sun-to1         23:00
    +  2006-01-03 18:26:45   mode            manual
    +  2006-01-03 18:26:44   desired-temp    21.5 (Celsius)
    +  2006-01-03 18:26:45   measured-temp   22.0 (Celsius)
    +  2006-01-03 18:26:46   state           Bat: ok, Window: closed
    +  2006-01-03 18:24:37   init            255
    +  2006-01-03 18:26:42   day-temp        21.0 (Celsius)
    +  2006-01-03 18:26:42   night-temp      17.0 (Celsius)
    +  2006-01-03 18:26:43   unknown_85      4
    +  2006-01-03 18:26:43   windowopen-temp 12.0 (Celsius)
    +  
    + +
+ + + + +

logfile

+
    + logfile +

    + Specify the logfile to write. Specify this as the first command in the + configfile as everything gets logged in the logfile. You can use "-" for + stdout, in this case the server won't background itself.
    + The logfile name can also take wildcards for easier logfile rotation, + see the FileLog section of the define command. +

    + + Examples: +
      + logfile /var/log/fhem
      + logfile /var/log/fhem-%Y-%m.log +
    +
+ + + +

modpath

+
    + modpath <path> +

    + Specify the path to the modules directory FHEM. The path + should not contain the directory FHEM. Every module there will + be loaded. +

    + + Example: +
      + modpath /usr/local/lib +
    +
+ + + +

notifyon

+
    + notifyon <name> <command> +

    + Execute a command when received an event for the definition <name>. As with normal + commands, if it is enclosed in {}, then it is a perl expression, if it is + enclosed in "", then it is a shell command, else it is a "plain" fhem.pl + command (chain). See the trigger command for testing + it. + + Examples: +
      + notifyon btn3 set lamp %
      + notifyon btn3 { fhz "set lamp %" }
      + notifyon btn3 "/usr/local/bin/setlamp "%""
      + notifyon btn3 set lamp1 %;;set lamp2 %
      + notifyon wz:measured.* "/usr/local/bin/logfht @ "%""
      + notifyon .*H:.* {DbLog("@","%")}
      + notifyon UNDEFINED "send-me-mail.sh "%""
      +
    +
    + + Notes: +
      +
    • The character % will be replaced with the received event, + e.g. with on or off or measured-temp: 21.7 + (Celsius)
      It is advisable to put the % into double + quotes, else the shell may get a syntax error.
      + To use % or @ in the text itself, use the double mode (%% or @@)
    • + +
    • The character @ will be replaced with the device + name.
    • + +
    • <name> may also be a compound of + definition:event to filter for events.
    • + +
    • <name> is in fact a regexp. It must completely (!) + match either the device name, or the compound of the device name and the + event. The event is either the string you see in the list output in paranthesis after the device name, or the + string you see when you do a detailed list of the device.
    • + +
    • To use database logging, copy the file contrib/91_DbLog.p into your + modules directory, and change the $dbconn parameter in the file.
    • + +
    • Each undefined device (FS20, HMS, FHT) will be reported with the device + name "UNDEFINED". The % parameter will contain the type (FS20, HMS100T, + etc) and device number, separated by a space.
    • + +
    + +
+ + +

pidfile

+
    + pidfile <filename> +

    + Write the process id of the perl process to the specified file. We are + running as a daemon, and some distributions would like to check by the pid if + we are still runnning. +

    + Example: +
      + pidfile /var/run/fhem.pid +
    +
+ + + + +

port

+
    + port <number> [<global>] +

    + Listen on the TCP/IP port <number> for incoming + connections. To offer at least a little bit of security, the server will only + listen for connections from the localhost per default. The optional global + parameters enables listening for non-localhost connections too. +

    + Example: +
      + port 7072 +
    +
+ + + +

quit

+
    + quit +

    + If used in a TCP/IP session, terminate the client session.
    + If used in a script, terminate the parsing of the script. +

    + Example: +
      + quit +
    +
+ + + +

reload

+
    + reload <module> +

    + Reload the given module from the module directory. Mainly intended to reload + the 99_PRIV file (or equivalent) in order to test perl-scripts. +

    + Example: +
      + reload 99_PRIV +
    +
+ + +

rereadcfg

+
    + rereadcfg +

    + Re-read the configuration file. + Note: The statefile will be saved first, then the config file will be read + (all devices will be initialized again), and at last the statefile will be reloaded again. +

    + Example: +
      + rereadcfg +
    +
+ + + +

savefile

+
    + savefile <filename> +

    + Set the filename where the state and at information will + be saved before shutdown. If not setting it, then no information will be + saved. +

    + Example: +
      + savefile /var/tmp/fhem.save +
    +
+ + + +

set

+ + + + +

setstate

+
    + setstate <name> <value> +

    + Set the "Last msg" for the definition + <name> shown in the list command + to <value> without sending any signals to the device + itself. This command is also used in the savefile. +
    + Setting/changing arbitrary comments is possible, if the value begins with + comment:. To delete the comment, set it with an empty value, + see below. +

    + Examples: +
      +
      +    setstate lamp on
      +    setstate lamp comment:location sleeping room
      +    setstate lamp comment:location # deletes the comment
      +
    +
    + Note: +
      +
    • You even may set the detailed state (i.e. what you get when you call + list <name;> by specifying "<time> + <attribute> <value>". Take a look at the + savefile + after devices reported values for an example.
    • +
    +
+ + + +

shutdown

+
    + shutdown +

    + Shut down the server (after saving the state information + ) +

    + Example: +
      + shutdown +
    +
+ + + +

trigger

+
    + trigger <dev> <state> +

    + Trigger a notifyon command. +

    + Example: +
      + trigger btn3 on +
    +
+ + +

sleep

+
    + sleep <sec> +

    + Sleep for a given amount, millisecond accuracy. +

    + Example: +
      + sleep 0.5
      + notifyon btn3 set lamp toggle;;sleep 0.5;;set lamp toggle +
    +
    + Note: As the server is not multithreaded, everything is blocked for + the given amount.
    + +
+ + +

verbose

+
    + verbose <level> +

    + Set the verbosity level. Possible values: +
      +
    • 0 - it will only tell you when the server was started, or stopped
    • +
    • 1 - it logs all error messages or unknown packets
    • +
    • 2 - it logs all signals received or sent in a "digested" format,
    • +
    • 3 - it logs the signals for undefined devices,
    • +
    • 4 - it logs the TCP/IP connections and the called programs with parameters,
    • +
    • 5 - is for debugging.
    • +
    + Recommended level is 3 for normal use. +

    + Example: +
      + verbose 3 +
    +
+ + +

xmllist

+
    + xmllist +

    + Returns an XML tree of all definitions, all notifyon settings and all at + entries. It is not intended for human consumption. +

    + Example: +
      FHZ> xmllist
    +    <FHZINFO>
    +	<FHZ_DEVICES>
    +	    <FHZ name="FHZ" definition="FHZ FHZ" state="fhtbuf: 1c">
    +                <STATE name="fhtbuf" value="23" measured="2006-02-12 14:03:39"/>
    +		<STATE name="serial" value="136e21bc" measured="2006-03-26 08:47:36"/>
    +    [...]
    +  
    +
+ + + +

Perl specials

+
    +
  • To use fhz commands from the perl expression, use the function "fhz", + which takes a string argument, this string will be evaluated as an fhz + command chain.
    + Example: +
      + notifyon piri:on { fhz "set light on" } +
    +
  • +
  • + To make date and time handling easier, before evaluating a perl expression + the variables $sec, $min, $hour, $mday, $month, $year, $wday, $yday, $isdst + are set (see perldoc -f localtime), with the exception that $month is in the + range of 1 to 12, and $year is also corrected by 1900 (as one would normally + expect). Additionally $we is 1 if it is weekend (i.e $wday == 0 || + $wday == 6), and 0 otherwise. + Example: +
      + notifyon piri:on { if($hour > 18 || $hour < 5) { fhz "set light on" } } +
    + +
  • + +
  • + Note: do not forget to escape the semicolon (;) with two semicolons + (;;), else your perl code will be interpreted as an fhz command and you + most certainly get syntax errors. +
  • + +
  • + The current value (the string you see in paranthesis in the output of the + list command) is available in the value hash, to access it, + use $value{<devicename>}
    + If you need the old value (and time) of the currentliy triggered device, + then you can access it with $oldvalue{$dev}{TIME} and + $oldvalue{$dev}{VAL}.
    +
  • + +
  • + To access the numerical value of an FS20 command (e.g. toggle), use the + hash fs20_c2b. E.g. { Log 2, $fs20_c2b{"toggle"} } +
  • + +
  • + If you add the 99_SUNRISE.pm from the contrib directory to your module + directory (NOTE: you have to install the Perl module + DateTime::Event::Sunrise first), then you have access to the follwing + functions:
    +
      + sunset_rel()
      + sunset_abs()
      + sunrise_rel()
      + sunrise_abs()
      + isday()
      +
    + The _rel functions should be used as "at" spec, and the _abs functions as + argument to the on-till argument of the set command.
    + isday returns 1 if the sun is visible, and 0 else. +
  • + + + +
+ + + diff --git a/docs/faq.html b/docs/faq.html new file mode 100644 index 000000000..089ff3425 --- /dev/null +++ b/docs/faq.html @@ -0,0 +1,163 @@ + + + + + + FHEM FAQ + + + + +

FHEM FAQ

+ +I get "undefined" messages in the log after upgrading fhem.pl +
    + Stop fhem.pl, delete the previous .save file and restart fhem.pl. + If the problem still exists, send a bugreport. +
+ +I switched on a FS20 device directly (without the remote), but + the fhem.pl did not noticed it.
Is it a bug?
+
    + The protocol used by the FS20 family is quite simple: it is not + encrypted in any way, and there is no feedback to the sender. So if you + push any buttons on a pure receiver, no radio waves will be sent out, and + the FHZ1000 won't notice anything. The FHZ1000PC does not even know if + somebody received its message, it simply sends it out 3 times and hopes + for good luck.
    + To answer the question: it is not a bug :-) +
+ +I have some FS20/FHT/HMS devices.
+ How do I know their housecode?

+
    + If you already programmed some devices, then just start fhem.pl with one + of the example configuration files, and watch the log. When activating a + deivce (e.g. with the remote) then it will be logged as an unknown device + with the housecode. Note: the verbose level must be 3 or higher. KS300 + devices do not have a proper code, so you can use anything. +
+ +I have the code for my devices in the ELV notation, which contains 1,2,3 and + 4, but you require a hex code.
+ How should I convert it?
+
    + The code used by ELV is in the "quaternal" (?) system plus one added to each + digit, so you can even use the 4 button remote for programming. To convert, + you have two choices: the program four2hex in the contrib directory, or the + good old unix program bc. +
      +
    • From quaternal (ELV Remote) to hex (fhem.pl):
      +
      +	 % bc
      +	 obase=16
      +	 ibase=4
      +	 <elv number>
      + where <elv number> is the number used on the remote, but 1 + substracted from every digit (so each digit is between 0 and 3).
    • + +
    • From hex (fhem.pl) to quaternal (ELV Remote):
      +
      +         % bc
      +	 ibase=16
      +	 obase=4
      +	 <hex number>
      + Now add 1 to each digit of the result, and prepend it with 1's if + it has less than 4 digits.
    + + +
+ +I replaced my FHZ1X00PC, now the FHT80b's does not work anymore.
+Help me!
+
    + The FHT80b's are talking to a single FHZ1XXX device, which has a unique + "FHTcode". You have two choices: if you know the old FHTcode, then you can + set it, or you can tell the FHT80b to forget the old FHZ, and start talking + with the new one. +
      +
    • Set the FHTcode: The problem with this method is that I don't know how + to read out the value, we just can change it with
      + set FHZ FHTcode <hex-code>
    • +
    • Resync the FHT80b: Press PROG until "Sond" appears, then select "CEnt" + with the wheel, press PROG again, Select "nA" with the wheel, press PROG + again.
    • + +
    +
+ +I can specify an optional ml/raincounter for a KS300.
+Why do you think that 255 should be the default?
+
    + The manual talks about 0.3l resolution, but I wanted to calibrate my device. + So I filled a plastic bottle with 0.5 liter water from the measuring cup, + drilled a small hole in the bottom of the bottle, let the water flow slowly + in the KS300 rain-cup, and looked at the counter after the bottle was empty.
    + + The raincounter was incremented by 130 ticks. The diameter of my KS300 + rain-cup is 13.9 cm, the area ca 151.75 cm2, which + is ca 1/65.9 m2. + The 0.5 liter corresponds to 32.95 liter per m2. 130 ticks + correspond to 32.95 l/m2 -> 1 tick is ca 253 ml. I estimate + the error margin to +/- 2%
    + + You are welcome to do your own experiments, I am interested in the results. +
+ +The time specification of the builtin at command is not very flexible.
+Please add day/month/weekday to it.
+
    + I think the command is complex and flexible enough. Use a perl expression + for this functionality like (described in the commandref.html): +
    at *07:00:00 { fhz "set lamp on" if($we) }
    +
+ + +I defined my FS20STR as an FHT device, but I do not get any data from it. +
    + The FS20STR is an FS20 device, even if it looks like an FHT80b. + You'll get "only" on-for-timer and off-fot-timer events sent. +
+ +How to convert the FHT8b code seen in its display to the hex code needed by fhem.pl? +
    + Convert the first 2 digits first from decimal to hex, then the next two. Example:
    +
    +         % bc
    +	 set obase=16
    +	 <first two digits>
    +	 <last two digits>
    + E.g The FHT8b code 1121 is 0b15 for the fhem.pl +
+ +I'd like to use this sunrise/sunset stuff, can you help me? +
    + First you (most probably) have to install the DateTime::Event::Sunrise perl + module, as it is not part of the standard distributions. If it is not + installed and you copy the contrib/99_SUNRISE.pm into your module (FHEM) + directory, then the program will not start up, telling you that this module + is missing. + The (IMHO) easiest way to install it is via the following command (probably + as root):
    +
    +   perl -MCPAN -e shell
    +   cpan> install DateTime::Event::Sunrise
    + This will fetch the module from a CPAN archive, compile it and install it, + and will do the same with each perl module which is needed by this one.
    + + Next look for the geographic coordinates of your home, e.g with a GPS + receiver or with googleearth. Compute the latitude/longitude as needed, and + enter them in your init file (fhem.cfg) with the command: +
    {sunrise_coord("", "", "Europe/Berlin") }
    + If you are living in a different timezone, then change the string above + according to the perldoc DateTime manpage.
    + + Now copy the contrib/99_SUNRISE.pm file into your module directory, and + restart the program. If everything is ok, typing +
    { sunrise_abs() }
    + in the telnet prompt, will return the time of the sunrise today, in a + HH:MM:SS format. +
+ + + diff --git a/docs/fhem.html b/docs/fhem.html new file mode 100644 index 000000000..ea64c8f97 --- /dev/null +++ b/docs/fhem.html @@ -0,0 +1,218 @@ + + + + +Home of FHEM + + + + + +

FHEM (GPL'd FS20/HMS/FHT/KS300/WS300 server for linux, formerly known as fhz1000.pl)

+ +

News (as of =DATE=, Version =VERS=)

+
    +
+ +

Description

+
    +This program makes the FHZ1000/FHZ1300/WS300 USB devices sold by ELV, Conrad and +others useable with Linux. In fact, there is nothing Linux special in it, you +should be able to use it on other platforms as long as you can access the +hardware as a serial device.
    +The program runs as a server, you can control it via telnet, command line +program or TCP/IP directly, like the supplied web frontends do.

    + +Currently implemented features:
    +
      +
    • reading and sending FS20 events (on/off/dimming, timer commands, etc)
      +
    • support of FS20 address features function group, local and global master +
    • reading and changing FHT80b parameters (temp, actuator, etc).
      + The FHT8b seems to work too. Note: the FHT8 wont work.
    • +
    • reading HMS data (HMS100-T,-TF,-WD,-MG,-TFK and RM100-2)
    • +
    • reading KS300 data
    • +
    • reading WS300 data
    • +
    • logging events to files (or database), with regexp filters
    • +
    • notifying external programs or internal modules when receiving certain + events
    • +
    • timed commands (e.g. switching a lamp on from sunset till midnight)
    • +
    • modular architecture
    • +
    • a lot of web frontends, choose your favorite
    • +
      +
    +See commandref.html for a detailed command +description and faq.html for the F.A.Q. +
+ +

Links:

+ +

Related projects:

+ + +

Installation

+
    + +

    Linux driver

    + +
      + For kernels newer than 2.6.14 add one of the following lines + to /etc/modprobe.conf:
      +
      +  # FHZ1000 PC
      +  options ftdi_sio vendor=0x0403 product=0xf06f
      +  # FHZ1300 PC
      +  options ftdi_sio vendor=0x0403 product=0xe0e8
      + If in doubt, look at the id of the device with lsusb. + For older kernels apply the patch from the doc directory to your kernel.
      + Recompile your kernel and reboot or load/reload the ftdi_sio module. +
    + +

    Perl modules

    +
      + You need perl with the Device::SerialPort ( + http://search.cpan.org/dist/Device-SerialPort/) + module. All other needed modules were present in my installation. + If this module reports Can't call method "opened" on an undefined + value... when starting the server, then you either may ignore + this message, or replace the mentioned line with:
      +    $self->{HANDLE}->close if (defined($self->{HANDLE}) &&
      +    	                       $self->{HANDLE}->opened);
      +
    + +

    Server installation

    +
      +
    • Copy the file fhem.pl into your path (e.g. + /usr/local/bin), and the FHEM directory e.g. to + /usr/local/lib.
    • +
    • Make sure that you can access the serial USB + device (e.g. /dev/tts/USB0).
    • +
    • Create a configuration file (see the examples directory and + docs/commandref.html), and change at least the modpath + (/usr/local/lib) and define FHZ FHZ (/dev/tts/USB0) + parameters.
    • +
    • Delete the savefile if you are upgrading from an older version.
    • +
    • Start the server with fhem.pl <configfile>
    • +
    • For using the WS300, look into the contrib/ws300 directory.
    • +
    + +

    General Notes for Webfrontends:

    +
      +
    • You don't have to install all of them, one is probably more than + enough :-)
    • +
    • The web server and the fhem server must be on the same host
    • +
    • Important: Make sure you add some protection (.htaccess, etc) + else everybody will be able to set your devices
    • +
    + +

    Web frontend 2 (webfrontend/pgm2, the simple one)

    +
      + This frontend is CGI/CSS based. It has support for rooms, and FHT/KS300 logs.
      + Screenshots: overview + and temperature logs

      + +
        +
      • Copy the file fhemweb.pl to your cgi-bin directory + (/home/httpd/cgi-bin), and all the icons (*.gif) to your httpd icons + directory (/home/httpd/icons).
        + Note: The program looks for icons in the following order: + <device-name>.<state>, <device-name>, + <device-type>.<state>, <device-type>
        +
      • +
      • Edit fhemweb.pl, and check the "Config" section.
      • +
      • If you want to have access to the FHT temperature logs, then + make sure that gnuplot is installed and you log the temperatures like + in example/04_log. Note: There is still a bug with displaying empty + logfiles.
      • +
      • Call <your-site>/cgi-bin/fhemweb.pl +
      + For special features like assigning devices to rooms see the README file. +
    + +

    Web frontend 3 (webfrontend/pgm3, the professional one)

    +
      + This frontend is PHP based and was contributed by Martin Haas. + Look at the webfrontends/pgm3/docs for more documentation or at + this screenshot. A lot more details can be + found on Martins page: + http://www.martin-haas.de/fhz +

      + +
        +
      • Install PHP and enable it by commenting in the "LoadModule + phpX_module ..." directive in httpd.conf (perhaps it is already + done by your distro). Restart/reload httpd.
      • + +
      • Create a directory (e.g.: /home/httpd/html/pgm3) and copy all the + files from the webfrontend/pgm3 to this directory.
        Make sure that this + directory is writeable by the webserver!
      • + +
      • Edit index.php (/home/httpd/html/pgm3/index.php), and check the + required settings section
      • + +
      • If you want to have access to the FHT temperature logs, then: +
          +
        • Make sure gnuplot is installed
        • +
        • check the showgnuplot section in index.php
        • +
        • For each FHT device copy the file docs/gnuplot/gnuplot.wz to + gnuplot.fhtdevicename (to the /home/httpd/html/pgm3 directory) and + replace fht.log in this file with the absolute name of the current + logfile.
        • +
        +
      • Call <your-site>/pgm3/index.php
      • +
      +
    + +

    Web frontend 4 (webfrontend/pgm4, the template)

    +
      + This frontend is PHP based and was contributed by Stefan Mayer. It won't work + for you without modification, it is meant as a template or as an example. See + the screenshot. To install: +
        +
      • Copy the directory webfrontend/pgm4 to your html directory.
      • +
      • Install/enable PHP (see the description for frontend3)
      • +
      • Call the program with http://webserver/pgm4/fs20.php
      • +
      + Now you can go on, and modify it to suit your needs :-) +
    + +
+ +

License:

+
    + Copyright:
    +
      +
    • Rudolf Koenig (r dot koenig at koeniglich dot de)
    • +
    • Raoul Matthiessen (webfrontends/pgm1)
    • +
    • Martin Haas (webfrontends/pgm3)
    • +
    + License: GPL (v2) +
+ + + +

Misc:

+
    + Thanks for Tosti for inspiration and numerous other people for help.
    +
+ + + + diff --git a/docs/ftdi_sio.fhz1000.patch b/docs/ftdi_sio.fhz1000.patch new file mode 100644 index 000000000..223aded50 --- /dev/null +++ b/docs/ftdi_sio.fhz1000.patch @@ -0,0 +1,44 @@ +*** orig/drivers/usb/serial/ftdi_sio.c 2004-12-14 02:54:04.000000000 +0100 +--- linux/drivers/usb/serial/ftdi_sio.c 2004-12-13 19:04:24.000000000 +0100 +*************** static struct usb_device_id id_table_8U2 +*** 350,355 **** +--- 350,356 ---- + { USB_DEVICE_VER(FTDI_VID, PROTEGO_SPECIAL_3, 0, 0x3ff) }, + { USB_DEVICE_VER(FTDI_VID, PROTEGO_SPECIAL_4, 0, 0x3ff) }, + { USB_DEVICE_VER(FTDI_VID, FTDI_ELV_UO100_PID, 0, 0x3ff) }, ++ { USB_DEVICE_VER(FTDI_VID, FTDI_ELV_FHZ1000_PID, 0, 0x3ff) }, + { } /* Terminating entry */ + }; + +*************** static struct usb_device_id id_table_FT2 +*** 431,436 **** +--- 432,438 ---- + { USB_DEVICE_VER(FTDI_VID, PROTEGO_SPECIAL_3, 0x400, 0xffff) }, + { USB_DEVICE_VER(FTDI_VID, PROTEGO_SPECIAL_4, 0x400, 0xffff) }, + { USB_DEVICE_VER(FTDI_VID, FTDI_ELV_UO100_PID, 0x400, 0xffff) }, ++ { USB_DEVICE_VER(FTDI_VID, FTDI_ELV_FHZ1000_PID, 0x400, 0xffff) }, + { USB_DEVICE(FTDI_VID, FTDI_CCSICDU20_0_PID) }, + { USB_DEVICE(FTDI_VID, FTDI_CCSICDU40_1_PID) }, + { } /* Terminating entry */ +*************** static __devinitdata struct usb_device_i +*** 531,536 **** +--- 533,539 ---- + { USB_DEVICE(FTDI_VID, PROTEGO_SPECIAL_3) }, + { USB_DEVICE(FTDI_VID, PROTEGO_SPECIAL_4) }, + { USB_DEVICE(FTDI_VID, FTDI_ELV_UO100_PID) }, ++ { USB_DEVICE(FTDI_VID, FTDI_ELV_FHZ1000_PID) }, + { USB_DEVICE(FTDI_VID, FTDI_CCSICDU20_0_PID) }, + { USB_DEVICE(FTDI_VID, FTDI_CCSICDU40_1_PID) }, + { } /* Terminating entry */ +*** orig/drivers/usb/serial/ftdi_sio.h 2004-12-14 02:54:04.000000000 +0100 +--- linux/drivers/usb/serial/ftdi_sio.h 2004-12-13 19:03:21.000000000 +0100 +*************** +*** 143,148 **** +--- 143,149 ---- + + /* ELV USB Module UO100 (PID sent by Stefan Frings) */ + #define FTDI_ELV_UO100_PID 0xFB58 /* Product Id */ ++ #define FTDI_ELV_FHZ1000_PID 0xF06F /* Product Id */ + + /* + * Definitions for ID TECH (www.idt-net.com) devices diff --git a/docs/pgm1-1.gif b/docs/pgm1-1.gif new file mode 100644 index 000000000..7463c9373 Binary files /dev/null and b/docs/pgm1-1.gif differ diff --git a/docs/pgm1-2.gif b/docs/pgm1-2.gif new file mode 100644 index 000000000..e3acd7c7c Binary files /dev/null and b/docs/pgm1-2.gif differ diff --git a/docs/pgm2-1.gif b/docs/pgm2-1.gif new file mode 100644 index 000000000..2e639852e Binary files /dev/null and b/docs/pgm2-1.gif differ diff --git a/docs/pgm2-2.png b/docs/pgm2-2.png new file mode 100644 index 000000000..db632d367 Binary files /dev/null and b/docs/pgm2-2.png differ diff --git a/docs/pgm3-0.5.1.png b/docs/pgm3-0.5.1.png new file mode 100644 index 000000000..23160a343 Binary files /dev/null and b/docs/pgm3-0.5.1.png differ diff --git a/docs/pgm4.gif b/docs/pgm4.gif new file mode 100644 index 000000000..f8ea66321 Binary files /dev/null and b/docs/pgm4.gif differ diff --git a/docs/raw-codes b/docs/raw-codes new file mode 100644 index 000000000..333a5d6e3 --- /dev/null +++ b/docs/raw-codes @@ -0,0 +1,16 @@ +On the telnet prompt do: + {FhzDecode("")} +where is one of: + +81xx04xx0101a0011234030011 FS20 dev: 1234 button: 03 on (11) +81xx04xx0101a0011234030000 FS20 dev: 1234 button: 03 off (00) + +810d04xx4027a001de53187654321f KS300: Raincounter hex:123 (ca 75l/m2), + Wind 45.6 km/h Humidity: 78%, Temp: 13.5 + Unknown fields: d,e,f +{FhzDecode("810d04xx4027a001de53187654321f")} + +810e04d70213a001b16d000003000000 RM100-2 smoke on +810e04d70213a001b16d000000000000 RM100-2 smoke off + +81xx04xx0101a00180c2030011 FS20 dev: 1234 button: 03 on (11) diff --git a/docs/rm100 b/docs/rm100 new file mode 100644 index 000000000..23d1e9ef8 --- /dev/null +++ b/docs/rm100 @@ -0,0 +1,28 @@ + +810e041f0213a001d396000000000000 + +// Reset: +810e04ea0293a001ae06000000000000 +// Normal: +810e046a0213a001ae06000000000000 + +========== +810e04ba0293a001b1d3000000000000 +810e04a90293a0016a09000000000000 + +By andikt: +// Batteries are inserted into the smoke detector +810e04540293a001b16d000000000000 +[the same comes 10 times] + +// No smoke applied, status messages each 30min +810e04d40213a001b16d000000000000 + +// Smoke applied, detector beeps loud +810e04d70213a001b16d000003000000 + +// no more smoke, detector stops beeping, red LED flashes for some more +// seconds +810e04d40213a001b16d000000000000 + + diff --git a/docs/snippet_1.txt b/docs/snippet_1.txt new file mode 100644 index 000000000..96afe8696 --- /dev/null +++ b/docs/snippet_1.txt @@ -0,0 +1,74 @@ +Startup +------- +1a + PUT C9 - 02 01 1F 64 + 1.st fhz1000: C9 - 01 02 1F 02 78 05 5C 24 FF + 2.nd fhz1000: C9 - 01 02 1F 02 78 05 5C 24 FF + 3.rd fhz1000: C9 - 01 02 1F 02 78 05 5C 0A FF +1b + PUT C9 - 02 01 1F 0A + 4.th fhz1000: C9 - 01 02 1F 02 78 07 B6 22 80 +1c + PUT C9 - 02 01 1F 60 + C9 - 01 02 1F 02 78 07 1C 22 80 + +2 + PUT 04 - C9 01 84 57 02 08 + 1.st fhz1000: C9 - 01 02 84 01 08 05 FF 13 6E 21 BC 4B 1B # Serial no: 136E21BC (?) + 3.rd fhz1000: C9 - 01 02 84 01 08 05 FF 13 6E 29 0E 51 18 # Serial no: 136E290E (?) + +3 + PUT: 04 - C9 01 86 + PUT: 04 - C9 01 96 + PUT: C9 - 02 01 61 04 0C 10 12 0A # Set Date to 2004-12-16 14:10 + + +Switching FS20 device on / off +------------------------------ + + PUT: 04 - 020101 - HHHH BT VL + + HHHH: Transmitter (Hauscode) + BT: Button + VL: Values: + 00 => off, + 01 => dim06%, + 02 => dim12%, + 03 => dim18%, + 04 => dim25%, + 05 => dim31%, + 06 => dim37%, + 07 => dim43%, + 08 => dim50%, + 09 => dim56%, + 0A => dim62%, + 0B => dim68%, + 0C => dim75%, + 0D => dim81%, + 0E => dim87%, + 0F => dim93%, + 10 => dim100%, + 11 => on, # Set to previous dim value (before switching it off) + 12 => toggle, # between off and previous dim val + 13 => dimup, + 14 => dimdown, + 15 => dimupdown, + 16 => timer, + 17 => sendstate, + 18 => off-for-timer, + 19 => on-for-timer, + 1a => on-old-for-timer, + 1b => reset, + + The FSST20 switches on for dim*. + + When setting bit 6 (counted from 1) in the value, you can also + send a further byte, the time as suggested in snippet_6. + + The FS20ST only respects this for the values + timer, off-for-timer, on-for-timer + If the timer is set, then it works for dim*, on, *-for-timer + + sendstate does not work for the FS20ST + +================================= diff --git a/docs/snippet_2.txt b/docs/snippet_2.txt new file mode 100644 index 000000000..98008e04b --- /dev/null +++ b/docs/snippet_2.txt @@ -0,0 +1,102 @@ +// Init FHZ1000PC (Studio) mit FHT80b und HMS +<- 81 06 c9 2c 02 01 1f 0a +-> 81 0b c9 fb 01 02 1f 02 78 07 b6 22 80 +<- 81 08 04 ba c9 01 84 5e 0b 03 +-> 81 0a c9 ea 01 02 84 01 03 01 5b 03 +<- 81 05 04 50 c9 01 86 +<- 81 0b 04 __ 02 01 83 HH HH 65 ff 66 ff // FHT80b 1 +<- 81 0b 04 __ 02 01 83 HH HH 65 ff 66 ff // FHT80b 2 +<- 81 06 04 62 c9 01 96 02 +<- 81 0a c9 af 02 01 61 05 0a 0e 16 18 // Datum Uhrzeit + +// Init FHZ1000PC (Standard) mit FHT80b und HMS +<- 81 06 c9 82 02 01 1f 60 +-> 81 0b c9 61 01 02 1f 02 78 07 1c 22 80 +<- 81 08 04 af c9 01 84 57 02 08 +-> 81 0f c9 __ 01 02 84 01 08 07 80 xx xx xx xx 51 0f // Seriennummer +<- 81 05 04 50 c9 01 86 +<- 81 0b 04 __ 02 01 83 HH HH 65 ff 66 ff // FHT80b 1 +<- 81 05 04 60 c9 01 96 +<- 81 0a c9 __ 02 01 61 jj mm tt ss mm // Datum Uhrzeit +<- 81 05 04 4f c9 01 85 // Speicherabfrage +-> 81 07 c9 ac 01 02 85 01 23 // 23 ?bytes? frei +<- 81 0b 04 __ 02 01 83 HH HH 65 ff 66 ff // FHT80b 2 +<- 81 05 04 4f c9 01 85 // Speicherabfrage +-> 81 07 c9 a5 01 02 85 01 1c // 1c ?bytes? frei + +// FHZ1000PC +<- 81 0a c9 __ 02 01 61 jj mm tt ss mm // Uhrzeit 50 mal am Anfang einer Minute + +<- 81 05 04 4f c9 01 85 // Speicherabfrage +-> 81 07 c9 __ 01 02 85 01 xx // xx = ?bytes? frei + +// FHT80b +// SB=StartByte LL=Laenge TT=TelegrammType BC=BlockCheck ST=Status +// HH=Hauscode +// SB LL TT BC CODE FUNKT ST Param +-> 81 0c 04 __ 09 09 a0 01 HH HH 00 00 _6 xx // Istwert Stellantrieb +-> 81 0c 04 __ 09 09 a0 01 HH HH 00 00 2c xx // Synczeit +-> 81 0c 04 __ 09 09 a0 01 HH HH 14 00 69 xx // Montag von1 in 1/6 Stunde (10 Min.) +-> 81 0c 04 __ 09 09 a0 01 HH HH 15 00 69 xx // Montag bis1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 16 00 69 xx // Montag von2 (90 = frei) +-> 81 0c 04 __ 09 09 a0 01 HH HH 17 00 69 xx // Montag bis2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 18 00 69 xx // Dienstag von1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 19 00 69 xx // bis 1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1a 00 69 xx // von2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1b 00 69 xx // bis2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1c 00 69 xx //Mittwoch von1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1d 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 1e 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 1f 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 20 00 69 xx // Donnerstag +-> 81 0c 04 __ 09 09 a0 01 HH HH 21 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 22 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 23 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 24 00 69 xx // Freitag +-> 81 0c 04 __ 09 09 a0 01 HH HH 25 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 26 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 27 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 28 00 69 xx // Samstag +-> 81 0c 04 __ 09 09 a0 01 HH HH 29 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2a 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2b 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2c 00 69 xx // Sonntag +-> 81 0c 04 __ 09 09 a0 01 HH HH 2d 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2e 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2f 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 3e 00 69 xx // 0=auto 1=manuell 2=Urlaub +-> 81 0c 04 __ 09 09 a0 01 HH HH 3f 00 69 xx // Urlaub Endeuhrzeit oder Endetag +-> 81 0c 04 __ 09 09 a0 01 HH HH 40 00 69 xx // Urlaub Endetag 10=heute 11=morgen 0x=Endemonat +-> 81 0c 04 __ 09 09 a0 01 HH HH 41 00 69 xx // aktuelle Solltemperatur (x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 42 00 69 xx // ist Temperatur (x/10) +-> 81 0c 04 __ 09 09 a0 01 HH HH 43 00 69 xx // ????? +-> 81 0c 04 __ 09 09 a0 01 HH HH 44 00 69 xx // bit0=Batterie 0=OK 1=leer bit5=Fenster 0=zu 1=offen +-> 81 0c 04 __ 09 09 a0 01 HH HH 4b 00 67 xx // ????? +-> 81 0c 04 __ 09 09 a0 01 HH HH 82 00 69 xx // Tag Temperatur (x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 84 00 69 xx // Nacht Temperatur (x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 85 00 69 04 // ????? +-> 81 0c 04 __ 09 09 a0 01 HH HH 8a 00 69 xx // Fenster offen Temperatur (x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 7e 00 67 xx // ????? + +<- 81 09 04 __ 02 01 83 HH HH 3e xx // Modus setzen 00=auto 01=manu 02=Urlaub +-> 81 0b 04 __ 84 09 83 01 HH HH 3e xx yy // Übernahmequittung yy=Speicherstelle +<- 81 09 04 __ 02 01 83 HH HH 41 xx // Solltemp setzen (x=soll/0.5) +Die anderen Register können genauso verändert werden. + +//HMS100TF +-> 81 0e 04 __ 05 10 a0 01 HH HH 00 00 ss tt gu ff // Temp = (utt*0.04) Feuchte = (ffg*100/4096) + + // Status bit7=Negative Temp. bit6=Batterie + +//ST-2 +<- 81 09 04 __ 02 01 01 a0 01 HH HH xx yy // xx=Taste yy=Funktion +//Funktionen +00 Aus +01-10 Dimmen direkt +11 Ein +12 Dimmen Eintastenmodus +13 Dimmen runter +14 Dimmen rauf +15 Umschalten +16 Timerprogrammierung start/stop +1b Auslieferungszustand diff --git a/docs/snippet_3.txt b/docs/snippet_3.txt new file mode 100644 index 000000000..d8544c88c --- /dev/null +++ b/docs/snippet_3.txt @@ -0,0 +1,60 @@ +// FHT80b +// SB=StartByte LL=Laenge TT=TelegrammType CC=CRC ST=Status HH=Hauscode + +// SB LL TT CC CODE FUNKT ST Param +-> 81 0c 04 __ 09 09 a0 01 HH HH 00 00 _6 xx // Istwert Stellantrieb +-> 81 0c 04 __ 09 09 a0 01 HH HH 00 00 2c xx // Synczeit ??? +-> 81 0c 04 __ 09 09 a0 01 HH HH 14 00 69 xx // Montag von1 in 1/6 +Stunde (10 Min.) +-> 81 0c 04 __ 09 09 a0 01 HH HH 15 00 69 xx // Montag bis1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 16 00 69 xx // Montag von2 (90 = frei) +-> 81 0c 04 __ 09 09 a0 01 HH HH 17 00 69 xx // Montag bis2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 18 00 69 xx // Dienstag von1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 19 00 69 xx // bis 1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1a 00 69 xx // von2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1b 00 69 xx // bis2 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1c 00 69 xx // Mittwoch von1 +-> 81 0c 04 __ 09 09 a0 01 HH HH 1d 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 1e 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 1f 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 20 00 69 xx // Donnerstag +-> 81 0c 04 __ 09 09 a0 01 HH HH 21 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 22 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 23 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 24 00 69 xx // Freitag +-> 81 0c 04 __ 09 09 a0 01 HH HH 25 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 26 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 27 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 28 00 69 xx // Samstag +-> 81 0c 04 __ 09 09 a0 01 HH HH 29 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2a 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2b 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2c 00 69 xx // Sonntag +-> 81 0c 04 __ 09 09 a0 01 HH HH 2d 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2e 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 2f 00 69 xx +-> 81 0c 04 __ 09 09 a0 01 HH HH 3e 00 69 xx // 0=auto 1=manuell +2=Urlaub lang, 3=Urlaub kurz +-> 81 0c 04 __ 09 09 a0 01 HH HH 3f 00 69 xx // für Mode3 Uhrzeit, für +Mode2 Endtag des Monats +-> 81 0c 04 __ 09 09 a0 01 HH HH 40 00 69 xx // für Mode3 Resttage ab +aktuellem Datum, für Mode2 Endmonat +-> 81 0c 04 __ 09 09 a0 01 HH HH 41 00 69 xx // aktuelle Solltemperatur +(x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 42 00 69 xx // ist Temperatur (Teil 1) +-> 81 0c 04 __ 09 09 a0 01 HH HH 43 00 69 xx // Ist-Temperatur (Teil 2) +(Die Ermittlung der Temperatur erfolgt nach der Formel Ist-Temp = (Teil2 +* 255 +Teil1) / 10) +-> 81 0c 04 __ 09 09 a0 01 HH HH 44 00 69 xx // Status (Bit0=LowBatt, +Bit1=Untertemperatur, Bit4=Störung Fenterkontakt, Bit5=Fenster auf) +-> 81 0c 04 __ 09 09 a0 01 HH HH 45 00 69 xx // Manuelle Temperatur ?? + +-> 81 0c 04 __ 09 09 a0 01 HH HH 4b 00 67 xx // ????? +-> 81 0c 04 __ 09 09 a0 01 HH HH 7e 00 67 xx // ????? +-> 81 0c 04 __ 09 09 a0 01 HH HH 82 00 69 xx // Komfort-Temperatur +(x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 84 00 69 xx // Absenk-Temperatur +(x*0.5) +-> 81 0c 04 __ 09 09 a0 01 HH HH 85 00 69 04 // Alarm-Temp.-Differenz +-> 81 0c 04 __ 09 09 a0 01 HH HH 8a 00 69 xx // Fenster offen Temperatur +(x*0.5) diff --git a/docs/snippet_4.txt b/docs/snippet_4.txt new file mode 100644 index 000000000..a59776588 --- /dev/null +++ b/docs/snippet_4.txt @@ -0,0 +1,39 @@ + fs20: + Modulation: 100% Amplitude, also pulse und spaces + + Keine festen Bitraster, ein Bit besteht immer aus pulse UND space + + Bit 0: pulse+space insgesamt ca 800us, + Bit 1: ca. 1200us + + Beginn einer Uebertragung: ca 20 Startbits 0, zum Start eine 1 + + dann Uebertragung der Datenbytes jeweils 8 Bit + Pruefbit: + msb + ... + lsb + pruefbit = xor-Verknuepfung der Datenbits. + + Die Uebertragung hat folgenden Aufbau: + 1. hausadr1 + 2. hausadr0 + 3. adr + 4. cmd + 5. arg1 + ... + n-1. argn optional + n. pruefbyte + + pruefbyte = unterste 8 Bit aus summe aller bytes 1..n-1 +6 + + Hauscode=hausadr1*256 + hausadr + + Dass ein hauscode 11111111 tatsaechlich fuer 0 steht, ist wohl klar. + + cmd 0..31: Keine Argumente + cmd 32..63: Ein Argumentbyte + Jede Uebertragung wird 3* ausgesendet. + + + Grueße, + automat diff --git a/docs/snippet_5.txt b/docs/snippet_5.txt new file mode 100644 index 000000000..10f7c2605 --- /dev/null +++ b/docs/snippet_5.txt @@ -0,0 +1,60 @@ +Fs20-Codes + 0 Aus + 1..15 setze auf Helligkeit + 16 An auf max + 17 An alter wert + 18 Toggel + 19 Dim UP + 20 Dim Down + 21 DIM up/down + 22 Prog.Modus/Zeit-Messung + 23 Adressmeldung / Nix + 24 Sofort Aus - Timer, sofort wieder an, evtl slow off + 25 Sofort Max - timer, sofort aus + 26 Sofort An alter Wert - timer, sofort aus + 27 ?? AUS - RESET? + 28 ? + 29 ? + 30 sofort Max, timer, dann sofort alter wert + 31 sofort An alter Wert, nach Timer - sofort Aus + + 32-47 S auf Helligkeit mit Speed S + 48 S An auf max, + 49 S An alter wert + 50 T Toggel fuer T, dann alter Wert + 51 T Dim up, T ? + 52 T Dim down, T ? + 53 T Dim up/down, T ? + 54 T Programmiere Einschaltzeit + 55 ? + 56 T Aus fuer EinT + 57 T Ein Max fuer T, dann alter Wert + 58 T Ein alter Wert fuer T, dann AUS + 59 ??? RESET + 60 T Progr. Einschalt-Speed mit T + 61 T Progr. Ausschalt-Speed mit T + 62 T Ein Max fuer T, dann alter Wert + 63 T Alter Wert fuer T, dann wieder aktueller Wert + + S und T Argumente 2. Byte. Zeit T=0: endlos + +Übrigens ein paar Anfänge zum HMS-Protokoll: +adr1 adr2 typ arg1 arg2/seqnr arg3 + + typ: + 0 temp/feucht hms100tf + 1 temp hms100t + 2 Wasser hms100w/wd + 3 Rauch hms100rm + 4 TuerFenster hms100tfk + 5 TuerFenster hms100tfk + 6 Gas hms100 MG/PG/CO + 7 Gas hms100 MG/PG/CO + 8 Gas hms100 MG/PG/CO + 9-13 unbekannt + 14 hms100 FI + 15 ? + +16 ?? + +32 Batteriewarnung + +64 neue Batterie + +128 Vorzeichen diff --git a/docs/snippet_6.txt b/docs/snippet_6.txt new file mode 100644 index 000000000..d98c90a91 --- /dev/null +++ b/docs/snippet_6.txt @@ -0,0 +1,127 @@ +S20-Funkschaltsystem + +1. Protokoll + +1.1. Datenrahmen: + + Hauscode 16 bit + Adresse 8 bit + Befehl 8 bit (16bit, wenn im ersten Befehlsbyte das Erweiterungsbit gesetzt + ist.) + Quersumme 8 bit + +1.1.1. Hauscode: + 0-65535 + Hauscode1: 8bit (High-Byte) + Hauscode2: 8bit (Low-Byte) + +1.1.2. Adresse: + High-Nibble (4bit): Adreß-Gruppe (Bank / Raum) + 15 = Master- / Funktionsgruppen-Bereich + 0-14 = Adreßraum für Einzeladressen + Low-Nibble (4bit): Unter-Adresse + 15 = alle der Adreß-Gruppe (lokal Master) + 0-14 = Einzel-Adresse / Adresse der Funktionsgruppe + + Das High-Nibble wählt die Adreß-Gruppe aus. Es stehen 15 Adreß-Gruppen zur + Verfügung. Die Adreß-Gruppe 15 wählt den Master- bzw. + Funktionsgruppen-Adreßbereich aus. Das Low-Nibble bestimmt innerhalb der + gewählten Adreß-Gruppe die anzusprechende Unter-Adresse. Zum Ausführen einer + globalen Master-Funktion müssen High- und Low-Nibble der Adresse 15 sein! + + + Reihenfolge der Eingabe und Speicherung von Hauscode und Adresse beim Setup + von Sendern: + 1. HC1: [A1|A0|B1|B0|C1|C0|D1|D0] + 2. HC2: [E1|E0|F1|F0|G1|G0|H1|H0] + 3. Adr: [I1|I0|J1|J0|K1|K0|L1|L0] + +1.1.3. Befehl: + Unteren 5 bit: + 0 00h aus + 1 01h an, 6,25% Einschalten auf Helligkeitsstufe 1 (min.) + 2 02h an, 12,5% + ... + 15 0fh an, 93,75% + 16 10h an, 100% Einschalten auf Helligkeitsstufe 16 (max.) + + 17 11h an, alter Wert Auf letztem Helligkeitswert einschalten + 18 12h toggle Wechsel zwischen aus und an, alter Wert + 19 13h dim up Eine Helligkeitsstufe heller + 20 14h dim down Eine Helligkeitsstufe dunkler + 21 15h dim up and down ..., + bis max, kurz warten, - bis min, kurz warten, + 22 16h timeset Timerprogrammierung (Start, Ende) + 23 17h send status Nur bei bidirektionalen Komponenten! + 24 18h aus, für Timerzeit + 25 19h an, 100%, für Timerzeit + 26 1ah an, alter Wert, für Timerzeit + 27 1bh reset (auf Auslieferzustand) + 28 1ch frei + 29 1dh frei + 30 1eh frei + 31 1fh frei + + Bit 5: Erweiterungsbit (0 = ohne, 1 = Erweiterungsbyte zwischen Befehl und + Quersumme) + Bit 6: bidirektionaler Befehl (normal = 0) + Bit 7: Antwort eines Empfängers (normal = 0) + + Erweiterungsbyte bei gesetztem Erweiterungsbit: + Ist im ersten Befehlsbyte das Erweiterungsbit gesetzt, wird ein + Erweiterungsbyte eingeschoben. + + Für die Befehle 0 bis 18 und 24 bis 26 gilt folgende Codierung des + Erweiterungsbytes: + Das Byte gibt die einmalige Zeitdauer für den Timer in Schritten von 0,25s an. + + Bei Befehl 22 wird der Timer des Empfängers fest auf den übertragenen + Timerwert gestellt. + Das Low-Nibble gibt den Zahlenwert an. Ist es Null (= 0), wird die + Timerfunktion ignoriert und der Verbraucher dauerhaft/sofort geschaltet. + + Das High-Nibble dient als Multiplikator mit dem Faktor 2^x. Es sind nur Werte + kleiner oder gleich 12 sinnvoll. Größere Werte werden auf 12 begrenzt! + + Zeit = 2^(High-Nibble) * Low-Nibble * 0,25s + Die maximale Zeitdauer beträgt damit ca. 4,25Std, die minimale 0,25s, sofern + dies jeweils von den Empfängern bis zu den angegebenen Grenzen unterstützt + wird. + +1.1.4. Quersumme + + 8bit-Summe aus 6, Hauscode, Adresse und Befehl (und Erweiterungsbyte) bilden + Werden Repeater verwendet, so treten auch um 1 oder 2 erhöhte Quersummen auf, + die von den Empfängern im Normalfall akzeptiert werden sollten. Wurde von + einem Empfänger bis 1,6s vor einem Repeater-Befehl ein normaler Befehl + empfangen, so wird der Repeater-Befehl ignoriert. + + +1.1.5. Komplette Übertragung: + Synchr, HC1, Parity, HC2, Parity, Adresse, Parity, Befehl, Parity, Quersumme, Parity, EOT + 13 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 1 bit + + oder: + Synchr, HC1, Parity, HC2, Parity, Adr, Parity, Bef1, Parity, Bef2, Par, Quersumme, Par, EOT + 13 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 8 bit 1 bit 1 bit + + Übertragung beginnt mit MSB. + + Die Übertragung für ein komplettes Datenpaket mit 1 Befehl dauert 47,6ms bis + 65,6ms. + + Wird an der Hand-FB eine Taste < 400ms gedrückt, so wird beim Loslassen + folgendes gesendet: DAT, 10msPAUSE, DAT, 10msPAUSE, DAT, 110msPAUSE + + Wird eine Taste an der Hand-FB länger als 400ms gedrückt, so wird alle 250ms + folgendes gesendet: DAT, 10msPAUSE, DAT + + + Im AUS-Zustand schaltet ein Dimmer bei EIN mit alter Helligkeit ein. + Im AUS-Zustand schaltet ein Dimmer bei DIMUP mit voller Helligkeit ein. + Im EIN-Zustand wird bei DIMUP eine Stufe aufgedimmt. + + Alle Befehle dürfen von Empfängern immer nur 1x ausgewertet werden. Gesendet +wird der Befehl 3x mit einer Pause von 10ms. Dim-Befehle werden nur 2x mit +einer Pause von ca. 10ms und 130ms gesendet. Nach einem erkannten Befehl +ignorieren die Empfänger für 120ms weitere Befehle. Befehle von Repeatern +werden für 1,6s ignoriert. diff --git a/docs/snippet_7.txt b/docs/snippet_7.txt new file mode 100644 index 000000000..d31b5f01d --- /dev/null +++ b/docs/snippet_7.txt @@ -0,0 +1,14 @@ +Get 81 0c c9 78 0102 1f04a0ffb6ffffff +Get 81 0c c9 da 0102 1f04a0ffb661ffff +Set 81 05 04 61 c90197 +Set 81 05 04 63 c90199 +Get 81 0d 04 ac 4027a0017100215000b30f KS300 msg +Set 81 05 04 50 c90186 initHMS +Set 81 06 04 62 c9019602 InitFS20 (+02?) +Set 81 06 c9 2c 02011f0a Init3 string +Set 81 09 04 46 020101xxxx0000 Set FS20 device off +Set 81 09 04 4e c901839e010161 Set fhtcode 97 (0x61) +Set 81 09 04 57 020101xxxx0011 Set FS20 device on +Set 81 0a c9 93 02016106050f1500 Time: 2006-05-15 21:00 +Set 81 0a c9 a4 02016106050f1511 Time: 2006-05-15 21:17 +Set 81 0b 04 8f 020183xxxx65ff66ff Request FHT refreshvalues diff --git a/docs/web-tipps b/docs/web-tipps new file mode 100644 index 000000000..9800e7584 --- /dev/null +++ b/docs/web-tipps @@ -0,0 +1,17 @@ +========================================= +#httpd.conf entry: no password for for distinguished hosts + +ScriptAlias /cgi-bin/ "/home/httpd/cgi-bin/" + + AuthType Basic + AuthName "Password Required" + AuthUserFile /home/httpd/etc/passwd + Require valid-user + Order deny,allow + Deny from all + Allow from 192.168.0.207 + Allow from 192.168.0.208 + Satisfy any + + +========================================= diff --git a/examples/01_fs20 b/examples/01_fs20 new file mode 100644 index 000000000..3f31715a1 --- /dev/null +++ b/examples/01_fs20 @@ -0,0 +1,18 @@ +# +# fhem.pl configfile +# +# Define a lamp (which is plugged in via an FS20ST). +# To program the FS20ST, start the server, plug the FS20ST in pressing its +# button, and then execute fhem.pl 7072 "set lamp on" +# + +# Common part +logfile /tmp/fhem-%Y-%m.log +savefile /tmp/fhem.save # where to save the state of the devices +verbose 3 # "normal" verbosity (min 1, max 5) +port 7072 # our TCP/IP port (working from localhost only) +modpath . # where our FHEM directory is +define FHZ FHZ /dev/tts/USB0 # the serial port of an FHZ 1000 PC + + +define lamp FS20 8765 01 # type FS20, transmitter code 8765, button 2 diff --git a/examples/02_fs20 b/examples/02_fs20 new file mode 100644 index 000000000..d0a374aef --- /dev/null +++ b/examples/02_fs20 @@ -0,0 +1,72 @@ +# +# fhem.pl configfile +# +# We have 2 rollades (which are connected via the FS20MS). Button 3 on the +# FS20S20 should activate both rollades. There are three solutions: +# 1. Builtin commands +# 2. Perl expression +# 3. Shell script (realized via external script at the end of this file) + + +# Common part +logfile /tmp/fhem-%Y-%m.log +savefile /tmp/fhem.save # where to save the state of the devices +verbose 3 # "normal" verbosity +port 7072 # our TCP/IP port (working from localhost only) +modpath . # where our FHEM directory is +define FHZ FHZ /dev/tts/USB0 # the serial port of an FHZ 1000 PC + + +define roll1 FS20 7777 02 # type FS20, transmitter code 7777, button 3 +define roll2 FS20 7777 03 # type FS20, transmitter code 7777, button 4 +define btn3 FS20 8765 03 # define a button from the FS20S20 +setstate roll1 off # initial state is closed + +# Note: Only one of the methods should be used + +# Method 1a: builtin commands. Note the double ; +notifyon btn3 set roll1 %;; set roll2 % + +# Method 1b: shorter: +notifyon btn3 set roll1,roll2 % + +# Method 2a: perl. +notifyon btn3 { fhz "set roll1 %;; set roll2 %" } + +# Method 2b: perl. open the rollades only to a certain amount if they are +# closed. Else do the required command. +notifyon btn3 {\ + if("%" eq "on" && $value{roll1} eq "off") {\ + fhz "set roll1 on-for-timer 10";;\ + fhz "set roll2 on-for-timer 16";;\ + } else { \ + fhz "set roll1,roll2 %"\ + } \ +} + +# Method 3: shell. The script follows after "quit". Dont forget to chmod u+x it. +notifyon btn3 "/usr/local/bin/roll.sh %" + +quit # Ignore the rest of this file + +#!/bin/sh +# +# roll1 needs 10 sec to open to a certain level, roll2 16 sec. The following +# shell script opens both of them when called woth "on", and closes both of +# them else. We rely on the fact, that the FS20MS switches off after ca 60s. +# +# Note that for greater time values the FS20 timer gets inaccurate, you +# can use something like +# $fhz 7072 "set roll1 on; at +00:00:21 set roll1 on" +# instead. +# + +fhz=/usr/local/bin/fhem.pl + +if test $1 = "on"; then + $fhz 7072 "set roll1 on-for-timer 10 + $fhz 7072 "set roll2 on-for-timer 16 +else + $fhz 7072 "set roll1,roll2 off" +fi + diff --git a/examples/03_fht b/examples/03_fht new file mode 100644 index 000000000..096bbe93f --- /dev/null +++ b/examples/03_fht @@ -0,0 +1,30 @@ +# +# fhem.pl configfile +# +# Define an FHT80b device. You have to know its transmitter code, +# or set verbose to 4 and wait for a while watching the log. +# +# wz stands for "wohnzimmer". +# After about 5-10 minutes, check if "list wz" returns something meaningful +# + +logfile /tmp/fhem-%Y-%m.log +savefile /tmp/fhem.save # where to save the state of the devices +verbose 3 # "normal" verbosity +port 7072 # our TCP/IP port (working from localhost only) +modpath . # where our FHEM directory is +define FHZ FHZ /dev/tts/USB0 # the serial port of an FHZ 1000 PC + +define wz FHT 3232 # type FHT, transmitter code 3232 (default value) + +######################### +# Some documentation suggests that the FHZ time should be set every minute. +# I only set it once a day. +at *03:30:00 set FHZ time + +######################### +# If you wish to have up-to date information on certain strange parameters +# then comment out the line below. My devices send a message when a value +# changes, and send measured-temp, actuator and state messages regularly. +# Be patient: the reply comes in 5-10 minutes. +at *04:00:00 set wz refreshvalues diff --git a/examples/04_log b/examples/04_log new file mode 100644 index 000000000..0bcb39257 --- /dev/null +++ b/examples/04_log @@ -0,0 +1,49 @@ +# +# fhem.pl configfile +# Logging FS20/KS300 data +# See the file fht.gnuplot for displaying the logged data (or webfrontend/pgm2) +# + +logfile /tmp/fhem-%Y-%m.log +savefile /tmp/fhem.save # where to save the state of the devices +verbose 3 # "normal" verbosity +port 7072 # our TCP/IP port (working from localhost only) +modpath . # where our FHEM directory is +define FHZ FHZ /dev/tts/USB0 # the serial port of an FHZ 1000 PC + +define wz FHT 3232 # type FHT, transmitter code 3232 (default value) +define ks1 KS300 1234 250 # type KS300, with 250ml rain / counter + + +######################### +# Log temperature and actuator changes into a file, its name changes weekly +define wzlog FileLog /var/tmp/wz-%Y-%U.log wz:.*(temp|actuator).* + +# ks300 log +define kslog FileLog /var/log/wz-%Y-%U.log ks1:.*H:.* +define avglog FileLog /var/log/avg.log ks1:.*avg.* + +############################## +# Alternative log method. It does the same, but it is somewhat slower as it +# starts the shellscript below. Don't forget the "", as some values contain +# paranthesis, and your shell will probably bark. +notifyon wz:temp.* "/usr/local/bin/log.sh @ "@ %"" + +############################## +# If using the frontends pgm2 or pgm3, then you can put the devices +# into separate rooms, see the corresponding README: +attr wz room InDoor +attr wzlog room InDoor +attr ks1 room OutDoor +attr kslog room OutDoor +attr avglog room OutDoor + +quit + +######################### +And here is /usr/local/bin/log.sh, don't forget chmod +x + +#!/bin/sh +fname=$1 +shift +echo `date +"%Y-%m-%d_%H:%M:%S"` "$*" >> /var/log/$fname.log diff --git a/examples/05_rm100 b/examples/05_rm100 new file mode 100644 index 000000000..14a06e8a2 --- /dev/null +++ b/examples/05_rm100 @@ -0,0 +1,23 @@ +# +# fhem.pl configfile +# +# Define RM100-2 devices: +# +# As the RM100-2 changes its code after the battery is changed (or the switch +# on the device itself is changed), we map _all_ RM100-2 to the device id 1001 +# Check the commandref.html define, Type HMS section for details + +logfile /tmp/fhem-%Y-%m.log +savefile /tmp/fhem.save # where to save the state of the devices +verbose 3 # "normal" verbosity (min 1, max 5) +port 7072 # our TCP/IP port (working from localhost only) +modpath . # where our FHEM directory is +define FHZ FHZ /dev/tts/USB0 # the serial port of an FHZ 1000 PC + + +define rm100 HMS 1001 # type HMS +define rm100log FileLog /var/log/wz-%Y-%U.log rm100:.* +notifyon rm100:smoke.*on "wall "FIRE: @ %"" + +# Test the log/notify +# fhem.pl 7072 'trigger rm100 smoke on' diff --git a/examples/06_at b/examples/06_at new file mode 100644 index 000000000..7cf2659f4 --- /dev/null +++ b/examples/06_at @@ -0,0 +1,39 @@ +# These are only examples for the at and notify command + + +################################## +# absolute ones: +at 17:00:00 set lamp on # fhz command +at 17:00:00 { Log 1, "Teetime" } # Perl command +at 17:00:00 "/bin/echo "Teetime" > /dev/console" # shell command +at *17:00:00 set lamp on # repeat every day + +################################## +# relative ones +at +00:00:10 set lamp on # switch the lamp on in 10 seconds +at +00:00:02 set lamp on-for-timer 1 # Blink once in 2 seconds +at +*{3}00:00:02 set lamp on-for-timer 1 # Blink 3 times + +################################## +# Switch the lamp on from sunset to 11 PM each day +# You have to install 99_SUNRISE.pm in the FHEM directory to have sunset() +# We have to use the relative versions, as the next event is computed now +{ sunrise_coord("8.686", "50.112", "Europe/Berlin") } +at +*{sunset_rel()} set lamp on +at *23:00:00 set lamp off + +################################## +# A more elegant solution, which even works if sunset is after 23:00 +at +*{sunset_rel()} set lamp on-till 23:00 + +################################## +# Only do this on weekend. For the preset perl variables see commandref.html +at +*{sunset_rel()} { fhz("set lamp on-till 23:00") if($we) } + +################################## +# Switch lamp1 and lamp2 on from 7:00 till 10 minutes after sunrise +at *07:00 set lamp1,lamp2 on-till {sunrise_abs(+600)} + +################################## +# Blink 3 times if the piri sends a command +notify piri:on.* at +*{3}00:00:02 set lamp on-for-timer 1 diff --git a/fhem.pl b/fhem.pl new file mode 100755 index 000000000..eeba12403 --- /dev/null +++ b/fhem.pl @@ -0,0 +1,1659 @@ +#!/usr/bin/perl + +my $version = "=VERS= from =DATE="; + +################################################################ +# +# Copyright notice +# +# (c) 2005 Copyright: Rudolf Koenig (r dot koenig at koeniglich dot de) +# All rights reserved +# +# This script 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! +# Thanks for Tosti's site () +# for inspiration. +# +# Homepage: http://www.koeniglich.de/fhem/fhem.html + + +use strict; +use warnings; +use IO::File; +use IO::Socket; +use Net::hostent; +use Time::HiRes qw(gettimeofday); + + +################################################## +# Forward declarations +# +sub AnalyzeInput($); +sub AnalyzeCommand($$); +sub AnalyzeCommandChain($$); +sub IOWrite($@); +sub AssignIoPort($); +sub InternalTimer($$$); +sub fhz($); +sub CommandChain($$); +sub DoClose($); +sub HandleTimeout(); +sub Log($$); +sub OpenLogfile($); +sub ResolveDateWildcards($@); +sub SignalHandling(); +sub TimeNow(); +sub DoSavefile(); +sub SemicolonEscape($); +sub XmlEscape($); + +sub CommandAt($$); +sub CommandAttr($$); +sub CommandDefine($$); +sub CommandDelete($$); +sub CommandFhzDev($$); +sub CommandGet($$); +sub CommandHelp($$); +sub CommandInclude($$); +sub CommandInform($$); +sub CommandList($$); +sub CommandLogfile($$); +sub CommandModpath($$); +sub CommandNotifyon($$); +sub CommandPidfile($$); +sub CommandPort($$); +sub CommandRereadCfg($$); +sub CommandQuit($$); +sub CommandSavefile($$); +sub CommandSet($$); +sub CommandSetstate($$); +sub CommandSleep($$); +sub CommandShutdown($$); +sub CommandVerbose($$); +sub CommandXmlList($$); +sub CommandTrigger($$); + +################################################## +# Variables: +# global, to be able to access them from modules +use vars qw(%defs); # FHEM device/button definitions +use vars qw(%logs); # Log channels +use vars qw(%attr); # Attributes +use vars qw(%value); # Current values, see commandref.html +use vars qw(%oldvalue); # Old values, see commandref.html +use vars qw(%devmods); # List of loaded device modules + +my %ntfy; +my %at; + +my $server; # Server socket +my $verbose = 0; +my $logfile; # logfile name, if its "-" then wont background +my $currlogfile; # logfile, without wildcards +my $logopened; +my %client; # Client array +my %logmods; # List of loaded logger modules +my $savefile = ""; # Save ste info and at cmd's here +my $nextat; +my $rcvdquit; # Used for quit handling in init files +my $configfile=$ARGV[0]; +my $sig_term = 0; # if set to 1, terminate (saving the state) +my $modpath_set; # Check if modpath was used, and report if not. +my $global_cl; # To use from perl snippets +my $devcount = 0; # To sort the devices +my %intAt; # Internal at timer hash. +my $intAtCnt=0; +my $init_done = 0; +my $pidfilename; + + +my %cmds = ( + "?" => { Fn=>"CommandHelp", + Hlp=>",get this help" }, + "at" => { Fn=>"CommandAt", + Hlp=>" ,issue a command at a given time" }, + "attr" => { Fn=>"CommandAttr", + Hlp=>" ,set attributes for " }, + "define" => { Fn=>"CommandDefine", + Hlp=>" ,define a code" }, + "delete" => { Fn=>"CommandDelete", + Hlp=>"{def|ntfy|at} name,delete the corresponding definition"}, + "get" => { Fn=>"CommandGet", + Hlp=>" ,request data from " }, + "help" => { Fn=>"CommandHelp", + Hlp=>",get this help" }, + "include" => { Fn=>"CommandInclude", + Hlp=>",read the commands from " }, + "inform" => { Fn=>"CommandInform", + Hlp=>"{on|off},echo all commands and events to this client" }, + "list" => { Fn=>"CommandList", + Hlp=>"[device],list definitions and status info" }, + "logfile" => { Fn=>"CommandLogfile", + Hlp=>"filename,use - for stdout" }, + "modpath" => { Fn=>"CommandModpath", + Hlp=>",the directory where the FHEM subdir is" }, + "notifyon"=> { Fn=>"CommandNotifyon", + Hlp=>" ,exec when recvd signal for " }, + "pidfile" => { Fn=>"CommandPidfile", + Hlp=>"filename,write the process id into the pidfile" }, + "port" => { Fn=>"CommandPort", + Hlp=>" [global],TCP/IP port for the server" }, + "quit" => { Fn=>"CommandQuit", + Hlp=>",end the client session" }, + "reload" => { Fn=>"CommandReload", + Hlp=>",reload the given module (e.g. 99_PRIV)" }, + "rereadcfg" => { Fn=>"CommandRereadCfg", + Hlp=>",reread the config file" }, + "savefile"=> { Fn=>"CommandSavefile", + Hlp=>",on shutdown save all states and at entries" }, + "set" => { Fn=>"CommandSet", + Hlp=>" ,transmit code for " }, + "setstate"=> { Fn=>"CommandSetstate", + Hlp=>" ,set the state shown in the command list" }, + "shutdown"=> { Fn=>"CommandShutdown", + Hlp=>",terminate the server" }, + "sleep" => { Fn=>"CommandSleep", + Hlp=>",sleep for usecs" }, + "trigger" => { Fn=>"CommandTrigger", + Hlp=>" ,trigger notify command" }, + "verbose" => { Fn=>"CommandVerbose", + Hlp=>",verbosity level, 0-5" }, + "xmllist" => { Fn=>"CommandXmlList", + Hlp=>",list definitions and status info as xml" }, +); + + + +################################################### +# Start the program +if(int(@ARGV) != 1 && int(@ARGV) != 2) { + print "Usage:\n"; + print "as server: fhem configfile\n"; + print "as client: fhem [host:]port cmd\n"; + CommandHelp(undef, undef); + exit(1); +} + +################################################### +# Client code +if(int(@ARGV) == 2) { + my $buf; + my $addr = $ARGV[0]; + $addr = "localhost:$addr" if($ARGV[0] !~ m/:/); + $server = IO::Socket::INET->new(PeerAddr => $addr); + die "Can't connect to $addr\n" if(!$server); + syswrite($server, "$ARGV[1] ; quit\n"); + my $err = 0; + while(sysread($server, $buf, 256) > 0) { + print($buf); + $err = 1; + } + exit($err); +} + +my $ret = CommandInclude(undef, $configfile); +die($ret) if($ret); + +if($logfile ne "-") { + defined(my $pid = fork) || die "Can't fork: $!"; + exit(0) if $pid; +} + +die("No modpath specified in the configfile.\n") if(!$modpath_set); + +if($savefile && -r $savefile) { + $ret = CommandInclude(undef, $savefile); + die($ret) if($ret); +} +SignalHandling(); + + +Log 0, "Server started (version $version, pid $$)"; + +################################################ +# Main loop + +$init_done = 1; +CommandPidfile(undef, $pidfilename) if($pidfilename); + +while (1) { + my ($rout, $rin) = ('', ''); + + vec($rin, $server->fileno(), 1) = 1; + foreach my $p (keys %defs) { + vec($rin, $defs{$p}{FD}, 1) = 1 if($defs{$p}{FD}); + } + foreach my $c (keys %client) { + vec($rin, fileno($client{$c}{fd}), 1) = 1; + } + + my $nfound = select($rout=$rin, undef, undef, HandleTimeout()); + + CommandShutdown(undef, undef) if($sig_term); + + if($nfound < 0) { + next if ($! == EAGAIN() || $! == EINTR() || $! == 0); + die("Select error $nfound / $!\n"); + } + + ############################### + # Message from the hardware (FHZ1000/WS3000/etc) + foreach my $p (keys %defs) { + next if(!$defs{$p}{FD} || !vec($rout, $defs{$p}{FD}, 1)); + no strict "refs"; + &{$devmods{$defs{$p}{TYPE}}{ReadFn}}($defs{$p}); + use strict "refs"; + } + + if(vec($rout, $server->fileno(), 1)) { + my @clientinfo = $server->accept(); + if(!@clientinfo) { + Print("ERROR", 1, "016 Accept failed for admin port"); + next; + } + my @clientsock = sockaddr_in($clientinfo[1]); + my $fd = $clientinfo[0]; + $client{$fd}{fd} = $fd; + $client{$fd}{addr} = inet_ntoa($clientsock[1]) . ":" . $clientsock[0]; + $client{$fd}{buffer} = ""; + Log 4, "Connection accepted from $client{$fd}{addr}"; + } + + foreach my $c (keys %client) { + + next unless (vec($rout, fileno($client{$c}{fd}), 1)); + + my $buf; + my $ret = sysread($client{$c}{fd}, $buf, 256); + if(!defined($ret) || $ret <= 0) { + DoClose($c); + next; + } + if(ord($buf) == 4) { # EOT / ^D + CommandQuit($c, ""); + next; + } + $buf =~ s/\r//g; + $client{$c}{buffer} .= $buf; + AnalyzeInput($c); + } +} + +################################################ +sub +IsDummy($) +{ + my $dev = shift; + + return 1 if(defined($attr{$dev}) && defined($attr{$dev}{dummy})); + return 0; +} + +################################################ +sub +GetLogLevel($) +{ + my $dev = shift; + + return $attr{$dev}{loglevel} + if(defined($attr{$dev}) && defined($attr{$dev}{loglevel})); + return 2; +} + + +################################################ +sub +Log($$) +{ + my ($loglevel, $text) = @_; + + return if($loglevel > $verbose); + + my @t = localtime; + my $nfile = ResolveDateWildcards($logfile, @t); + OpenLogfile($nfile) if($currlogfile && $currlogfile ne $nfile); + my $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d", + $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0]); + +# my ($seconds, $microseconds) = gettimeofday(); +# $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d.%03d", +# $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0], $microseconds/1000); + + if($logopened) { + print LOG "$tim $loglevel: $text\n"; + } else { + print "$tim $loglevel: $text\n"; + } + return undef; +} + + +##################################### +sub +DoClose($) +{ + my $c = shift; + + Log 4, "Connection closed for $client{$c}{addr}"; + close($client{$c}{fd}); + delete($client{$c}); + return undef; +} + +##################################### +sub +IOWrite($@) +{ + my ($hash, @a) = @_; + + my $iohash = $hash->{IODev}; + if(!$iohash) { + Log 5, "No IO device found for $hash->{NAME}"; + return; + } + + no strict "refs"; + &{$devmods{$iohash->{TYPE}}{WriteFn}}($iohash, @a); + use strict "refs"; +} + +##################################### +sub +AnalyzeInput($) +{ + my $c = shift; + + while($client{$c}{buffer} =~ m/\n/) { + my ($cmd, $rest) = split("\n", $client{$c}{buffer}, 2); + $client{$c}{buffer} = $rest; + if($cmd) { + AnalyzeCommandChain($c, $cmd); + return if(!defined($client{$c})); # quit + } else { + $client{$c}{prompt} = 1; + } + syswrite($client{$c}{fd}, "FHZ> ") + if($client{$c}{prompt} && $rest !~ m/\n/); + } +} + +##################################### +sub +AnalyzeCommandChain($$) +{ + my ($c, $cmd) = @_; + $cmd =~ s/#.*$//; + $cmd =~ s/;;/____/g; + foreach my $subcmd (split(";", $cmd)) { + $subcmd =~ s/____/;/g; + AnalyzeCommand($c, $subcmd); + last if($c && !defined($client{$c})); # quit + } +} + +##################################### +# Used from perl oneliners inside of scripts +sub +fhz($) +{ + my $param = shift; + return AnalyzeCommandChain($global_cl, $param); +} + +##################################### +sub +AnalyzeCommand($$) +{ + my ($cl, $cmd) = @_; + + $cmd =~ s/^[ \t]*//; # Strip space + $cmd =~ s/[ \t]*$//; + + Log 5, "Cmd: >$cmd<"; + return if(!$cmd); + + if($cmd =~ m/^{.*}$/) { # Perl code + + # Make life easier for oneliners: + %value = (); + foreach my $d (keys %defs) { $value{$d} = $defs{$d}{STATE } } + my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) = localtime; + my $we = (($wday==0 || $wday==6) ? 1 : 0); + $month++; + $year+=1900; + + $global_cl = $cl; + my $ret = eval $cmd; + $ret = $@ if($@); + if($ret) { + if($cl) { + syswrite($client{$cl}{fd}, "$ret\n") + } else { + Log 3, $ret; + } + } + return $ret; + + } + + if($cmd =~ m/^"(.*)"$/) { # Shell code, always in bg + system("$1 &"); + return; + } + + $cmd =~ s/^[ \t]*//; + my ($fn, $param) = split("[ \t][ \t]*", $cmd, 2); + + return if(!$fn); + + ############# + # Search for abbreviation + if(!defined($cmds{$fn})) { + foreach my $f (sort keys %cmds) { + if(length($f) > length($fn) && substr($f, 0, length($fn)) eq $fn) { + Log 5, "$fn => $f"; + $fn = $f; + last; + } + } + } + + if(!defined($cmds{$fn})) { + if($cl) { + syswrite($client{$cl}{fd}, "Unknown command $fn, try help\n"); + } else { + Log 1, "Unknown command >$fn<, try help"; + } + return; + } + $param = "" if(!defined($param)); + no strict "refs"; + my $ret = &{$cmds{$fn}{Fn} }($cl, $param); + use strict "refs"; + if($ret) { + if($cl) { + syswrite($client{$cl}{fd}, $ret . "\n"); + } else { + Log 1, $ret; + return $ret; + } + } +} + +##################################### +sub +CommandHelp($$) +{ + my ($cl, $param) = @_; + + my $str = "\n" . + "Possible commands:\n\n" . + "Command Parameter Description\n" . + "-----------------------------------------------\n"; + + for my $cmd (sort keys %cmds) { + my @a = split(",", $cmds{$cmd}{Hlp}, 2); + + $str .= sprintf("%-9s %-25s %s\n", $cmd, $a[0], $a[1]); + } + return $str; +} + +sub +CommandInclude($$) +{ + my ($cl, $arg) = @_; + if(!open(CFG, $arg)) { + return "Can't open $arg: $!"; + } + + my $bigcmd = ""; + $rcvdquit = 0; + while(my $l = ) { + chomp($l); + if($l =~ m/^(.*)\\$/) { # Multiline commands + $bigcmd .= $1; + } else { + AnalyzeCommandChain($cl, $bigcmd . $l); + $bigcmd = ""; + } + last if($rcvdquit); + } + close(CFG); + return undef; +} + +##################################### +sub +CommandPort($$) +{ + my ($cl, $arg) = @_; + + my ($port, $global) = split(" ", $arg); + if($global && $global ne "global") { + return "Bad syntax, usage: port [global]"; + } + + close($server) if($server); + $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => ($global ? undef : "localhost"), + LocalPort => $port, + Listen => 10, + ReuseAddr => 1); + + die "Can't open server port at $port\n" if(!$server); + return undef; +} + +##################################### +sub +OpenLogfile($) +{ + my $param = shift; + + close(LOG) if($logfile); + $logopened=0; + $currlogfile = $param; + if($currlogfile eq "-") { + + open LOG, '>&STDOUT' or die "Can't dup stdout: $!"; + + } else { + + open(LOG, ">>$currlogfile") || return("Can't open $currlogfile: $!"); + # Redirect stdin/stderr + + open STDIN, '>$currlogfile") or return "Can't append STDERR to log: $!"; + STDERR->autoflush(1); + + close(STDOUT); + open STDOUT, '>&STDERR' or return "Can't dup stdout: $!"; + STDOUT->autoflush(1); + } + LOG->autoflush(1); + $logopened = 1; + return undef; +} + +##################################### +sub +CommandLogfile($$) +{ + my ($cl, $param) = @_; + + $logfile = $param; + + my @t = localtime; + my $ret = OpenLogfile(ResolveDateWildcards($param, @t)); + die($ret) if($ret); + return undef; +} + + + +##################################### +sub +CommandVerbose($$) +{ + my ($cl, $param) = @_; + if($param =~ m/^[0-5]$/) { + $verbose = $param; + return undef; + } else { + return "Valid value for verbose are 0,1,2,3,4,5"; + } +} + +##################################### +sub +CommandRereadCfg($$) +{ + my ($cl, $param) = @_; + + return "RereadCfg: No parameters are accepted" if($param); + DoSavefile(); + + foreach my $d (keys %defs) { + no strict "refs"; + my $ret = &{$devmods{$defs{$d}{TYPE}}{UndefFn}}($defs{$d}, $d); + use strict "refs"; + return $ret if($ret); + } + + %defs = (); + %logs = (); + %attr = (); + %ntfy = (); + %at = (); + + my $ret; + $ret = CommandInclude($cl, $configfile); + return $ret if($ret); + $ret = CommandInclude($cl, $savefile) if($savefile); + return $ret; +} + +##################################### +sub +CommandQuit($$) +{ + my ($cl, $param) = @_; + + if(!$cl) { + $rcvdquit = 1; + return; + } + + syswrite($client{$cl}{fd}, "Bye...\n") if($client{$cl}{prompt}); + DoClose($cl); + return undef; +} + +##################################### +sub +DoSavefile() +{ + return if(!$savefile); + if(!open(SFH, ">$savefile")) { + Log 1, "Cannot open $savefile: $!"; + return; + } + + my $t = localtime; + print SFH "#$t\n"; + + foreach my $d (sort keys %defs) { + my $t = $defs{$d}{TYPE}; + print SFH "setstate $d $defs{$d}{STATE}\n" + if($defs{$d}{STATE} && $defs{$d}{STATE} ne "unknown"); + + ############# + # Now the detailed list + no strict "refs"; + my $str = &{$devmods{$defs{$d}{TYPE}}{ListFn}}($defs{$d}); + use strict "refs"; + next if($str =~ m/^No information about/); + + foreach my $l (split("\n", $str)) { + print SFH "setstate $d $l\n" + } + + } + + foreach my $t (sort keys %at) { + # $t =~ s/_/ /g; # Why is this here? + print SFH "at $t\n"; + } + + close(SFH); +} + +##################################### +sub +CommandShutdown($$) +{ + my ($cl, $param) = @_; + Log 0, "Server shutdown"; + DoSavefile(); + exit(0); +} + +##################################### +sub +CommandNotifyon($$) +{ + my ($cl, $param) = @_; + + my @a = split("[ \t]", $param, 2); + + # Checking for misleading regexps + eval { "Hallo" =~ m/^$a[0]$/ }; + return "Bad regexp: $@" if($@); + + $ntfy{$a[0]} = SemicolonEscape($a[1]); + return undef; +} + +##################################### +sub +DoSet(@) +{ + my @a = @_; + + my $dev = $a[0]; + my $ret; + no strict "refs"; + $ret = &{$devmods{$defs{$dev}{TYPE}}{SetFn}}($defs{$dev}, @a); + use strict "refs"; + + return $ret if($ret); + + shift @a; + return DoTrigger($dev, join(" ", @a)); +} + +##################################### +sub +CommandSet($$) +{ + my ($cl, $param) = @_; + my @a = split("[ \t][ \t]*", $param); + return "Usage: set " if(int(@a) < 1); + + my $dev = $a[0]; + my @rets; + + if(defined($defs{$dev})) { + + return DoSet(@a); + + } elsif($dev =~ m/,/) { # Enumeration (separated by ,) + + foreach my $sdev (split(",", $dev)) { + push @rets, "Please define $sdev first" if(!defined($defs{$sdev})); + $a[0] = $sdev; + my $ret = DoSet(@a); + push @rets, $ret if($ret); + } + return join("\n", @rets); + + } elsif($dev =~ m/-/) { # Range (separated by -) + + my @lim = split("-", $dev); + foreach my $sdev (keys %defs) { + next if($sdev lt $lim[0] || $sdev gt $lim[1]); + $a[0] = $sdev; + my $ret = DoSet(@a); + push @rets, $ret if($ret); + } + return join("\n", @rets); + + } else { + + return "Please define $dev first ($param)"; + + } +} + + +##################################### +sub +CommandGet($$) +{ + my ($cl, $param) = @_; + + my @a = split("[ \t][ \t]*", $param); + return "Usage: get " if(int(@a) < 1); + my $dev = $a[0]; + return "Please define $dev first ($param)" if(!defined($defs{$dev})); + + ######################## + # Type specific set + my $ret; + no strict "refs"; + $ret = &{$devmods{$defs{$a[0]}{TYPE}}{GetFn}}($defs{$dev}, @a); + use strict "refs"; + + return $ret; +} + +##################################### +sub +GetTimeSpec($) +{ + my ($tspec) = @_; + my ($hr, $min, $sec, $fn); + + if($tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { + ($hr, $min, $sec) = ($1, $2, $3); + } elsif($tspec =~ m/^([0-9]+):([0-5][0-9])$/) { + ($hr, $min, $sec) = ($1, $2, 0); + } elsif($tspec =~ m/^{(.*)}$/) { + $fn = $1; + $tspec = eval $fn; + if(!$@ && $tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { + ($hr, $min, $sec) = ($1, $2, $3); + } elsif(!$@ && $tspec =~ m/^([0-9]+):([0-5][0-9])$/) { + ($hr, $min, $sec) = ($1, $2, 0); + } else { + $tspec = "" if(!$tspec); + return ("the at function must return a timespec HH:MM:SS and not $tspec.", + undef, undef, undef, undef); + } + } else { + return ("Wrong timespec $tspec: either HH:MM:SS or {perlcode}", + undef, undef, undef, undef); + } + return (undef, $hr, $min, $sec, $fn); +} + +##################################### +sub +CommandAt($$) +{ + my ($cl, $def) = @_; + my ($tm, $command) = split("[ \t]+", $def, 2); + + return "Usage: at " if(!$command); + return "Wrong timespec, use \"[+][*[{count}]]