From cbe477350805954bc5c5e32af64e90cf9e7fb466 Mon Sep 17 00:00:00 2001 From: immiimmi <> Date: Sun, 16 Feb 2014 00:41:08 +0000 Subject: [PATCH] Added new module 00_THZ.pm git-svn-id: https://svn.fhem.de/fhem/trunk@4947 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/00_THZ.pm | 957 ++++++++++++++++++++++++++++++++++++++++++++ fhem/HISTORY | 7 +- fhem/MAINTAINER.txt | 1 + 4 files changed, 965 insertions(+), 1 deletion(-) create mode 100644 fhem/FHEM/00_THZ.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 3ff7d1ebf..442163301 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,6 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. - SVN + - added: new module 00_THZ.pm (immiimmi) - added: new module 98_HTTPMOD.pm (stefanstrobel) - added: new module 51_BBB_WATCHDOG.pm (betateilchen) - bugfix: SYSMON: Fix: uninitialized variable diff --git a/fhem/FHEM/00_THZ.pm b/fhem/FHEM/00_THZ.pm new file mode 100644 index 000000000..a2a214e19 --- /dev/null +++ b/fhem/FHEM/00_THZ.pm @@ -0,0 +1,957 @@ +############################################## +# 00_THZ +# by immi 02/2014 +# v. 0.067 +# this code is based on the hard work of Robert; I just tried to port it +# http://robert.penz.name/heat-pump-lwz/ +# http://heatpumpmonitor.penz.name/heatpumpmonitorwiki/ +######################################################################################## +# +# This programm is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +######################################################################################## + + +package main; +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use feature ":5.10"; +sub THZ_Read($); +sub THZ_ReadAnswer($); +sub THZ_Ready($); +sub THZ_Write($$); +sub THZ_Parse($); +sub THZ_Parse1($); +sub THZ_checksum($); +sub THZ_replacebytes($$$); +sub THZ_decode($); +sub THZ_overwritechecksum($); +sub THZ_encodecommand($$); +sub hex2int($); +sub quaters2time($); +sub THZ_debugread($); +sub THZ_GetRefresh($); +sub THZ_Refresh_all_gets($); + + + +######################################################################################## +# +# %sets - all supported protocols are listed +# +######################################################################################## + +my %sets = ( + "p01RoomTempDayHC1" => {cmd2=>"0B0005", argMin => "13", argMax => "28" }, + "p02RoomTempNightHC1" => {cmd2=>"0B0008", argMin => "13", argMax => "28" }, + "p02RoomTempStendbyHC1" => {cmd2=>"0B013D", argMin => "13", argMax => "28" }, + "p01RoomTempDayHC2" => {cmd2=>"0C0005", argMin => "13", argMax => "28" }, + "p02RoomTempNightHC2" => {cmd2=>"0C0008", argMin => "13", argMax => "28" }, + "p02RoomTempStendbyHC2" => {cmd2=>"0C013D", argMin => "13", argMax => "28" }, + "p04DHWsetDay" => {cmd2=>"0A0013", argMin => "13", argMax => "46" }, + "p05DHWsetNight" => {cmd2=>"0A05BF", argMin => "13", argMax => "46" }, + "p07FanStageDay" => {cmd2=>"0A056C", argMin => "0", argMax => "3" }, + "p08FanStageNight" => {cmd2=>"0A056D", argMin => "0", argMax => "3" }, + "p09FanStageStandby" => {cmd2=>"0A056F", argMin => "0", argMax => "3" }, + "holidayBegin_day" => {cmd2=>"0A011B", argMin => "1", argMax => "31" }, + "holidayBegin_month" => {cmd2=>"0A011C", argMin => "1", argMax => "12" }, + "holidayBegin_year" => {cmd2=>"0A011D", argMin => "12", argMax => "20" }, + # "holidayBegin-time" => {cmd2=>"0A05D3", argMin => "0", argMax => "3" }, + "holidayEnd_day" => {cmd2=>"0A011E", argMin => "1", argMax => "31" }, + "holidayEnd_month" => {cmd2=>"0A011F", argMin => "1", argMax => "12" }, + "holidayEnd_year" => {cmd2=>"0A0120", argMin => "12", argMax => "20" }, + #"holidayEnd-time" => {cmd2=>"0A05D4"}, # the answer look like 0A05D4-0D0A05D40029 for year 41 which is 10:15 + #"party-time" => {cmd2=>"0A05D1"}, # value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + ); + + + + +######################################################################################## +# +# %gets - all supported protocols are listed +# +######################################################################################## + +my %gets = ( +# "hallo" => { }, +# "debug_read_raw_register_slow" => { }, + "history" => {cmd2=>"09"}, + "last10errors" => {cmd2=>"D1"}, + "allFB" => {cmd2=>"FB"}, + "timedate" => {cmd2=>"FC"}, + "firmware" => {cmd2=>"FD"}, + "p01RoomTempDayHC1" => {cmd2=>"0B0005"}, + "p02RoomTempNightHC1" => {cmd2=>"0B0008"}, + "p03RoomTempStendbyHC1" => {cmd2=>"0B013D"}, + "p01RoomTempDayHC2" => {cmd2=>"0C0005"}, + "p02RoomTempNightHC2" => {cmd2=>"0C0008"}, + "p03RoomTempStendbyHC2" => {cmd2=>"0C013D"}, + "p04DHWsetDay" => {cmd2=>"0A0013"}, + "p05DHWsetNight" => {cmd2=>"0A05BF"}, + "p07FanStageDay" => {cmd2=>"0A056C"}, + "p08FanStageNight" => {cmd2=>"0A056D"}, + "p09FanStageStandby" => {cmd2=>"0A056F"}, + "holidayBegin_day" => {cmd2=>"0A011B"}, + "holidayBegin_month" => {cmd2=>"0A011C"}, + "holidayBegin_year" => {cmd2=>"0A011D"}, + "holidayBegin-time" => {cmd2=>"0A05D3"}, + "holidayEnd_day" => {cmd2=>"0A011E"}, + "holidayEnd_month" => {cmd2=>"0A011F"}, + "holidayEnd_year" => {cmd2=>"0A0120"}, # the answer look like 0A0120-3A0A01200E00 for year 14 + "holidayEnd-time" => {cmd2=>"0A05D4"}, # the answer look like 0A05D4-0D0A05D40029 41 which is 10:15 + "party-time" => {cmd2=>"0A05D1"}, # value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + "programHC1_Mo_0" => {cmd2=>"0B1410"}, #1 is monday 0 is first prog; start and end; value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + "programHC1_Mo_1" => {cmd2=>"0B1411"}, + "programHC1_Mo_2" => {cmd2=>"0B1412"}, + "programHC1_Tu_0" => {cmd2=>"0B1420"}, + "programHC1_Tu_1" => {cmd2=>"0B1421"}, + "programHC1_Tu_2" => {cmd2=>"0B1422"}, + "programHC1_We_0" => {cmd2=>"0B1430"}, + "programHC1_We_1" => {cmd2=>"0B1431"}, + "programHC1_We_2" => {cmd2=>"0B1432"}, + "programHC1_Th_0" => {cmd2=>"0B1440"}, + "programHC1_Th_1" => {cmd2=>"0B1441"}, + "programHC1_Th_2" => {cmd2=>"0B1442"}, + "programHC1_Fr_0" => {cmd2=>"0B1450"}, + "programHC1_Fr_1" => {cmd2=>"0B1451"}, + "programHC1_Fr_2" => {cmd2=>"0B1452"}, + "programHC1_Sa_0" => {cmd2=>"0B1460"}, + "programHC1_Sa_1" => {cmd2=>"0B1461"}, + "programHC1_Sa_2" => {cmd2=>"0B1462"}, + "programHC1_So_0" => {cmd2=>"0B1470"}, + "programHC1_So_1" => {cmd2=>"0B1471"}, + "programHC1_So_2" => {cmd2=>"0B1472"}, + "programHC1_Mo-Fr_0" => {cmd2=>"0B1480"}, + "programHC1_Mo-Fr_1" => {cmd2=>"0B1481"}, + "programHC1_Mo-Fr_3" => {cmd2=>"0B1482"}, + "programHC1_Sa-So_0" => {cmd2=>"0B1490"}, + "programHC1_Sa-So_1" => {cmd2=>"0B1491"}, + "programHC1_Sa-So_3" => {cmd2=>"0B1492"}, + "programHC1_Mo-So_0" => {cmd2=>"0B14A0"}, + "programHC1_Mo-So_1" => {cmd2=>"0B14A1"}, + "programHC1_Mo-So_3" => {cmd2=>"0B14A2"}, + "programHC2_Mo_0" => {cmd2=>"0C1510"}, #1 is monday 0 is first prog; start and end; value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + "programHC2_Mo_1" => {cmd2=>"0C1511"}, + "programHC2_Mo_2" => {cmd2=>"0C1512"}, + "programHC2_Tu_0" => {cmd2=>"0C1520"}, + "programDHW_Mo_0" => {cmd2=>"0A1710"}, + "programDHW_Mo_1" => {cmd2=>"0A1711"}, + "programFan_Mo_0" => {cmd2=>"0A1D10"}, + "programFan_Mo_1" => {cmd2=>"0A1D11"} + ); + + +######################################################################################## +# +# THZ_Initialize($) +# +# Parameter hash +# +######################################################################################## +sub THZ_Initialize($) +{ + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + +# Provider + $hash->{ReadFn} = "THZ_Read"; + $hash->{WriteFn} = "THZ_Write"; + $hash->{ReadyFn} = "THZ_Ready"; + +# Normal devices + $hash->{DefFn} = "THZ_Define"; + $hash->{UndefFn} = "THZ_Undef"; + $hash->{GetFn} = "THZ_Get"; + $hash->{SetFn} = "THZ_Set"; + $hash->{AttrList}= "do_not_notify:1,0 dummy:1,0 loglevel:0,1,2,3,4,5,6 interval_allFB:0,60,120,180,300,600,3600,7200,43200,86400 interval_history:0,3600,7200,28800,43200,86400"; +} + + +######################################################################################## +# +# THZ_define +# +# Parameter hash and configuration +# +######################################################################################## +sub THZ_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + my $name = $a[0]; + + return "wrong syntax. Correct is: define THZ ". + "{devicename[\@baudrate]|ip:port}" + if(@a != 3); + + DevIo_CloseDev($hash); + my $dev = $a[2]; + + if($dev eq "none") { + Log 1, "$name device is none, commands will be echoed only"; + $attr{$name}{dummy} = 1; + return undef; + } + + $hash->{DeviceName} = $dev; + my $ret = DevIo_OpenDev($hash, 0, undef); + + + #my %par = (command => "allFB", hash => $hash ); + #InternalTimer(gettimeofday() +200, "THZ_GetRefresh", \%par, 0); + #my %par1 = ( command => "firmware", hash => $hash); + #InternalTimer(gettimeofday() +200, "THZ_GetRefresh", \%par1, 0); + #foreach my $cmdhash (keys %gets) { THZ_Get($hash, $hash->{NAME}, $cmdhash); } #refresh all registers + THZ_Refresh_all_gets($hash); + return $ret; +} + +######################################################################################## +# +# THZ_Refresh_all_gets - Called once refreshes current reading for all gets and initializes the regular interval calls +# +# Parameter $hash +# +######################################################################################## +sub THZ_Refresh_all_gets($) { + my ($hash) = @_; + RemoveInternalTimer($hash); + my $timedelay= 1; + foreach my $cmdhash (keys %gets) { + my %par = ( command => $cmdhash, hash => $hash ); + RemoveInternalTimer(\%par); + InternalTimer(gettimeofday() + $timedelay++ , "THZ_GetRefresh", \%par, 0); + } #refresh all registers; the register with interval_command ne 0 will keep on refreshing +} + + +######################################################################################## +# +# THZ_GetRefresh - Called in regular intervals to obtain current reading +# +# Parameter (hash => $hash, command => "allFB" ) +# it get the intervall directly from a attribute; the register with interval_command ne 0 will keep on refreshing +######################################################################################## +sub THZ_GetRefresh($) { + my ($par)=@_; + my $hash=$par->{hash}; + my $command=$par->{command}; + my $interval = AttrVal($hash->{NAME}, ("interval_".$command), 0); + if ($interval) { + $interval = 60 if ($interval < 60); #do not allow intervall <60 sec + InternalTimer(gettimeofday()+ $interval, "THZ_GetRefresh", $par, 1) ; + } + my $replyc = ""; + $replyc = THZ_Get($hash, $hash->{NAME}, $command) if (!($hash->{STATE} eq "disconnected")); + return ($replyc); +} + + + +##################################### +# THZ_Write -- simple write +# Parameter: hash and message HEX +# +######################################################################################## +sub THZ_Write($$) { + my ($hash,$msg) = @_; + my $name = $hash->{NAME}; + my $ll5 = GetLogLevel($name,5); + my $bstring; + $bstring = $msg; + + Log $ll5, "$hash->{NAME} sending $bstring"; + + DevIo_SimpleWrite($hash, $bstring, 1); +} + + +##################################### +# sub THZ_Read($) +# called from the global loop, when the select for hash reports data +# used just for testing the interface +######################################################################################## +sub THZ_Read($) +{ + my ($hash) = @_; + + my $buf = DevIo_SimpleRead($hash); + return "" if(!defined($buf)); + + my $name = $hash->{NAME}; + my $ll5 = GetLogLevel($name,5); + my $ll2 = GetLogLevel($name,2); + + my $data = $hash->{PARTIAL} . uc(unpack('H*', $buf)); + +Log $ll5, "$name/RAW: $data"; +Log $ll2, "$name/RAW: $data"; + +} + + + +##################################### +# +# THZ_Ready($) - Cchecks the status +# +# Parameter hash +# +######################################################################################## +sub THZ_Ready($) +{ + my ($hash) = @_; + + return DevIo_OpenDev($hash, 1, undef) + if($hash->{STATE} eq "disconnected"); + + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + if($po) { + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; + return ($InBytes>0); + } + +} + + + + + +##################################### +# +# THZ_Set - provides a method for setting the heatpump +# +# Parameters: hash and command to be sent to the interface +# +######################################################################################## +sub THZ_Set($@){ + my ($hash, @a) = @_; + my $dev = $hash->{DeviceName}; + my $name = $hash->{NAME}; + my $ll5 = GetLogLevel($name,5); + my $ll2 = GetLogLevel($name,2); + + return "\"set $name\" needs at least two parameters: and " if(@a < 2); + my $cmd = $a[1]; + my $arg = $a[2]; + my ($err, $msg) =("", " "); + my $cmdhash = $sets{$cmd}; + return "Unknown argument $cmd, choose one of " . join(" ", sort keys %sets) if(!defined($cmdhash)); + return "\"set $name $cmd\" needs at least one further argument: " if(!defined($arg)); + my $argreMax = $cmdhash->{argMax}; + my $argreMin = $cmdhash->{argMin}; + return "Argument does not match the allow inerval Max $argreMax .... Min $argreMin " if(($arg > $argreMax) or ($arg < $argreMin)); + my $cmdHex2 = $cmdhash->{cmd2}; + + if (substr($cmdHex2,0,4) eq "0A01") {$arg=$arg*256} # shift 2 times -- the answer look like 0A0120-3A0A01200E00 for year 14 + # elsif ((substr($message,4,2) eq "1D") or (substr($message,4,2) eq "17")) {$message = quaters2time(substr($message, 8,2)) ."--". quaters2time(substr($message, 10,2))} #value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + # elsif (substr($message,4,3) eq "05D") {$message = quaters2time(substr($message, 10,2)) } #value 1Ch 28dec is 7 + elsif (substr($cmdHex2,0,5) eq "0A056") { } # fann speed: do not multiply + else {$arg=$arg*10} + + THZ_Write($hash, "02"); # STX start of text + ($err, $msg) = THZ_ReadAnswer($hash); #Expectedanswer1 is "10" DLE data link escape + + if ($msg eq "10") { + $cmdHex2=THZ_encodecommand(($cmdHex2 . sprintf("%04X", $arg)),"set"); + THZ_Write($hash, $cmdHex2); # send request SOH start of heading -- Null -- ?? -- DLE data link escape -- EOT End of Text + ($err, $msg) = THZ_ReadAnswer($hash); #Expectedanswer is "10", DLE data link escape + } + + if ($msg eq "10") { + ($err, $msg) = THZ_ReadAnswer($hash); #Expectedanswer is "02" -- STX start of text + THZ_Write($hash, "10"); # DLE data link escape // ack datatranfer + ($err, $msg) = THZ_ReadAnswer($hash); # Expectedanswer3 // read from the heatpump + THZ_Write($hash, "10"); + $msg =$msg . "kkkkkkk" + } + + if (defined($err)) {return ($cmdHex2 . "-". $msg ."--" . $err);} + else { + sleep 1; + THZ_Get($hash, $name, $cmd); + #return ($cmd . " " . $cmdHex2 . "-x- ". $msg); + } +} + + + + +##################################### +# +# THZ_Get - provides a method for polling the heatpump +# +# Parameters: hash and command to be sent to the interface +# +######################################################################################## +sub THZ_Get($@){ + my ($hash, @a) = @_; + my $dev = $hash->{DeviceName}; + my $name = $hash->{NAME}; + my $ll5 = GetLogLevel($name,5); + my $ll2 = GetLogLevel($name,2); + + return "\"get $name\" needs one parameter" if(@a != 2); + my $cmd = $a[1]; + my ($err, $msg) =("", " "); + + my $cmdhash = $gets{$cmd}; + return "Unknown argument $cmd, choose one of " . + join(" ", sort keys %gets) if(!defined($cmdhash)); + + if ($cmd eq "debug_read_raw_register_slow") { + THZ_debugread($hash); + return ("all raw registers read and saved"); + } + + THZ_Write($hash, "02"); # STX start of text + ($err, $msg) = THZ_ReadAnswer($hash); #Expectedanswer1 is "10" DLE data link escape + + my $cmdHex2 = $cmdhash->{cmd2}; + if(defined($cmdHex2) and ($msg eq "10") ) { + $cmdHex2=THZ_encodecommand($cmdHex2,"get"); + THZ_Write($hash, $cmdHex2); # send request SOH start of heading -- Null -- ?? -- DLE data link escape -- EOT End of Text + ($err, $msg) = THZ_ReadAnswer($hash); #Expectedanswer2 is "1002", DLE data link escape -- STX start of text + } + + if($msg eq "1002") { + THZ_Write($hash, "10"); # DLE data link escape // ack datatranfer + ($err, $msg) = THZ_ReadAnswer($hash); # Expectedanswer3 // read from the heatpump + THZ_Write($hash, "10"); + } + + if (defined($err)) {return ($msg ."\n" . $err);} + else { + ($err, $msg) = THZ_decode($msg); #clean up and remove footer and header + if (defined($err)) {return ($msg ."\n" . $err);} + else { + $msg = THZ_Parse($msg); + my $activatetrigger =1; + readingsSingleUpdate($hash, $cmd, $msg, $activatetrigger); + return ($msg); + } + } +} + + + + +##################################### +# +# THZ_ReadAnswer- provides a method for simple read +# +# Parameter hash and command to be sent to the interface +# +######################################################################################## +sub THZ_ReadAnswer($) { + my ($hash) = @_; +#--next line added in order to slow-down 100ms + select(undef, undef, undef, 0.1); +#-- + my $buf = DevIo_SimpleRead($hash); + return ("InterfaceNotRespondig", "") if(!defined($buf)); + + my $name = $hash->{NAME}; + + my $data = uc(unpack('H*', $buf)); + return (undef, $data); +} + + +##################################### +# +# THZ_checksum - takes a string, removes the footer (4bytes) and computes checksum (without checksum of course) +# +# Parameter string +# returns the checksum 2bytes +# +######################################################################################## +sub THZ_checksum($) { + my ($stringa) = @_; + my $ml = length($stringa) - 4; + my $checksum = 0; + for(my $i = 0; $i < $ml; $i += 2) { + ($checksum= $checksum + hex(substr($stringa, $i, 2))) if ($i != 4); + } + return (sprintf("%02X", ($checksum %256))); +} + +##################################### +# +# hex2int - convert from hex to int with sign 16bit +# +######################################################################################## +sub hex2int($) { + my ($num) = @_; + $num = unpack('s', pack('S', hex($num))); + return $num; +} + +#################################### +# +# quaters2time - convert from hex to time; specific to the week programm registers +# +# parameter 1 byte representing number of quarter from midnight +# returns string representing time +# +# example: value 1E is converted to decimal 30 and then to a time 7:30 +######################################################################################## +sub quaters2time($) { + my ($num) = @_; + return("n.a.") if($num eq "80"); + my $quarters= hex($num) %4; + my $hour= (hex($num) - $quarters)/4 ; + my $time = sprintf("%02u", ($hour)) . ":" . sprintf("%02u", ($quarters*15)); + return $time; +} + + + + +#################################### +# +# time2quarters - convert from time to quarters in hex; specific to the week programm registers +# +# parameter: string representing time +# returns: 1 byte representing number of quarter from midnight +# +# example: a time 7:30 is converted to decimal 30 and then to value 1E +######################################################################################## +sub time2quaters($) { + my ($h,$m) = split(":", shift); + $m = 0 if(!$m); + $h = 0 if(!$h); + my $num = $h*4 + $m %15; + return (sprintf("%02X", $num)); +} + + +#################################### +# +# THZ_replacebytes - replaces bytes in string +# +# parameters: string, bytes to be searched, replacing bytes +# retunrns changed string +# +######################################################################################## +sub THZ_replacebytes($$$) { + my ($stringa, $find, $replace) = @_; + my $leng_str = length($stringa); + my $leng_find = length($find); + my $new_stringa =""; + for(my $i = 0; $i < $leng_str; $i += 2) { + if (substr($stringa, $i, $leng_find) eq $find){ + $new_stringa=$new_stringa . $replace; + if ($leng_find == 4) {$i += 2;} + } + else {$new_stringa=$new_stringa . substr($stringa, $i, 2);}; + } + return ($new_stringa); +} + + +## usage THZ_overwritechecksum("0100XX". $cmd."1003"); not needed anymore +sub THZ_overwritechecksum($) { + my ($stringa) = @_; + my $checksumadded=substr($stringa,0,4) . THZ_checksum($stringa) . substr($stringa,6); + return($checksumadded); +} + + +#################################### +# +# THZ_encodecommand - creates a telegram for the heatpump with a given command +# +# usage THZ_encodecommand($cmd,"get") or THZ_encodecommand($cmd,"set"); +# parameter string, +# retunrns encoded string +# +######################################################################################## + +sub THZ_encodecommand($$) { + my ($cmd,$getorset) = @_; + my $header = "0100"; + $header = "0180" if ($getorset eq "set"); # "set" and "get" have differnt header + my $footer ="1003"; + my $checksumadded=THZ_checksum($header . "XX" . $cmd . $footer) . $cmd; + # each 2B byte must be completed by byte 18 + # each 10 byte must be repeated (duplicated) + my $find = "10"; + my $replace = "1010"; + #$checksumadded =~ s/$find/$replace/g; #problems in 1% of the cases, in middle of a byte + $checksumadded=THZ_replacebytes($checksumadded, $find, $replace); + $find = "2B"; + $replace = "2B18"; + #$checksumadded =~ s/$find/$replace/g; + $checksumadded=THZ_replacebytes($checksumadded, $find, $replace); + return($header. $checksumadded .$footer); +} + + + + + +#################################### +# +# THZ_decode - decodes a telegram from the heatpump -- no parsing here +# +# Each response has the same structure as request - header (four bytes), optional data and footer: +# Header: 01 +# Read/Write: 00 for Read (get) response, 80 for Write (set) response; when some error occured, then device stores error code here; actually, I know only meaning of error 03 = unknown command +# Checksum: ? 1 byte - the same algorithm as for request +# Command: ? 1 byte - should match Request.Command +# Data: ? only when Read, length depends on data type +# Footer: 10 03 +# +######################################################################################## + +sub THZ_decode($) { + my ($message_orig) = @_; + # raw data received from device have to be de-escaped before header evaluation and data use: + # - each sequece 2B 18 must be replaced with single byte 2B + # - each sequece 10 10 must be replaced with single byte 10 + my $find = "1010"; + my $replace = "10"; + $message_orig=THZ_replacebytes($message_orig, $find, $replace); + $find = "2B18"; + $replace = "2B"; + $message_orig=THZ_replacebytes($message_orig, $find, $replace); + #check header and if ok 0100, check checksum and return the decoded msg + if ("0100" eq substr($message_orig,0,4)) { + if (THZ_checksum($message_orig) eq substr($message_orig,4,2)) { + $message_orig =~ /0100(.*)1003/; + my $message = $1; + return (undef, $message) + } + else {return (THZ_checksum($message_orig) . "crc_error", $message_orig)}; } + + if ("0103" eq substr($message_orig,0,4)) { return (" command not known", $message_orig)}; + + if ("0102" eq substr($message_orig,0,4)) { return (" CRC error in request", $message_orig)} + else {return (" new error code " , $message_orig);}; +} + +######################################################################################## +# +# THZ_Parse -0A01 +# +######################################################################################## + +sub THZ_Parse($) { + my ($message) = @_; + given (substr($message,2,2)) { + when ("0A") { + if (substr($message,4,2) eq "01") {$message = hex(substr($message, 8,2))} # the answer look like 0A0120-3A0A01200E00 for year 14 + elsif ((substr($message,4,2) eq "1D") or (substr($message,4,2) eq "17")) {$message = quaters2time(substr($message, 8,2)) ."--". quaters2time(substr($message, 10,2))} #value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + elsif (substr($message,4,4) eq "05D1") {$message = quaters2time(substr($message, 10,2)) ."--". quaters2time(substr($message, 8,2))} #like above but before stop then start !!!! + elsif ((substr($message,4,4) eq "05D3") or (substr($message,4,4) eq "05D4")) {$message = quaters2time(substr($message, 10,2)) } #value 1Ch 28dec is 7 + elsif (substr($message,4,3) eq "056") {$message = hex(substr($message, 8,4))} + else {$message = hex2int(substr($message, 8,4))/10 ." °C" } + } + when ("0B") { #set parameter HC1 + if (substr($message,4,2) eq "14") {$message = quaters2time(substr($message, 8,2)) ."--". quaters2time(substr($message, 10,2))} #value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + else {$message = hex2int(substr($message, 8,4))/10 ." °C" } + } + when ("0C") { #set parameter HC2 + if (substr($message,4,2) eq "15") {$message = quaters2time(substr($message, 8,2)) ."--". quaters2time(substr($message, 10,2))} #value 1Ch 28dec is 7 ; value 1Eh 30dec is 7:30 + else {$message = hex2int(substr($message, 8,4))/10 ." °C" } + } + when ("F4") { #allF4 + $message = + "x08: " . hex2int(substr($message, 8,4))/10 . " " . + "x12: " . hex2int(substr($message,12,4))/10 . " " . + "x16: " . hex2int(substr($message,16,4))/10 . " " . + "x20: " . hex2int(substr($message,20,4))/10 . " " . + "x24: " . hex2int(substr($message,24,4))/10 . " " . + "x28: " . hex2int(substr($message,28,4))/10 . " " . + "x32: " . hex2int(substr($message,32,4))/10 . " " . + "x36: " . hex2int(substr($message,36,4))/10 . " " . + "x40: " . hex2int(substr($message,40,4))/10 . " " . + "x44: " . hex2int(substr($message,44,4))/10 . " " . + "x48: " . hex2int(substr($message,48,4))/10 . " " . + "x52: " . hex2int(substr($message,52,4))/10 . " " . + "x56: " . hex2int(substr($message,56,4))/10 . " " . + "x60: " . hex2int(substr($message,60,4))/10 . " " . + "x64: " . hex2int(substr($message,64,4))/10 . " " . + "x68: " . hex2int(substr($message,68,4))/10 . " " . + "x72: " . hex2int(substr($message,72,4))/10 . " " . + "x76: " . hex2int(substr($message,76,4))/10 . " " . + "x80: " . hex2int(substr($message,80,4))/10 ; + } + when ("FD") { #firmware_ver + $message = "version: " . hex(substr($message,4,4))/100 ; + } + when ("FC") { #timedate 00 - 0F 1E 08 - 0D 03 0B + my %weekday = ( "0" =>"Monday", "1" => "Tuesday", "2" =>"Wednesday", "3" => "Thursday", "4" => "Friday", "5" =>"Saturday", "6" => "Sunday" ); + $message = "Weekday: " . $weekday{hex(substr($message, 4,2))} . " " . + "Hour: " . hex(substr($message, 6,2)) . " Min: " . hex(substr($message, 8,2)) . " Sec: " . hex(substr($message,10,2)) . " " . + "Date: " . (hex(substr($message,12,2))+2000) . "/" . hex(substr($message,14,2)) . "/" . hex(substr($message,16,2)); + } + when ("FB") { #allFB + $message = + "outside_temp: " . hex2int(substr($message, 8,4))/10 . " " . + "flow_temp: " . hex2int(substr($message,12,4))/10 . " " . #Vorlauf Temperatur + "return_temp: " . hex2int(substr($message,16,4))/10 . " " . #Rücklauf Temperatur + "hot_gas_temp: " . hex2int(substr($message,20,4))/10 . " " . #Heißgas Temperatur + "dhw_temp: " . hex2int(substr($message,24,4))/10 . " " . #Speicher Temperatur current cilinder water temperature + "flow_temp_HC2: " . hex2int(substr($message,28,4))/10 . " " . #Vorlauf TemperaturHK2 + "evaporator_temp: " . hex2int(substr($message,36,4))/10 . " " . #Speicher Temperatur + "condenser_temp: " . hex2int(substr($message,40,4))/10 . " " . + "Mixer_open: " . ((hex(substr($message,44,1)) & 0b0001) / 0b0001) . " " . + "Mixer_closed: " . ((hex(substr($message,44,1)) & 0b0010) / 0b0010) . " " . + "HeatPipeValve: " . ((hex(substr($message,44,1)) & 0b0100) / 0b0100) . " " . + "DiverterValve: " . ((hex(substr($message,44,1)) & 0b1000) / 0b1000) . " " . + "DHW_Pump: " . ((hex(substr($message,45,1)) & 0b0001) / 0b0001) . " " . + "HeatingCircuit_Pump: " . ((hex(substr($message,45,1)) & 0b0010) / 0b0010) . " " . + "Solar_Pump: " . ((hex(substr($message,45,1)) & 0b1000) / 0b1000) . " " . + "Compressor: " . ((hex(substr($message,46,1)) & 0b1000) / 0b1000) . " " . + "BoosterStage3: " . ((hex(substr($message,47,1)) & 0b0001) / 0b0001) . " " . + "BoosterStage2: " . ((hex(substr($message,47,1)) & 0b0010) / 0b0010) . " " . + "BoosterStage1: " . ((hex(substr($message,47,1)) & 0b0100) / 0b0100). " " . + "HighPressureSensor: " . ((hex(substr($message,48,1)) & 0b0001) / 0b0001). " " . #P1 inverterd? + "LowPressureSensor: " . ((hex(substr($message,48,1)) & 0b0010) / 0b0010). " " . #P3 inverterd? + "EvaporatorIceMonitor: " . ((hex(substr($message,48,1)) & 0b0100) / 0b0100). " " . #N3 + "SignalAnode: " . ((hex(substr($message,48,1)) & 0b1000) / 0b1000). " " . #S1 + "EVU_release: " . ((hex(substr($message,49,1)) & 0b0001) / 0b0001). " " . + "OvenFireplace: " . ((hex(substr($message,49,1)) & 0b0010) / 0b0010). " " . + "STB: " . ((hex(substr($message,49,1)) & 0b0100) / 0b0100). " " . + "OutputVentilatorPower: " . hex(substr($message,50,4))/10 . " " . + "InputVentilatorPower: " . hex(substr($message,54,4))/10 . " " . + "MainVentilatorPower: " . hex(substr($message,58,4))/10 . " " . + "OutputVentilatorSpeed: " . hex(substr($message,62,4))/1 . " " . # m3/h + "InputVentilatorSpeed: " . hex(substr($message,66,4))/1 . " " . # m3/h + "MainVentilatorSpeed: " . hex(substr($message,70,4))/1 . " " . # m3/h + "Outside_tempFiltered: " . hex2int(substr($message,74,4))/10 . " " . + "Rel_humidity: " . hex2int(substr($message,78,4))/10 . " " . + "DEW_point: " . hex2int(substr($message,86,4))/1 . " " . + "P_Nd: " . hex2int(substr($message,86,4))/100 . " " . #bar + "P_Hd: " . hex2int(substr($message,90,4))/100 . " " . #bar + "Actual_power_Qc: " . hex2int(substr($message,94,8))/1 . " " . #kw + "Actual_power_Pel: " . hex2int(substr($message,102,4))/1 . " " . #kw + "collector_temp: " . hex2int(substr($message, 4,4))/10 ; + } + when ("F5") { #unknownF5 + $message = $message . "\n" . + substr($message, 18,6) . "\n" . + substr($message, 6,6) . "\n" ; + } + when ("09") { #operating history + $message = "compressor_heating: " . hex(substr($message, 4,4)) . " " . + "compressor_cooling: " . hex(substr($message, 8,4)) . " " . + "compressor_dhw: " . hex(substr($message, 12,4)) . " " . + "booster_dhw: " . hex(substr($message, 16,4)) . " " . + "booster_heating: " . hex(substr($message, 20,4)) ; + } + when ("D1") { #last10errors non testato e dte non convertita + $message = "number_of_faults: " . hex(substr($message, 4,4)) . " " . + "fault0CODE: " . hex(substr($message, 8,4)) . " " . + "fault0TIME: " . hex(substr($message, 12,2)) . ":" . hex(substr($message, 14,2)) . " " . + "fault0DATE: " . hex(substr($message, 16,4)) . " " . + "fault1CODE: " . hex(substr($message, 20,4)) . " " . + "fault1TIME: " . hex(substr($message, 24,2)) . ":" . hex(substr($message, 26,2)) . " " . + "fault1DATE: " . hex(substr($message, 28,4)) . " " . + "fault2CODE: " . hex(substr($message, 32,4)) . " " . + "fault2TIME: " . hex(substr($message, 36,2)) . ":" . hex(substr($message, 38,2)) . " " . + "fault2DATE: " . hex(substr($message, 40,4)) . " " . + "fault3CODE: " . hex(substr($message, 44,4)) . " " . + "fault3TIME: " . hex(substr($message, 48,2)) . ":" . hex(substr($message, 50,2)) . " " . + "fault3DATE: " . hex(substr($message, 52,4)) . " " . + "fault4CODE: " . hex(substr($message, 56,4)) . " " . + "fault4TIME: " . hex(substr($message, 60,2)) . ":" . hex(substr($message, 62,2)) . " " . + "fault4DATE: " . hex(substr($message, 64,4)) . " " . + "fault5CODE: " . hex(substr($message, 68,4)) . " " . + "fault5TIME: " . hex(substr($message, 72,2)) . ":" . hex(substr($message, 72,2)) . " " . + "fault5DATE: " . hex(substr($message, 76,4)) . " " . + "fault6CODE: " . hex(substr($message, 80,4)) . " " . + "fault6TIME: " . hex(substr($message, 84,2)) . ":" . hex(substr($message, 86,2)) . " " . + "fault6DATE: " . hex(substr($message, 88,4)) ; + } + } + return (undef, $message); +} + + +####################################### +#THZ_Parse1($) could be used in order to test an external config file; I do not know if I want it +# +####################################### + +sub THZ_Parse1($) { +my %parsinghash = ( + "D1" => {"number_of_faults:" => [4,"hex",1], + "fault0CODE:" => [8,"hex",1], + "fault0DATE:" => [12,"hex",1] + }, + "D2" => {"number_of_faults:" => [8,"hex",1], + "fault0CODE:" => [8,"hex",1] + } +); + + my ($Msg) = @_; + my $parsingcmd = $parsinghash{substr($Msg,2,2)}; + my $ParsedMsg = $Msg; + if(defined($parsingcmd)) { + $ParsedMsg = ""; + foreach my $parsingkey (keys %$parsingcmd) { + my $positionInMsg = $parsingcmd->{$parsingkey}[0]; + my $Type = $parsingcmd->{$parsingkey}[1]; + my $divisor = $parsingcmd->{$parsingkey}[2]; + my $value = substr($Msg, $positionInMsg ,4); + given ($Type) { + when ("hex") { $value= hex($value);} + when ("hex2int") { $value= hex($value);} + } + $ParsedMsg = $ParsedMsg ." ". $parsingkey ." ". $value/$divisor ; + } + } + return (undef, $ParsedMsg); +} + + + + + +######################################################################################## +# only for debug +# +######################################################################################## +sub THZ_debugread($){ + my ($hash) = @_; + my ($err, $msg) =("", " "); + #my @numbers=('01', '09', '0B0005', '0B0008', '0C0005','0C0008','0A0013', '16', 'D1', 'D2', 'E8', 'E9', 'F2', 'F3', 'F4', 'F5', 'F6', 'FB', 'FC', 'FD', 'FE'); + # my @numbers=('0B14A2', '0B54A2', '0B2000', '0B2010', '0C2000','0A2008','0A3010', '0B54A2', '0B64A2', '0B7000', '0B8010', '0C8000','0A8008','0A9010'); + + #my @numbers = (1..255); + my @numbers = (1..65535); + my $indice= "FF"; + unlink("data.txt"); #delete debuglog + foreach $indice(@numbers) { + #my $cmd = sprintf("%02X", $indice); + my $cmd = "0A" . sprintf("%04X", $indice); + #my $cmd = $indice; + # STX start of text + THZ_Write($hash, "02"); + ($err, $msg) = THZ_ReadAnswer($hash); + # send request + my $cmdHex2 = THZ_encodecommand($cmd,"get"); + THZ_Write($hash, $cmdHex2); + ($err, $msg) = THZ_ReadAnswer($hash); + # ack datatranfer and read from the heatpump + THZ_Write($hash, "10"); + ($err, $msg) = THZ_ReadAnswer($hash); + THZ_Write($hash, "10"); + + #my $activatetrigger =1; + # readingsSingleUpdate($hash, $cmd, $msg, $activatetrigger); + # open (MYFILE, '>>data.txt'); + # print MYFILE ($cmdHex2 . "-" . $msg . "\n"); + # close (MYFILE); + + if (defined($err)) {return ($msg ."\n" . $err);} + else { #clean up and remove footer and header + ($err, $msg) = THZ_decode($msg); + if (defined($err)) {$msg=$cmdHex2 ."-". $msg ."-". $err;} + my $activatetrigger =1; + # readingsSingleUpdate($hash, $cmd, $msg, $activatetrigger); + open (MYFILE, '>>data.txt'); + print MYFILE ($cmd . "-" . $msg . "\n"); + close (MYFILE); + } + select(undef, undef, undef, 0.2); #equivalent to sleep 200ms + } +} + + +sub THZ_Undef($$) { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + foreach my $d (sort keys %defs) { + if(defined($defs{$d}) && + defined($defs{$d}{IODev}) && + $defs{$d}{IODev} == $hash) + { + my $lev = ($reread_active ? 4 : 2); + Log GetLogLevel($name,$lev), "deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + DevIo_CloseDev($hash); + return undef; +} + +1; + +=pod +=begin html + + +

