# $Id$ ############################################################################## # # 11_FHT.pm # Copyright by # e-mail: # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Foobar 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; sub doSoftBuffer($); sub softBufferTimer($); sub getFhtMin($); sub getFhtBuffer($); my %codes = ( "00" => "actuator", "01" => "actuator1", "02" => "actuator2", "03" => "actuator3", "04" => "actuator4", "05" => "actuator5", "06" => "actuator6", "07" => "actuator7", "08" => "actuator8", "14" => "mon-from1", "15" => "mon-to1", "16" => "mon-from2", "17" => "mon-to2", "18" => "tue-from1", "19" => "tue-to1", "1a" => "tue-from2", "1b" => "tue-to2", "1c" => "wed-from1", "1d" => "wed-to1", "1e" => "wed-from2", "1f" => "wed-to2", "20" => "thu-from1", "21" => "thu-to1", "22" => "thu-from2", "23" => "thu-to2", "24" => "fri-from1", "25" => "fri-to1", "26" => "fri-from2", "27" => "fri-to2", "28" => "sat-from1", "29" => "sat-to1", "2a" => "sat-from2", "2b" => "sat-to2", "2c" => "sun-from1", "2d" => "sun-to1", "2e" => "sun-from2", "2f" => "sun-to2", "3e" => "mode", "3f" => "holiday1", # Not verified "40" => "holiday2", # Not verified "41" => "desired-temp", "XX" => "measured-temp", # sum of next. two, never really sent "42" => "measured-low", "43" => "measured-high", "44" => "warnings", "45" => "manu-temp", # No clue what it does. "4b" => "ack", "53" => "can-xmit", "54" => "can-rcv", "60" => "year", "61" => "month", "62" => "day", "63" => "hour", "64" => "minute", "65" => "report1", "66" => "report2", "69" => "ack2", "7d" => "start-xmit", "7e" => "end-xmit", "82" => "day-temp", "84" => "night-temp", "85" => "lowtemp-offset", # Alarm-Temp.-Differenz "8a" => "windowopen-temp", ); my %cantset = ( "actuator" => 1, "actuator1" => 1, "actuator2" => 1, "actuator3" => 1, "actuator4" => 1, "actuator5" => 1, "actuator6" => 1, "actuator7" => 1, "actuator8" => 1, "ack" => 1, "ack2" => 1, "battery" => 1, "can-xmit" => 1, "can-rcv" => 1, "start-xmit" => 1, "end-xmit" => 1, "lowtemp" => 1, "measured-temp" => 1, "measured-high" => 1, "measured-low" => 1, "warnings" => 1, "window" => 1, "windowsensor" => 1, ); # additional warnings my %warnings = ( "battery" => 1, "lowtemp" => 1, "window" => 1, "windowsensor" => 1, ); my %priority = ( "desired-temp"=> 1, "mode" => 2, "report1" => 3, "report2" => 3, "holiday1" => 4, "holiday2" => 5, "day-temp" => 6, "night-temp" => 7, ); my %c2m = (0 => "auto", 1 => "manual", 2 => "holiday", 3 => "holiday_short"); my %m2c; # Reverse c2m my %c2b; # command->button hash (reverse of codes) my %c2bset; # command->button hash (settable values) my $defmin = 0; # min fhtbuf free bytes before sending commands my $retryafter = 240; # in seconds, only when fhtsoftbuffer is active my $cmdcount = 0; ##################################### sub FHT_Initialize($) { my ($hash) = @_; foreach my $k (keys %codes) { my $v = $codes{$k}; $c2b{$v} = $k; $c2bset{$v} = $k if(!$cantset{$v}); } foreach my $k (keys %c2m) { $m2c{$c2m{$k}} = $k; } # { Dispatch($defs{CUL}, "810b04028309830151024130001d", undef) } # 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->{DefFn} = "FHT_Define"; $hash->{UndefFn} = "FHT_Undef"; $hash->{ParseFn} = "FHT_Parse"; $hash->{AttrList} = "IODev do_not_notify:1,0 model:fht80b dummy:1,0 " . "showtime:1,0 retrycount " . "minfhtbuffer lazy tmpcorr ignore:1,0 ". $readingFnAttributes; $hash->{AutoCreate}= { "FHT.*" => { GPLOT => "fht:Temp/Act,", FILTER => "%NAME" } }; } sub FHT_Set($@) { my ($hash, @a) = @_; my $ret = ""; return "\"set $a[0]\" needs at least two parameters" if(@a < 2); my $name = shift(@a); # Replace refreshvalues with report1 and report2, and time with hour/minute for(my $i = 0; $i < @a; $i++) { splice(@a,$i,1,("report1","255","report2","255")) if($a[$i] eq "refreshvalues"); if($a[$i] eq "time") { my @t = localtime; splice(@a,$i,1,("hour",$t[2],"minute",$t[1])); } if($a[$i] eq "date") { my @t = localtime; splice(@a,$i,1,("year",$t[5]-100,"month",$t[4]+1,"day",$t[3])); } } my $ncmd = 0; my $arg = "020183" . $hash->{CODE}; my ($cmd, $allcmd, $val) = ("", "", ""); my $lazy= defined($attr{$name}) && defined($attr{$name}{"lazy"}) && ($attr{$name}{"lazy"}>0); my $readings= $hash->{READINGS}; while(@a) { $cmd = shift(@a); if(!defined($c2b{$cmd})) { my $cmdList = join(" ",sort keys %c2bset); my @list = map { ($_.".0", $_+0.5) } (6..30); pop @list; my $tmpList="on,off,".join(",",@list); $cmdList =~ s/-temp/-temp:$tmpList/g; # FHEMWEB sugar $cmdList =~ s/(-from.|-to.)/$1:time/g; return "Unknown argument $cmd, choose one of $cmdList"; } return "Readonly parameter $cmd" if(defined($cantset{$cmd})); return "\"set $name $cmd\" needs a parameter" if(@a < 1); $val = shift(@a); $arg .= $c2b{$cmd}; if ($cmd =~ m/-temp/) { if(!($val eq "on" || $val eq "off" || ($val =~ m/^\d*\.?\d+$/ && $val >= 5.5 && $val <= 30.5))) { my @list = map { ($_.".0", $_+0.5) } (6..30); pop @list; return "Invalid temperature $val, choose one of on off " . join(" ",@list); } $val = 30.5 if($val eq "on"); $val = 5.5 if($val eq "off"); my $a = int($val*2); $arg .= sprintf("%02x", $a); $val = sprintf("%.1f", $a/2); } elsif($cmd =~ m/-from/ || $cmd =~ m/-to/) { return "Invalid timeformat, use HH:MM" if($val !~ m/^([0-2]\d):([0-5]\d)/); my $a = ($1*6) + ($2/10); $arg .= sprintf("%02x", $a); my $nt = sprintf("%02d:%02d", $1, int($2/10)*10); $ret .= "Rounded $cmd to $nt" if($nt ne $val); $val = $nt; } elsif($cmd eq "mode") { return "Invalid mode, choose one of " . join(" ", sort keys %m2c) if(!defined($m2c{$val})); $arg .= sprintf("%02x", $m2c{$val}); } elsif ($cmd eq "lowtemp-offset") { return "Invalid lowtemperature-offset, must between 1 and 5" if($val !~ m/^[1-5]$/); $arg .= sprintf("%02x", $val); $val = "$val.0"; } else { # Holiday1, Holiday2 return "Invalid argument, must be between 1 and 255" if($val !~ m/^\d+$/ || $val < 0 || $val > 255); $arg .= sprintf("%02x", $val) if(defined($val)); } if($lazy && $cmd ne "report1" && $cmd ne "report2" && $cmd ne "refreshvalues" && defined($readings->{$cmd}) && $readings->{$cmd}{VAL} eq $val) { $ret .= "Lazy mode ignores $cmd"; Log3 $name, 2, "Lazy mode ignores $cmd $val"; } else { $ncmd++; $allcmd .=" " if($allcmd); $allcmd .= $cmd; $allcmd .= " $val" if(defined($val)); } } return "Too many commands specified, an FHT only supports up to 8" if($ncmd > 8); return $ret if(!$ncmd); my $ioname = ""; $ioname = $hash->{IODev}->{NAME} if($hash->{IODev}); if($attr{$ioname} && $attr{$ioname}{fhtsoftbuffer}) { my $io = $hash->{IODev}; my %h = (HASH => $hash, CMD => $allcmd, ARG => $arg); my $prio = $priority{$cmd}; $prio = "9" if(!$prio); my $key = $prio . ":" . gettimeofday() . ":" . $cmdcount++; $io->{SOFTBUFFER}{$key} = \%h; doSoftBuffer($io); } else { IOWrite($hash, "04", $arg); Log3 $name, 2, "FHT set $name $allcmd"; } return $ret; } ##################################### sub FHT_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); 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]; AssignIoPort($hash); # Check if the CULs id collides with our id. if($hash->{IODev} && $hash->{IODev}{TYPE} eq "CUL") { $hash->{IODev}{FHTID} =~ m/^(..)(..)$/; my ($i1, $i2) = (hex($1), hex($2)); $a[2] =~ m/^(..)(..)$/; my ($l1, $l2) = (hex($1), hex($2)); if($l2 == $i2 && $l1 >= $i1 && $l1 <= $i1+7) { my $err = "$a[0]: CODE collides with the FHTID of the corresponding CUL"; Log3 $a[0], 1, $err; return $err; } } $modules{FHT}{defptr}{$a[2]} = $hash; #Log3 $a[0], 2, "Asking the FHT device $a[0]/$a[2] to send its data"; #FHT_Set($hash, ($a[0], "report1", "255", "report2", "255")); return undef; } ##################################### sub FHT_Undef($$) { my ($hash, $name) = @_; delete($modules{FHT}{defptr}{$hash->{CODE}}) if($hash && $hash->{CODE}); return undef; } ##################################### sub FHT_Parse($$) { my ($hash, $msg) = @_; $msg = lc($msg); my $dev = substr($msg, 16, 4); my $cde = substr($msg, 20, 2); my $val = (length($msg) > 26 ? substr($msg, 26, 2) : undef); my $confirm = 0; if(!defined($modules{FHT}{defptr}{$dev})) { # it might be our own FHT8v, then be silent foreach my $d (%defs) { my $dp = $defs{$d}; next if(!$dp->{TYPE} || $dp->{TYPE} ne "FHT8V"); return "" if($dp->{addr} eq $dev); } Log3 $hash, 3, "FHT Unknown device $dev, please define it"; return "UNDEFINED FHT_$dev FHT $dev"; } my $def = $modules{FHT}{defptr}{$dev}; my $name = $def->{NAME}; return "" if(IsIgnored($name)); my $io = $def->{IODev}; # Short message if(length($msg) < 26) { Log3 $name, 4, "FHT Short message. Device $name, Message: $msg"; return ""; } if($io->{TYPE} eq "CUL") { $confirm = 1; } elsif(!$val || $cde eq "65" || $cde eq "66") { # This is a confirmation message. We reformat it so that # it looks like a real message, and let the rest parse it Log3 $name, 4, "FHT $name confirmation: $cde"; $val = substr($msg, 22, 2); $confirm = 1; } $val = hex($val); my $cmd = $codes{$cde}; if(!$cmd) { Log3 $name, 4, "FHT $name (Unknown: $cde => $val)"; readingsSingleUpdate($def, "unknown_$cde", $val, 1); return $name; } # # special treatment for measured-temp which is actually sent in two bytes # # the measured temperature comes in two bytes: measured-low and measured-high # measured-temp= (measured-high * 256 + measured-low) / 10. # measured-low and measured-high will only be stored as internals if($cmd eq "measured-low") { $def->{".measuredLow"}= $val; return ""; } elsif($cmd eq "measured-high") { $def->{".measuredHigh"}= $val; if(defined($def->{".measuredLow"})) { $val = sprintf("%.1f", ($val*256.0 + $def->{".measuredLow"})/10.0+ AttrVal($name, "tmpcorr", 0.0)); $cmd = "measured-temp"; } else { return ""; } } # # from here readings are effectively updated # readingsBeginUpdate($def); # The first four are confirmation messages, so they must be converted to # the same format as the input (for the softbuffer) if($cmd =~ m/-from/ || $cmd =~ m/-to/) { $val = sprintf("%02d:%02d", $val/6, ($val%6)*10); } elsif($cmd eq "mode") { $val = $c2m{$val} if(defined($c2m{$val})); } elsif($cmd =~ m/.*-temp/ && $cmd ne "measured-temp") { $val = sprintf("%.1f", $val / 2); if($cmd eq "desired-temp") { $val = ($val > 30 ? "on" : ($val < 6 ? "off" : $val)); } } elsif($cmd eq "lowtemp-offset") { $val = sprintf("%d.0", $val) } elsif($cmd =~ m/^actuator/) { my $sval = lc(substr($msg,24,2)); my $fv = sprintf("%d%%", int(100*$val/255+0.5)); if($sval =~ m/[ab]0/) { $val = $fv; } # sync in the summer elsif($sval =~ m/.0/) { $val = "syncnow"; } elsif($sval =~ m/.1/) { $val = "99%" } # FHT set to 30.5, FHT80B=="ON" elsif($sval =~ m/.2/) { $val = "0%" } # FHT set to 5.5 elsif($sval =~ m/.6/) { $val = "$fv" } elsif($sval =~ m/.8/) { $val = "offset: " . ($val>128?(128-$val):$val) } elsif($sval =~ m/[23]a/) { $val = "lime-protection" } elsif($sval =~ m/[ab]a/) { $val = $fv } # lime protection bug elsif($sval =~ m/.c/) { $val = sprintf("synctime: %d", int($val/2)-1); } elsif($sval =~ m/.e/) { $val = "test" } elsif($sval =~ m/.f/) { $val = "pair" } else { $val = "unknown_$sval: $fv" } } elsif($cmd eq "warnings") { my $nVal; # initialize values for additional warnings my $valBattery; my $valLowTemp; my $valWindow; my $valSensor; my $nBattery; my $nLowTemp; my $nWindow; my $nSensor; # parse warnings if($val & 1) { $nVal = "Battery low"; $nBattery = "low"; } if($val & 2) { $nVal .= "; " if($nVal); $nVal .= "Temperature too low"; $nLowTemp = "warn"; } if($val &32) { $nVal .= ", " if($nVal); $nVal .= "Window open"; $nWindow = "open"; } if($val &16) { $nVal .= ", " if($nVal); $nVal .= "Fault on window sensor"; $nSensor = "fault"; } # set default values or new values if they were changed $valBattery = $nBattery? $nBattery : "ok"; $valLowTemp = $nLowTemp? $nLowTemp : "ok"; $valWindow = $nWindow? $nWindow : "closed"; $valSensor = $nSensor? $nSensor : "ok"; $val = $nVal? $nVal : "none"; # set additional warnings and trigger notify readingsBulkUpdate($def, "battery", $valBattery); Log3 $name, 4, "FHT $name battery: $valBattery"; readingsBulkUpdate($def, "lowtemp", $valLowTemp); Log3 $name, 4, "FHT $name lowtemp: $valLowTemp"; readingsBulkUpdate($def, "window", $valWindow); Log3 $name, 4, "FHT $name window: $valWindow"; readingsBulkUpdate($def, "windowsensor", $valSensor); Log3 $name, 4, "FHT $name windowsensor: $valSensor"; } $cmd = "FHZ:$cmd" if(substr($msg,24,1) eq "7"); readingsBulkUpdate($def, $cmd, $val); if($cmd eq "measured-temp") { readingsBulkUpdate($def, "state", "measured-temp: $val", 0); readingsBulkUpdate($def, "temperature", $val); # For dewpoint } Log3 $name, 4, "FHT $name $cmd: $val"; # # now we are done with updating readings # readingsEndUpdate($def, 1); ################################ # Softbuffer: delete confirmed commands if($confirm) { my $found; foreach my $key (sort keys %{$io->{SOFTBUFFER}}) { my $h = $io->{SOFTBUFFER}{$key}; my $hcmd = $h->{CMD}; my $hname = $h->{HASH}->{NAME}; Log3 $name, 4, "FHT softbuffer check: $hname / $hcmd"; if($hname eq $name && $hcmd =~ m/^$cmd $val/) { $found = $key; Log3 $name, 4, "FHT softbuffer found"; last; } } delete($io->{SOFTBUFFER}{$found}) if($found); } return $name; } ##################################### # Check the softwarebuffer and send/resend commands sub doSoftBuffer($) { my ($io) = @_; my $now = gettimeofday(); my $count = 0; my $fhzbuflen = -999; foreach my $key (keys %{ $io->{SOFTBUFFER} }) { $count++; my $h = $io->{SOFTBUFFER}{$key}; my $name = $h->{HASH}->{NAME}; if($h->{NSENT}) { next if($now-$h->{SENDTIME} < $retryafter); my $retry = AttrVal($name, "retrycount", 1); if($h->{NSENT} > $retry) { Log3 $name, 2, "$name set $h->{CMD}: ". "no confirmation after $h->{NSENT} tries, giving up"; delete($io->{SOFTBUFFER}{$key}); next; } } # Check if it is still in the CUL buffer. if($io->{TYPE} eq "CUL") { my $cul = CallFn($io->{NAME}, "GetFn", $io, (" ", "raw", "T02")); my $arg = uc($h->{ARG}); $arg =~ s/^020183//; $arg =~ s/(....)/,$1/g; $arg =~ s/,(....),/$1:/; $arg = uc($arg); if($cul =~ m/$arg/) { Log3 $name, 3, "fhtsoftbuffer: $name set $h->{CMD} ". "is still in the culfw buffer, wont send it again"; $h->{SENDTIME} = $now; $h->{NSENT}++; next; } } $fhzbuflen = getFhtBuffer($io) if($fhzbuflen == -999); my $arglen = length($h->{ARG})/2 - 2; # Length in bytes next if($fhzbuflen < $arglen || $fhzbuflen < getFhtMin($io)); IOWrite($h->{HASH}, "04", $h->{ARG}); Log3 $name, 2, "FHT set $name $h->{CMD}"; $fhzbuflen -= $arglen; $h->{SENDTIME} = $now; $h->{NSENT}++; } if($count && !$io->{SOFTBUFFERTIMER}) { $io->{SOFTBUFFERTIMER} = 1; InternalTimer(gettimeofday()+30, "softBufferTimer", $io, 0); } } ##################################### # Wrapper for the InternalTimer sub softBufferTimer($) { my ($io) = @_; delete($io->{SOFTBUFFERTIMER}); doSoftBuffer($io); } ##################################### sub getFhtMin($) { my ($io) = @_; my $ioname = $io->{NAME}; return $attr{$ioname}{minfhtbuffer} if($attr{$ioname} && $attr{$ioname}{minfhtbuffer}); return $defmin; } ##################################### # get the FHZ hardwarebuffer without logentry as decimal value sub getFhtBuffer($) { my ($io) = @_; my $count = 0; return getFhtMin($io) if(IsDummy($io->{NAME})); for(;;) { return 0 if(!defined($io->{FD})); # Avoid crash if the CUL/FHZ is absent my $msg = CallFn($io->{NAME}, "GetFn", $io, (" ", "fhtbuf")); Log3 $io, 5, "getFhtBuffer: $count $msg"; return hex($1) if($msg && $msg =~ m/=> ([0-9A-F]+)$/i); return 0 if($count++ >= 5); } } 1; =pod =begin html

FHT

=end html =cut