THZ

+
    + THZ module: comunicate through serial interface (eg /dev/ttyxx) or through ser2net (e.g 10.0.x.x:5555) with a Tecalor/Eltron heatpump.
    + Tested on a THZ303 (with serial speed 57600) and a THZ403 (with serial speed 115200) with the same Firmware 4.39.
    + Tested on fritzbox, nas-qnap, raspi and macos.
    + This module is not working if you have an older firmware; Nevertheless, "parsing" could be easily updated, because now the registers are well described. + https://answers.launchpad.net/heatpumpmonitor/+question/100347
    + Implemented: read of status parameters and read/write of configuration parameters. +

    + + + Define +
      + define <name> THZ <device>
      +
      + device can take the same parameters (@baudrate, @directio, + TCP/IP, none) like the CUL, e.g 57600 baud or 115200.
      + Example: + direct connection +
        + define Mytecalor THZ /dev/ttyUSB0@115200
        +
      + or network connection (like via ser2net)
      +
        + define Myremotetecalor THZ 192.168.0.244:2323 +
      +
      +
        + define Mythz THZ /dev/ttyUSB0@115200
        + attr Mythz interval_allFB 300 # internal polling interval 5min
        + attr Mythz interval_history 28800 # internal polling interval 8h
        + define FileLog_Mythz FileLog ./log/Mythz-%Y.log Mythz
        +
      +
      + If the attributes interval_allFB and interval_history are not defined (or 0), their internal polling is disabled. + Clearly you can also define the polling interval outside the module with the "at" command. +
      +
        + define Mythz THZ /dev/ttyUSB0@115200
        + define atMythzFB at +*00:05:00 {fhem "get Mythz allFB","1";;return()}
        + define atMythz09 at +*08:00:00 {fhem "get Mythz history","1";;return()}
        + define FileLog_Mythz FileLog ./log/Mythz-%Y.log Mythz
        +
      + +
    +
    +
+ +=end html +=cut + + diff --git a/fhem/HISTORY b/fhem/HISTORY index da82b7d98..5523e15b6 100644 --- a/fhem/HISTORY +++ b/fhem/HISTORY @@ -544,4 +544,9 @@ - Added new module "pilight" - Sat Jan 18 2014 (tobiasfaust) - - Added new Module "Text2Speech" + - Added new Module "Text2Speech" + +- Sat Feb 16 2014 (immiimmi) + - Added new Module "THZ" + + diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index b1dea91da..7c1addffe 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -24,6 +24,7 @@ FHEM/00_LIRC.pm rudolfkoenig http://forum.fhem.de Sonstiges FHEM/00_MAXLAN.pm mgehre http://forum.fhem.de MAX FHEM/00_OWX.pm pahenning http://forum.fhem.de 1Wire FHEM/00_TCM.pm klaus-schauer http://forum.fhem.de EnOcean +FHEM/00_THZ.pm immiimmi http://forum.fhem.de Sonstiges FHEM/00_TUL.pm hotmaz http://forum.fhem.de KNX/EIB FHEM/00_ZWDongle.pm rudolfkoenig http://forum.fhem.de ZWave FHEM/01_FHEMWEB.pm rudolfkoenig http://forum.fhem.de Frontends