From daff73c258ee215a3594042d6b57323203ad37a0 Mon Sep 17 00:00:00 2001 From: StefanStrobel <> Date: Thu, 14 Apr 2022 16:30:23 +0000 Subject: [PATCH] 98_PHC.pm: own namespace and better documentation git-svn-id: https://svn.fhem.de/fhem/trunk@25959 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_PHC.pm | 1449 +++++++++++++++++++++++++------------------ 1 file changed, 851 insertions(+), 598 deletions(-) diff --git a/fhem/FHEM/98_PHC.pm b/fhem/FHEM/98_PHC.pm index 0b875632d..1fc422845 100755 --- a/fhem/FHEM/98_PHC.pm +++ b/fhem/FHEM/98_PHC.pm @@ -1,4 +1,4 @@ -############################################## +############################################################################## # $Id$ # fhem Modul für PHC # @@ -18,17 +18,15 @@ # along with fhem. If not, see . # ############################################################################## -# Changelog: -# -# 2017-02-11 started initial PoC version -# 2017-08-08 optimized logging, silentReconnect, singleLastCommand -# 2017-08-18 ping command, reset / config, sendRaw -# 2019-06-02 don't do event / lastcommmand reading for CLK -# +# First version: 2017-02-11 # # Todo / Ideas: # ============= # +# +# Timer im STM von Fhem aus ändern? +# Dimer setzen - zuerst DimProz in Byte 1, dann Zeit in Byte 2 wie bei heller / dunkler dimmen kodiert (Max 160) +# # command 01 (Ping), configure commands ... # # input validation for attrs (allowed EMD address) @@ -45,11 +43,9 @@ # Per Default auf eindeutige Befehle Readings setzen - Wert aus Command Hash (bei Ein / Aus, nicht bei Umschalten) # Dim Feedback codes (ein / aus) # -# Timer im STM von Fhem aus ändern? -# # What does not work: # =================== -# AMD direkt ansprechen +# AMD über Bus direkt ansprechen # Problem: keine Unterscheidung im Protokoll zwischen Befehlen vom STM und solchen von Modulen. Erst ACK ist Unterscheid # bei direktem Befehl meint STM dass es ein Feedback-Befeh ist. Die Acks überschneiden sich. # -> direktes Ansprechen von AMDs scheint nicht sinnvoll. @@ -59,30 +55,79 @@ # insgesamt kommen 63 Resends ... # - -package main; +package PHC; use strict; use warnings; -use Time::HiRes qw( gettimeofday tv_interval time ); -use Encode qw(decode encode); -sub PHC_Initialize($); -sub PHC_Define($$); -sub PHC_Undef($$); -sub PHC_Set($@); -sub PHC_Get($@); -sub PHC_Read($); -sub PHC_Ready($); -sub PHC_ReadAnswer($$$); -sub PHC_ParseFrames($); -sub PHC_HandleSendQueue($); -sub PHC_TimeoutSend($); +use GPUtils qw(:all); +use Time::HiRes qw(gettimeofday); +use Encode qw(decode encode); +use SetExtensions qw(:all); +use DevIo; +use FHEM::HTTPMOD::Utils qw(:all); -my $PHC_Version = '0.43 - 2.6.2019'; +use Exporter ('import'); +our @EXPORT_OK = qw(); +our %EXPORT_TAGS = (all => [@EXPORT_OK]); + + +BEGIN { + GP_Import( qw( + LoadModule + parseParams + CommandAttr + CommandDeleteAttr + addToDevAttrList + AttrVal + ReadingsVal + ReadingsTimestamp + readingsSingleUpdate + readingsBeginUpdate + readingsBulkUpdate + readingsEndUpdate + InternalVal + makeReadingName + DoTrigger + + Log3 + RemoveInternalTimer + InternalTimer + deviceEvents + EvalSpecials + AnalyzePerlCommand + CheckRegexp + + gettimeofday + FmtDateTime + GetTimeSpec + fhemTimeLocal + time_str2num + rtrim + + DevIo_OpenDev + DevIo_SimpleWrite + DevIo_SimpleRead + DevIo_CloseDev + SetExtensions + + featurelevel + defs + modules + attr + init_done + )); + + GP_Export( qw( + Initialize + )); +}; + + +my $Version = '0.70 - 8.3.2022'; -my %PHC_AdrType = ( +my %AdrType = ( 0x00 => ['EMD'], # Class 0 starts at 0x00 (EMD, ...) 0x20 => ['MCC', 'UIM'], # Class 1 starts at 0x20 / 32 (UIM, MCC, ...) 0x40 => ['AMD', 'JRM'], # Class 2 starts at 0x40 / 64 (AMD, JRM) @@ -94,8 +139,8 @@ my %PHC_AdrType = ( ); -# shr bits for channel, &-Mask for function -my %PHC_CodeSplit = ( +# number of bits to shift right for channel, &-Mask for function +my %CodeSplit = ( 'EMD' => [4, 0x0F], 'MCC' => [4, 0x0F], 'UIM' => [4, 0x0F], @@ -109,21 +154,24 @@ my %PHC_CodeSplit = ( # # Options: -# cbm Channel bits in Message -# cba Channel bits in Ack +# cbm Channel bits in command message +# cba Channel bits in ack +# cb2 2 Channel buts at the end of command message # o use name for output channel # i use name for input channel # t1 time in data bytes 1+2 # t2 time in data bytes 2+3, 4+5 and 6+7 if present +# dt1 dim time in data byte 1 (coded) and data byte 2 is zero +# dt2 dim time in data byte 2 (coded) and dim value (0-255) in data byte 1 # p priority information in data byte 1 # # format: 'Type Function Len Acklen' => ['Function Name', Options] -# len=1 means just one byte for function/channel +# Len=1 means just one byte for function/channel # e.g. a Frame with Adr, 81/01, Fkt/Chan CRC -my %PHC_functions = ( - 'EMD02+01' => ['Ein > 0', 'i'], +my %functions = ( + 'EMD02+01' => ['Ein > 0', 'i'], # EMD, Function 2, Len flexible, AckLen 1 'EMD03+01' => ['Aus < 1', 'i'], 'EMD04+01' => ['Ein > 1', 'i'], 'EMD05+01' => ['Aus > 1', 'i'], @@ -134,36 +182,35 @@ my %PHC_functions = ( 'EMD030103' => ['LED_Aus', 'o'], 'AMD010102' => ['Ping', 'cba'], - 'AMD020102' => ['Ein', 'cba'], - 'AMD030102' => ['Aus', 'cba'], - 'AMD040102' => ['An Lock', 'cba'], - 'AMD050102' => ['Aus Lock', 'cba'], - 'AMD060102' => ['Umschalten', 'cba'], - 'AMD070102' => ['Unlock', 'cba'], - 'AMD080302' => ['An verzögert', 'cba', 't1'], - 'AMD090302' => ['Aus verzögert', 'cba', 't1'], - 'AMD100302' => ['An mit Timer', 'cba', 't1'], - 'AMD110302' => ['Aus mit Timer', 'cba', 't1'], - 'AMD120302' => ['Umschalten verzögert', 'cba', 't1'], - 'AMD130302' => ['Umschalten mit Timer', 'cba', 't1'], - 'AMD140102' => ['Lock', 'cba'], - 'AMD150102' => ['Lock for time running', 'cba'], - 'AMD160302' => ['Timer Addieren', 'cba', 't1'], - 'AMD170302' => ['Timer setzen', 'cba', 't1'], - 'AMD180102' => ['Timer cancel', 'cba'], + 'AMD020102' => ['Ein', 'cba', 'o'], + 'AMD030102' => ['Aus', 'cba', 'o'], + 'AMD040102' => ['An Lock', 'cba', 'o'], + 'AMD050102' => ['Aus Lock', 'cba', 'o'], + 'AMD060102' => ['Umschalten', 'cba', 'o'], + 'AMD070102' => ['Unlock', 'cba', 'o'], + 'AMD080302' => ['An verzögert', 'cba', 't1', 'o'], + 'AMD090302' => ['Aus verzögert', 'cba', 't1', 'o'], + 'AMD100302' => ['An mit Timer', 'cba', 't1', 'o'], + 'AMD110302' => ['Aus mit Timer', 'cba', 't1', 'o'], + 'AMD120302' => ['Umschalten verzögert', 'cba', 't1', 'o'], + 'AMD130302' => ['Umschalten mit Timer', 'cba', 't1', 'o'], + 'AMD140102' => ['Lock', 'cba', 'o'], + 'AMD150102' => ['Lock for time running', 'cba', 'o'], + 'AMD160302' => ['Timer Addieren', 'cba', 't1', 'o'], + 'AMD170302' => ['Timer setzen', 'cba', 't1', 'o'], + 'AMD180102' => ['Timer cancel', 'cba', 'o'], 'AMD020201' => ['FB_Ein', 'cbm'], 'AMD030201' => ['FB_Aus', 'cbm'], 'AMD290201' => ['FB_Timer_Aus', 'cbm'], # kommt nach F10 wenn Zeit abgelaufen ist todo: check aus mit timer feedback? - - 'JRM020202' => ['Stop'], - 'JRM030402' => ['Umschalten heben stop', 'p', 't2'], - 'JRM040402' => ['Umschalten senken stop', 'p', 't2'], - 'JRM050402' => ['Heben', 'p', 't2'], - 'JRM060402' => ['Senken', 'p', 't2'], - 'JRM070402' => ['Flip auf', 'p', 't2'], - 'JRM080402' => ['Flip ab', 'p', 't2'], + 'JRM020202' => ['Stop', 'o'], + 'JRM030402' => ['Umschalten heben stop', 'p', 't2', 'o'], + 'JRM040402' => ['Umschalten senken stop', 'p', 't2', 'o'], + 'JRM050402' => ['Heben', 'p', 't2', 'o'], + 'JRM060402' => ['Senken', 'p', 't2', 'o'], + 'JRM070402' => ['Flip auf', 'p', 't2', 'o'], + 'JRM080402' => ['Flip ab', 'p', 't2', 'o'], 'JRM030201' => ['FB_Senken_Ein', 'cbm'], 'JRM040201' => ['FB_Heben_Aus', 'cbm'], @@ -181,39 +228,40 @@ my %PHC_functions = ( 'JRM12' => ['Lernen aus'], 'JRM130202' => ['Prio setzen', 'p'], 'JRM140202' => ['Prio löschen', 'p'], - 'JRM150602' => ['Sensor heben', 'p', 't2'], - 'JRM160802' => ['Sensor heben flip', 'p', 't2'], - 'JRM170602' => ['Sensor senken', 'p', 't2'], - 'JRM180802' => ['Sensor senken flip', 'p', 't2'], - 'JRM190302' => ['Zeitmessung verzögert an', 't1'], - 'JRM200302' => ['Zeitmessung verzögert aus', 't1'], - 'JRM210302' => ['Zeitmessung an mit Timer', 't1'], - 'JRM220102' => ['Zeitmessung cancel'], + 'JRM150602' => ['Sensor heben', 'p', 't2', 'o'], + 'JRM160802' => ['Sensor heben flip', 'p', 't2', 'o'], + 'JRM170602' => ['Sensor senken', 'p', 't2', 'o'], + 'JRM180802' => ['Sensor senken flip', 'p', 't2', 'o'], + 'JRM190302' => ['Zeitmessung verzögert an', 't1', 'o'], + 'JRM200302' => ['Zeitmessung verzögert aus', 't1', 'o'], + 'JRM210302' => ['Zeitmessung an mit Timer', 't1', 'o'], + 'JRM220102' => ['Zeitmessung cancel', 'o'], - 'DIM020102' => ['Ein Max mit Memory', 'cba'], - 'DIM030102' => ['Ein Max ohne Memory', 'cba'], - 'DIM040102' => ['Aus', 'cba'], - 'DIM050102' => ['Umschalten Max mit Memory', 'cba'], - 'DIM060102' => ['Umschalten Max ohne Memory', 'cba'], - 'DIM070302' => ['Dimmen Gegenrichtung', 'cba'], - 'DIM080302' => ['Heller Dimmen', 'cba'], - 'DIM090302' => ['Dunkler Dimmen', 'cba'], - 'DIM100102' => ['Speichern Memory', 'cba'], - 'DIM110102' => ['Umschalten Memory', 'cba'], - 'DIM120102' => ['Ein Memory', 'cba'], - 'DIM130102' => ['Speichern DIA1', 'cba'], - 'DIM140102' => ['Umschalten DIA1', 'cba'], - 'DIM150102' => ['Ein DIA1', 'cba'], - 'DIM160102' => ['Speichern DIA2', 'cba'], - 'DIM170102' => ['Umschalten DIA2', 'cba'], - 'DIM180102' => ['Ein DIA2', 'cba'], - 'DIM190102' => ['Speichern DIA3', 'cba'], - 'DIM200102' => ['Umschalten DIA3', 'cba'], - 'DIM210102' => ['Ein DIA3', 'cba'], - 'DIM220302' => ['Dimmwert und Zeit setzen', 'cba'], + 'DIM020102' => ['Ein Max mit Memory', 'cb2', 'o'], + 'DIM030102' => ['Ein Max ohne Memory', 'cb2', 'o'], + 'DIM040102' => ['Aus', 'cb2', 'o'], + 'DIM050102' => ['Umschalten Max mit Memory', 'cb2', 'o'], + 'DIM060102' => ['Umschalten Max ohne Memory', 'cb2', 'o'], + 'DIM070302' => ['Dimmen Gegenrichtung', 'cb2', 'dt1', 'o'], + 'DIM080302' => ['Heller Dimmen', 'cb2', 'dt1', 'o'], + 'DIM090302' => ['Dunkler Dimmen', 'cb2', 'dt1', 'o'], + 'DIM100102' => ['Speichern Memory', 'cb2', 'o'], + 'DIM110102' => ['Umschalten Memory', 'cb2', 'o'], + 'DIM120102' => ['Ein Memory', 'cb2', 'o'], + 'DIM130102' => ['Speichern DIA1', 'cb2'], + 'DIM140102' => ['Umschalten DIA1', 'cb2'], + 'DIM150102' => ['Ein DIA1', 'cb2'], + 'DIM160102' => ['Speichern DIA2', 'cb2'], + 'DIM170102' => ['Umschalten DIA2', 'cb2'], + 'DIM170302' => ['Umschalten DIA2', 'cb2'], # mcp reported that both variants can be observed + 'DIM180102' => ['Ein DIA2', 'cb2'], + 'DIM190102' => ['Speichern DIA3', 'cb2'], + 'DIM200102' => ['Umschalten DIA3', 'cb2'], + 'DIM210102' => ['Ein DIA3', 'cb2'], + 'DIM220302' => ['Dimmwert und Zeit setzen', 'cb2', 'dt2'], - 'DIM020201' => ['FB_Ein', 'cba'], - 'DIM030201' => ['FB_Aus', 'cba'], + 'DIM020201' => ['FB_Ein'], + 'DIM030201' => ['FB_Aus'], 'MFM020103' => ['Ein', 'o'], 'MFM030103' => ['Aus', 'o'], @@ -234,12 +282,13 @@ my %PHC_functions = ( 'MFM030101' => ['Taste I Ein > 0', 'i'], 'MFM040101' => ['Taste O Aus', 'i'], 'MFM050101' => ['Taste I Aus', 'i'], - 'MFM060101' => ['unknown'], - 'MFM070101' => ['unknown'], - 'MFM080101' => ['unknown'], - 'MFM090101' => ['unknown'], - 'MFM100101' => ['unknown'], - 'MFM110101' => ['unknown'], + 'MFM060101' => ['unknown06'], + 'MFM070101' => ['unknown07'], + 'MFM080101' => ['unknown08'], + 'MFM090101' => ['unknown09'], + 'MFM100101' => ['Taste O Ein > 1', 'i'], + 'MFM110101' => ['Taste I Ein > 1', 'i'], + 'MFM130301' => ['unknown13-3-1'], 'UIM020101' => ['On/Off Ein', 'i'], 'UIM040101' => ['Auf Ein', 'i'], @@ -251,162 +300,146 @@ my %PHC_functions = ( 'MCC090103' => ['LED Blink'], 'CLK03' => ['Clock Request'] - ); ##################################### -sub PHC_Initialize($) -{ - my ($hash) = @_; +# called when module is loaded +sub Initialize { + my $hash = shift; - require "$attr{global}{modpath}/FHEM/DevIo.pm"; - - $hash->{ReadFn} = "PHC_Read"; - $hash->{ReadyFn} = "PHC_Ready"; - $hash->{DefFn} = "PHC_Define"; - $hash->{UndefFn} = "PHC_Undef"; - $hash->{SetFn} = "PHC_Set"; - $hash->{GetFn} = "PHC_Get"; - $hash->{AttrFn} = "PHC_Attr"; + $hash->{ReadFn} = \&ReadFn; + $hash->{ReadyFn} = \&ReadyFn; + $hash->{DefFn} = \&DefineFn; + $hash->{UndefFn} = \&UndefFn; + $hash->{SetFn} = \&SetFn; + $hash->{GetFn} = \&GetFn; + $hash->{AttrFn} = \&AttrFn; $hash->{AttrList}= "do_not_notify:1,0 " . - "queueDelay " . - "timeout " . - "queueMax " . + #"timeout " . "silentReconnect " . - "singleLastCommandReading " . "sendEcho:1,0 " . "module[0-9]+description " . "module[0-9]+type " . "channel(EMD|AMD|JRM|DIM|UIM|MCC|MFM)[0-9]+[io]?[0-9]+description " . + "channel(EMD|AMD|JRM|DIM|UIM|MCC|MFM)[0-9]+[o]?[0-9]+set " . "virtEMD[0-9]+C[0-9]+Name " . # virtual emd channel for set - $readingFnAttributes; + "EMDReadings " . + 'HTTPMOD ' . + 'STM_ADR ' . + "BusEvents:none,simple,long " . + $main::readingFnAttributes; + + LoadModule "HTTPMOD"; + return; } -##################################### -sub PHC_Define($$) -{ - my ($hash, $def) = @_; - my @a = split("[ \t]+", $def); - my ($name, $PHC, $dev) = @a; - return "wrong syntax: define PHC [devicename]" - if(@a < 3); +############################################ +# called by Fhem to define a device as PHC +sub DefineFn { + my $hash = shift; # reference to device hash + my $def = shift; # definition string + my @a = split("[ \t]+", $def); # split definition string + my ($name, $PHC, $dev) = @a; # device name, "PHC", RS485 interface path + + return "wrong syntax: define PHC [devicename]" if(@a < 3); - eval "use Digest::CRC qw(crc crc32 crc16 crcccitt)"; + eval {use Digest::CRC qw(crc crc32 crc16 crcccitt)}; if($@) { return "Please install the Perl Digest Library (apt-get install libdigest-crc-perl) - error was $@"; } - $hash->{BUSY} = 0; - $hash->{EXPECT} = ""; - $hash->{ModuleVersion} = $PHC_Version; + $hash->{BUSY} = 0; + $hash->{EXPECT} = ''; + $hash->{ModuleVersion} = $Version; + return if ($dev eq 'none'); + if ($dev !~ /.+@([0-9]+)/) { - $dev .= '@19200,8,N,2'; + $dev .= '@19200,8,N,2'; # default baud rate } else { Log3 $name, 3, "$name: Warning: connection speed $1 is probably wrong for the PHC bus. Default is 19200,8,N,2" if ($1 != 19200); } - $hash->{DeviceName} = $dev; + $hash->{DeviceName} = $dev; $hash->{devioLoglevel} = (AttrVal($name, "silentReconnect", 0) ? 4 : 3); + DevIo_CloseDev($hash); my $ret = DevIo_OpenDev($hash, 0, 0); - return $ret; } ##################################### -sub PHC_Undef($$) -{ - my ($hash, $arg) = @_; +# called hen Fhem deletes a device +sub UndefFn { + my $hash = shift; + my $arg = shift; my $name = $hash->{NAME}; DevIo_CloseDev($hash); - return undef; + return; } +##################################### # Attr command -######################################################################### -sub PHC_Attr(@) -{ - my ($cmd,$name,$aName,$aVal) = @_; - # $cmd can be "del" or "set" - # $name is device name - # aName and aVal are Attribute name and value +sub AttrFn { + my $cmd = shift; # 'set' or 'del' + my $name = shift; # device name + my $aName = shift; # attribute name + my $aVal = shift; # attribute value + my $hash = $defs{$name}; # reference to the device hash - my $hash = $defs{$name}; - - Log3 $name, 5, "$name: Attr called with @_"; - if ($cmd eq "set") { - if ($aName =~ 'virtEMD([0-9]+)C([0-9]+)Name(.*)') { + Log3 $name, 5, "$name: Attr called with $cmd $name $aName $aVal"; + if ($cmd eq 'set') { + if ($aName =~ m{\A virtEMD ([0-9]+) C ([0-9]+) Name \z}xms) { my $modAdr = $1; my $cnlAdr = $2; if ($modAdr >= 32) { return "illegal EMD module address $modAdr - address needs to be < 32 and it must not be used on the bus"; } - if ($cnlAdr >= 15) { + if ($cnlAdr >= 16) { return "illegal EMD channel address $cnlAdr - address needs to be < 16"; } - - my @virtEMDList = grep (/virtEMD[0-9]+C[0-9]+Name/, keys %{$attr{$name}}); - my $emdAdr = ""; - my $emdChannel = ""; + my @virtEMDList = grep { m{\A virtEMD [0-9]+ C [0-9]+ Name \z}xms} keys %{$attr{$name}}; foreach my $attrName (@virtEMDList) { if ($aVal eq $attr{$name}{$attrName}) { # ist es der im konkreten Attr verwendete Name? - if ($attrName =~ /virtEMD([0-9]+)C([0-9]+)Name/) { - return "Name $aVal is already used for virtual EMD $modAdr channel $cnlAdr"; + if ($attrName ne $aName) { + return "Name $aVal is already used for virtual EMD $attrName"; } } } } } + return; } ##################################### -sub PHC_Get($@) -{ - my ($hash, @a) = @_; - my $name = $hash->{NAME}; - my $getName = $a[1]; +# get command +sub GetFn { + my @getValArr = @_; # rest is optional values + my $hash = shift @getValArr; # reference to device hash + my $name = shift @getValArr; # device name + my $getName = shift @getValArr; # get option name + my $getVal = join(' ', @getValArr); # optional value after get name - return "\"get $name\" needs at least one argument" if(@a < 2); - - return undef; + return "\"get $name\" needs at least one argument" if (!$getName); + return; } ##################################### -sub PHC_DoEMD($$$$) -{ - my ($hash, $modAdr, $channel, $fName) = @_; - my $name = $hash->{NAME}; - my $function = 0; - Log3 $name, 3, "$name: DoEMD called for module $modAdr, channel $channel, function $fName"; - foreach my $hkey (grep (/EMD/, keys %PHC_functions)) { - #Log3 $name, 5, "$name: hkey $hkey"; - my $fn = lc $PHC_functions{$hkey}[0]; - #Log3 $name, 5, "$name: fn $fn"; - $fn =~ s/ //g; - if ($fn =~ /(.*),.*/) { - $fn = $1; - } - #Log3 $name, 5, "$name: compare to $fn"; - if ($fn eq $fName) { - if ($hkey =~ /EMD0([0-9]).*/) { - $function = $1; - last; - } - } - } - return "function $fName not found" if (!$function); - Log3 $name, 5, "$name: found function $function"; - +# switch toggle +sub DoToggle { + my $hash = shift; # reference to Fhem device hash + my $modAdr = shift; # PHC module address + my $name = $hash->{NAME}; # Fhem device name + if ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 's') { $hash->{Toggle}{$modAdr} = 'c'; Log3 $name, 3, "$name: toggle for module $modAdr was set and will now be cleared"; @@ -417,455 +450,731 @@ sub PHC_DoEMD($$$$) $hash->{Toggle}{$modAdr} = 'c'; Log3 $name, 3, "$name: toggle for module $modAdr was unknown and will now be cleared"; } - my $len = 1 | (($hash->{Toggle}{$modAdr} eq 's' ? 1 : 0) << 7); + return; +} + + +##################################################### +# send PHC command +sub SendFrame { + my $hash = shift; # reference to Fhem device hash + my $modAdr = shift; # PHC module address + my $hexCmd = shift; # combined function and channel as hex string + my $name = $hash->{NAME}; # Fhem device name + + Log3 $name, 5, "$name: SendFrame called with hexCmd $hexCmd for module $modAdr"; + DoToggle($hash, $modAdr); - #Log3 $name, 3, "$name: len is $len"; - my $code = ($function + ($channel << 4)); - - my $frame = pack ('CCC', $modAdr, $len, $code); + my $lUTog = (length ($hexCmd) / 2) | (($hash->{Toggle}{$modAdr} eq 's' ? 1 : 0) << 7); + my $frame = pack ('CCH*', $modAdr, $lUTog, $hexCmd); my $crc = pack ('v', crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0)); - $frame = $frame . $crc; + $frame = $frame . $crc; - my $hFrame = unpack ('H*', $frame); - Log3 $name, 3, "$name: sends $hFrame"; + Log3 $name, 5, "$name: sends " . unpack ('H*', $frame); if (!AttrVal($name, "sendEcho", 0)) { my $now = gettimeofday(); $hash->{helper}{buffer} .= $frame; $hash->{helper}{lastRead} = $now; } - DevIo_SimpleWrite($hash, $frame, 0); + return; +} + + +######################################################## +# find PHC function in function hash +# to be called from DoEMD and setFn (new XMLRPC stuff) +sub FindFunction { + my $hash = shift; # reference to Fhem device hash + my $fName = shift; # input function name + my $mType = shift // 'EMD'; + my $name = $hash->{NAME}; # Fhem device name + my $function = 0; + $fName = '^' . lc($fName) . '$'; # input function name to be compared as regex + $fName =~ s/(\s|_| |(\\xc2\\xa0))+/_*/g; # function name with spaces as \s* + + FuncLOOP: # look for entry in PHC functions parse information hash (ModuleFuncLenAcklen => Name, Options) + foreach my $hkey (grep {m/^$mType/i} keys %functions) { + #Log3 $name, 5, "$name: FindFunction hkey $hkey"; + my $fn = lc $functions{$hkey}[0]; # function name in hash + #Log3 $name, 5, "$name: FindFunction fn >$fn<"; + $fn =~ s/ //g; # function name without spaces + $fn = $1 if ($fn =~ /(.*),.*/); # strip anything in the name after a komma ... todo: is this necessary? + Log3 $name, 5, "$name: FindFunction regex compare >$fn< to >$fName< "; + if ($fn =~ $fName) { # is this the function name passed? + Log3 $name, 4, "$name: FindFunction found function $fn as $hkey"; + return $hkey; + } + } + Log3 $name, 4, "$name: FindFunction did not find function for $fName on $mType" if (!$function); + return; +} + + +######################################################## +# find possible PHC output functions per module type +sub FindOutFunctions { + my $hash = shift; # reference to Fhem device hash + my $mType = shift; + my $name = $hash->{NAME}; # Fhem device name + my %fList; + + foreach my $hkey (grep {m/^$mType/i} keys %functions) { + my @opts = @{$functions{$hkey}}; + #Log3 $name, 5, "$name: FindOutFunctions: hkey $hkey, opts " . join (' ', @opts); + my $fn = lc $functions{$hkey}[0]; # function name in hash + $fn =~ s/\s/ /g; # convert spaces for fhemweb + #$fn =~ s/ /_/g; # function name without spaces + if (grep {/^o$/} @opts) { + $fList{$fn} = 1; + #Log3 $name, 5, "$name: FindOutFunctions: match fn >$fn<"; + } + } + my @functions = keys %fList; + Log3 $name, 5, "$name: FindOutFunctions: did find functions " . join (' ', @functions); + return @functions; +} + + +##################################################### +# EMD aktion (mod, channel, function) simulieren +# aufgerufen von set +sub DoEMD { + my $hash = shift; # reference to Fhem device hash + my $modAdr = shift; # PHC module address + my $channel = shift; # channel number in module + my $fName = shift; # input function name + my $name = $hash->{NAME}; # Fhem device name + + Log3 $name, 3, "$name: DoEMD called for module $modAdr, channel $channel, function $fName"; + my $hKey = FindFunction($hash, $fName, 'EMD'); + return "function $fName not found" if (!$hKey); + + if ($hKey !~ /EMD([0-9][0-9]).*/i) { + return "can not get function number for $hKey"; + } + my $function = $1; + my $code = ($function + ($channel << 4)); # EMD commands are code without aditional data, len is always 1 + my $hexCmd = unpack ('H2', pack ('C', $code)); + Log3 $name, 5, "$name: DoCmd: channel $channel, function $function => $code / $hexCmd"; + + SendFrame($hash, $modAdr, $hexCmd); + return; +} + + +##################################################### +# import PHC channel descriptions +sub DoImport { + my $hash = shift; + my $setVal = shift; + my $name = $hash->{NAME}; # Fhem device name + + my $iFile; + if (!open($iFile, "<", $setVal)) { + Log3 $name, 3, "$name: Cannot open template file $setVal"; + return "Cannot open template file $setVal"; + }; + my $mType = 'unknown'; + my $mAdr = 'unknown'; + my $aAdr = 'unknown'; + my $mName = 'unknown'; + my $mDisp = 'unknown'; + my $cType = 'i'; + my $encoding = ''; + my ($key, $cAdr, $cName); + while (<$iFile>) { + my $line = $_; + Log3 $name, 5, "$name: import read line $line"; + if ($line =~ /xml version.* encoding="([^\"]+)"/) { + $encoding = $1; + Log3 $name, 5, "$name: import encoding is $encoding"; + next; + } + $line = decode($encoding, $line) if ($encoding); + + if ($line =~ /^\s*/) { + $cType = 'o'; + } elsif ($line =~ //) { + $cType = 'i'; + } elsif ($line =~ /^\s*{NAME}; + my $hName = AttrVal($name, 'HTTPMOD', ''); # which HTTPMOD to use? (url defined there) + my $data = " $service "; + foreach my $arg (@args) { + $data .= "$arg"; # for now only a list of ints + } + $data .= " "; + Log3 $name, 4, "$name: XMLRPC called with $service and " . join (',', map {sprintf("0x%02X", $_)} @args); + Log3 $name, 5, "$name: XMLRPC data = $data"; + if ($hName && $defs{$hName} && $defs{$hName}{TYPE} && $defs{$hName}{TYPE} eq "HTTPMOD") { + HTTPMOD::AddToSendQueue($defs{$hName}, {'url' => $defs{$hName}{MainURL}, 'data' => $data, 'type' => 'external'}); + } else { + Log3 $name, 3, "$name: XMLRPC does not have valid HTTPMOD device. Please set attr HTTPMOD to a device configured to your STM with port 6680"; + } + return; +} + + +################################################# +# set an output via xmlrpc +sub DoChannelSet { + my $hash = shift; + my $modType = shift; + my $modAdr = shift; + my $modChannel = shift; + my $setVal = shift; + my $name = $hash->{NAME}; + + my $adrOffset = 0; + my $stmAdr = AttrVal($name, 'STM_ADR', 0); + my($args, $keys) = parseParams(lc($setVal)); + my $fName = join ' ', @{$args}; + + OFFLOOP: + foreach my $off (keys %AdrType) { + if (grep {/^$modType$/i} @{$AdrType{$off}}) { + $adrOffset = $off; + last OFFLOOP + } + } + $modAdr += $adrOffset; # absolute address + + Log3 $name, 5, "$name: DoChannelSet called for direct output on $modType (offset $adrOffset), adr $modAdr, ch $modChannel, $fName"; + my $hKey = FindFunction($hash, $fName, $modType); + return "function $fName not found" if (!$hKey); + + if ($hKey !~ /$modType([0-9][0-9]).*/i) { + return "can not get function number for $hKey"; + } + my $function = $1; + + my $split = $CodeSplit{uc($modType)}; # get number of bits to combine channel and function + return "unknown module type $modType - can not use splitCode" if (!$split); + my $cmdByte = ($modChannel << $split->[0]) + $function; + + my @parseOpts = @{$functions{$hKey}}; + Log3 $name, 5, "$name: function def = " . join ",", @parseOpts; + + shift @parseOpts; + my %opts; + foreach (@parseOpts) {$opts{$_} = 1}; + my @cmdOpts; + + if ($opts{'p'}) { + my $prio = $keys->{prio} // 3; # default prio 3 + return "illegal prio $prio" if ($prio > 7); + $prio |= 0x40 if ($keys->{set}); # set priority? + push @cmdOpts, $prio; + } + if ($opts{'t1'}||$opts{'t2'}) { + my $time = $keys->{time} // 600; # 60 secs as default + return "illegal time $time" if ($time > 3000); + my $t1 = int($time / 256); + my $t2 = int($time) % 256; + push @cmdOpts, $t2; # low byte + push @cmdOpts, $t1; # high byte + } + if ($opts{'dt1'}) { + my $time = $keys->{time} // 5; # 5 secs as default + return "illegal time $time" if ($time > 160); + my $t1 = int($time * 25 / 16); + push @cmdOpts, $t1; + push @cmdOpts, 0; + } + if ($opts{'dt2'}) { + my $value = $keys->{value} // 128; # 128 as default (50%) + my $time = $keys->{time} // 5; # 5 secs as default + return "illegal time $time" if ($time > 160); + my $t1 = int($time * 25 / 16); + push @cmdOpts, $value; + push @cmdOpts, $t1; + } + + XMLRPC($hash, 'service.stm.sendTelegram', $stmAdr, $modAdr, $cmdByte, @cmdOpts); + return; } ##################################### -sub PHC_Set($@) -{ - my ($hash, @a) = @_; - return "\"set $a[0]\" needs at least an argument" if(@a < 2); +# set comand +sub SetFn { + my @setValArr = @_; # remainder is set values + my $hash = shift @setValArr; # reference to Fhem device hash + my $name = shift @setValArr; # Fhem device name + my $setName = shift @setValArr; # name of the set option + my $setVal = join(' ', @setValArr); # set values as one string - my ($name, $setName, @setValArr) = @a; - my $setVal = (@setValArr ? join(' ', @setValArr) : ""); + Log3 $name, 5, "$name: SetFn called from " . FhemCaller() . " with $setName and $setVal"; + + return "\"set $name\" needs at least one argument" if(!$setName); if ($setName eq 'importChannelList') { - if ($setVal) { - my $iFile; - if (!open($iFile, "<", $setVal)) { - Log3 $name, 3, "$name: Cannot open template file $setVal"; - return; - }; - my $mType = 'unknown'; - my $mAdr = 'unknown'; - my $aAdr = 'unknown'; - my $mName = 'unknown'; - my $mDisp = 'unknown'; - my $cType = 'i'; - my ($key, $cAdr, $cName); - while (<$iFile>) { - Log3 $name, 5, "$name: import read line $_"; - if ($_ =~ //) { - $cType = 'o'; - } elsif ($_ =~ //) { - $cType = 'i'; - } elsif ($_ =~ //) { - my $rAdr = $1 & 0x1f; - $cAdr = sprintf('%02d', $rAdr); - $cName = encode ('UTF-8', $2); - $key = $mType . $mAdr . $cType . $cAdr; - CommandAttr(undef, "$name channel${key}description $cName"); - } - } - } else { - return "please specify a filename"; + if (!$setVal) { + return 'please specify a filename'; } - } elsif ($setName eq "emd") { + return DoImport($hash, $setVal); + } + elsif ($setName eq 'emd') { my @arg = @setValArr; - shift @arg; shift @arg; - my $fName = lc join('', @arg); - return PHC_DoEMD($hash, $setValArr[0], $setValArr[1], $fName); - - } elsif ($setName eq "sendRaw") { - - my $modAdr = $setValArr[0]; + shift @arg; + shift @arg; + my $fName = lc join(' ', @arg); + return DoEMD($hash, $setValArr[0], $setValArr[1], $fName); + } + elsif ($setName eq "sendRaw") { + my $modAdr = unpack ('H2', $setValArr[0]); my $hexCmd = $setValArr[1]; - - if ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 's') { - $hash->{Toggle}{$modAdr} = 'c'; - Log3 $name, 3, "$name: toggle for module $modAdr was set and will now be cleared"; - } elsif ($hash->{Toggle}{$modAdr} && $hash->{Toggle}{$modAdr} eq 'c') { - $hash->{Toggle}{$modAdr} = 's'; - Log3 $name, 5, "$name: toggle for module $modAdr was cleared and will now be set"; - } else { - $hash->{Toggle}{$modAdr} = 'c'; - Log3 $name, 3, "$name: toggle for module $modAdr was unknown and will now be cleared"; + SendFrame($hash, $modAdr, $hexCmd); + } + elsif ($setName =~ m{ (EMD|MCC|UIM|AMD|JRM|MFM|DIM) ([\d]+) o ([\d]+) }xmsi) { + my $modType = $1; + my $modAdr = $2; + my $modChannel = $3; + return DoChannelSet($hash, $1, $2, $3, $setVal); + } + else { + my @ChannelSetList = grep { m{channel (EMD|AMD|JRM|DIM|UIM|MCC|MFM) [0-9]+ [o]? [0-9]+ set}xms } keys %{$attr{$name}}; + my @setModHintList; + Log3 $name, 5, "$name: check setName $setName against attrs " . join ",", @ChannelSetList if ($setName ne '?'); + foreach my $setAttr (@ChannelSetList) { + if ($setAttr =~ m{channel (EMD|AMD|JRM|DIM|UIM|MCC|MFM) ([0-9]+) ([o]?) ([0-9]+) set}xms) { + my $modType = $1; + my $modAdr = $2; + my $o = $3; + my $chAdr = $4; + my $aName = "channel$modType$modAdr$o$chAdr" . 'description'; + my $aVal = SanitizeReadingName(lc($attr{$name}{$aName})); + my $nameCmp = SanitizeReadingName(lc($setName)); + $nameCmp =~ s/ //g; # channel name without spaces + Log3 $name, 5, "$name: compare $nameCmp with $aVal" if ($setName ne '?'); + if ($nameCmp eq $aVal) { + return DoChannelSet($hash, $modType, $modAdr, $chAdr, $setVal); + } + push @setModHintList, $aVal . ':' . join (',', FindOutFunctions($hash, $modType)); + } } - - my $len = (length ($hexCmd) / 2) | (($hash->{Toggle}{$modAdr} eq 's' ? 1 : 0) << 7); - #Log3 $name, 3, "$name: len is $len"; - - my $frame = pack ('H2CH*', $modAdr, $len, $hexCmd); - my $crc = pack ('v', crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0)); - $frame = $frame . $crc; - - my $hFrame = unpack ('H*', $frame); - Log3 $name, 3, "$name: sends $hFrame"; - - if (!AttrVal($name, "sendEcho", 0)) { - my $now = gettimeofday(); - $hash->{helper}{buffer} .= $frame; - $hash->{helper}{lastRead} = $now; - } - DevIo_SimpleWrite($hash, $frame, 0); - - } else { - my @virtEMDList = grep (/virtEMD[0-9]+C[0-9]+Name/, keys %{$attr{$name}}); - my $emdAdr = ""; - my $emdChannel = ""; + my @virtEMDList = grep { m{virtEMD [0-9]+ C [0-9]+ Name}xms } keys %{$attr{$name}}; foreach my $aName (@virtEMDList) { - if ($setName eq $attr{$name}{$aName}) { # ist es der im konkreten Set verwendete setName? - if ($aName =~ /virtEMD([0-9]+)C([0-9]+)Name/) { - $emdAdr = $1; # gefunden -> merke Nummer X im Attribut - $emdChannel = $2; + if (lc($setName) eq lc($attr{$name}{$aName})) { # ist es der im konkreten Set verwendete setName? + if ($aName =~ m{virtEMD ([0-9]+) C ([0-9]+) Name}xms) { + return DoEMD($hash, $1, $2, $setVal); } } } - if ($emdAdr eq "") { - # todo: map to values, add hints - my $hints = ":ein>0,ein>1,ein>2,aus,aus<1,aus>1"; - return "Unknown argument $setName, choose one of importChannelList sendRaw " . join (' ', map ($attr{$name}{$_} . $hints, @virtEMDList)); - } - return PHC_DoEMD($hash, $emdAdr, $emdChannel, $setVal); + # todo: also take input functions from functions hash + my $hints = "Unknown argument $setName, choose one of importChannelList sendRaw amd.*:ein,aus,umschalten" . + ' ' . join (' ', map { $attr{$name}{$_} . ':ein>0,ein>1,ein>2,aus,aus<1,aus>1' } @virtEMDList ) . + ' ' . join (' ', @setModHintList); + return $hints; } - return undef; + return; } - - -##################################### +############################################################################### # Called from ParseCommands -sub PHC_ParseCode($$) -{ - my ($hash, $command) = @_; - my $name = $hash->{NAME}; +# find out type of module at $command->{ADR} +# then split the code field into channel and function +# then search in functions hash for matching function and details / options +# set keys in command hash: MTYPE, CHANNEL, FUNCTION, FNAME, PARSEOPTS, CTYPE +# +sub ParsePHCCode { + my $hash = shift; # reference to Fhem device hash + my $command = shift; # reference to command hash containing ADR and CMD + my $name = $hash->{NAME}; # Fhem device name - my $fAdr = sprintf('%03d', $command->{ADR}); # formatted abs adr for attr lookup (mod type) + my $fAdr = sprintf('%03d', $command->{ADR}); # formatted abs module adr for attr lookup (mod type) my @typeArr = split (',', AttrVal($name, "module${fAdr}type", "")); # potential types from attr my $typeAttrLen = @typeArr; # number of potential types in attr - @typeArr = @{$PHC_AdrType{$command->{ADR} & 0xE0}} if (!@typeArr); # fallback to types from AdrType hash - my $mType = $typeArr[0]; # first option for split (same for all options) + @typeArr = @{$AdrType{$command->{ADR} & 0xE0}} if (!@typeArr); # fallback to types from AdrType hash + my $mType = $typeArr[0]; # first option for split (same for all options) - Log3 $name, 5, "$name: ParseCode called, fAdr $fAdr, typeArr = @typeArr, code " . sprintf ('x%02X', $command->{CODE}); + #Log3 $name, 5, "$name: ParseCode called, Adr $fAdr, typeArr = @typeArr, code " . sprintf ('x%02X', $command->{CODE}); #Log3 $name, 5, "$name: ParseCode data = @{$command->{DATA}}"; #Log3 $name, 5, "$name: ParseCode ackdata = @{$command->{ACKDATA}}"; - return PHC_LogCommand($hash, $command, "unknown module type", 3) if (!$mType); + return 'unknown module type' if (!$mType); $command->{MTYPE} = $mType; # first idea unless we find a fit later # splitting and therefore channel and function are the same within one address class # so they are ok to calculate here regardless of the exact module type identified later - my ($channel, $function) = PHC_SplitCode($hash, $mType, $command->{CODE}); + my ($channel, $function) = SplitPHCCode($hash, $mType, $command->{CODE}); $command->{CHANNEL} = $channel; $command->{FUNCTION} = $function; - my $key1 = sprintf('%02d', $function); - my $key2 = sprintf('%02d', $command->{LEN}); - my $key3 = sprintf('%02d', $command->{ACKLEN}); + my $key1 = sprintf('%02d', $function); # formatted function number + my $key2 = sprintf('%02d', $command->{LEN}); # formatted LEN + my $key3 = sprintf('%02d', $command->{ACKLEN}); # formatted ACKLEN my $wldk = '+'; my @keys = ("$mType$key1$key2$key3", "$mType$key1$wldk$key3", "$mType$key1$key2", "$mType$key1"); - Log3 $name, 5, "$name: ParseCode checks typelist @typeArr against" . - " F=" . sprintf ('x%02X', $function) . " C=" . sprintf ('x%02X', $channel) . "Len=$command->{LEN}, ackLen=$command->{ACKLEN}"; + Log3 $name, 5, "$name: ParseCode for Adr $fAdr checks typelist @typeArr against" . + " Fkt=" . sprintf ('x%02X', $function) . " Ch=" . sprintf ('x%02X', $channel) . + " Len=$command->{LEN}, ackLen=$command->{ACKLEN}"; + my $bestFit = 0; # any fit of key 3, 2 or 1 is better than 0 + TYPELOOP: foreach my $mTypePot (@typeArr) { #Log3 $name, 5, "$name: ParseCode checks if type of module at $fAdr can be $mTypePot"; - my $idx = 4; # fourlevels of abstraction in the PHC_functions hash # does this module type match better than a previously tested type? - foreach my $key (@keys) { - if ($PHC_functions{$key}) { + my $idx = 4; # four levels of abstraction in the functions hash + FUNCLOOP: + foreach my $key (@keys) { # four keys, one for each abstraction + if ($functions{$key}) { #Log3 $name, 5, "$name: match: $key"; if ($idx > $bestFit) { # longer = better matching type found - $bestFit = $idx; - my @parseOpts = @{$PHC_functions{$key}}; + $bestFit = $idx; # save for next type + my @parseOpts = @{$functions{$key}}; $command->{MTYPE} = $mTypePot; $command->{FNAME} = shift @parseOpts; foreach (@parseOpts) {$command->{PARSEOPTS}{$_} = 1}; Log3 $name, 5, "$name: ParseCode match $key / $command->{FNAME} " . join (',', @parseOpts); } - last; # first match is the best for this potential type - } else { - if (!$idx) { # this was the last try for this type with $idx=0, $key=$mTypePot$key1 - @typeArr = grep {!/$mTypePot/} @typeArr; # module type is not an option any more - Log3 $name, 5, "$name: ParseCode could not match to $mTypePot, delete this option"; - } + last FUNCLOOP; # first match is the best for this potential type + } + if (!$idx) { # this was the last try for this type with $idx=0, $key=$mTypePot$key1 + @typeArr = grep {!/$mTypePot/} @typeArr; # module type is not an option any more + Log3 $name, 5, "$name: ParseCode could not match to $mTypePot, delete this option"; + last FUNCLOOP; # not really necessary because at idx=0 FUNCLOOP is through anyway -> next TYPELOOP } $idx--; } } Log3 $name, 4, "$name: ParseCode typelist after matching is @typeArr" if (@typeArr > 1); - return PHC_LogCommand($hash, $command, "no parse info", 3) if (!$command->{FNAME}); + return 'no parse info' if (!$command->{FNAME}); $command->{CTYPE} = ($command->{PARSEOPTS}{'i'} ? 'i' : 'o'); if (!$typeAttrLen || (scalar(@typeArr) >= 1 && scalar(@typeArr) < $typeAttrLen)) { # no moduleType attr so far or we could eliminate an option -> set more specific new attr CommandAttr(undef, "$name module${fAdr}type " . join (',', @typeArr)); - Log3 $name, 4, "$name: set attr $name module${fAdr}type " . join (',', @typeArr); + #Log3 $name, 4, "$name: set attr $name module${fAdr}type " . join (',', @typeArr); } - return 1; + return; } ##################################### # Called from ParseCommands -sub PHC_ParseOptions($$) -{ - my ($hash, $command) = @_; - my $name = $hash->{NAME}; - my $dLen = @{$command->{DATA}}; +sub ParseOptions { + my $hash = shift; # reference to Fhem device hash + my $command = shift; # reference to command hash containing ADR and CMD + my $name = $hash->{NAME}; # Fhem device name + my $dLen = @{$command->{DATA}}; # length of Data - if ($command->{PARSEOPTS}{'p'}) { + if ($command->{PARSEOPTS}{'p'}) { # priority in data[1] $command->{PRIO} = unpack ('b6', pack ('C', $command->{DATA}[1] & 0x3F)); $command->{PSET} = $command->{DATA}[1] & 0x40; } - if ($command->{PARSEOPTS}{'t1'}) { + if ($command->{PARSEOPTS}{'t1'}) { # time in data[1] / data[2] (JRM) $command->{TIME1} = $command->{DATA}[1] + ($command->{DATA}[2] << 8) if ($dLen > 2); } - if ($command->{PARSEOPTS}{'t2'}) { + if ($command->{PARSEOPTS}{'t2'}) { # times in data[2/3] and data[4/5] ... if existant (JRM) $command->{TIME1} = $command->{DATA}[2] + ($command->{DATA}[3] << 8) if ($dLen > 3); $command->{TIME2} = $command->{DATA}[4] + ($command->{DATA}[5] << 8) if ($dLen > 5); $command->{TIME3} = $command->{DATA}[6] + ($command->{DATA}[7] << 8) if ($dLen > 7); } + + if ($command->{PARSEOPTS}{'dt1'}) { # time in data[1], data[2]=0 (DIM) + $command->{TIME1} = sprintf ('%.0f', $command->{DATA}[1]*16/25 + 0.1) if ($dLen > 2); + } + if ($command->{PARSEOPTS}{'dt2'}) { # time in data[1], data[2]=0 (DIM) + $command->{VALUE} = sprintf ('%.0f', $command->{DATA}[1]) if ($dLen > 2); + $command->{TIME1} = sprintf ('%.0f', $command->{DATA}[2]*16/25 + 0.1) if ($dLen > 2); + } + + return; } # todo: zumindest bei emds können mehrere codes (channel/function) nacheinender in einer message kommen # wenn zwei tasten gleichzeitig gedrückt werden... -##################################### -# Called from ParseFrames -sub PHC_ParseCommands($$) -{ - my ($hash, $command) = @_; - my $name = $hash->{NAME}; +########################################################################################## +# Called from ParseFrames when a valid command frame and its ACK have been received +# all data is in $hash->{COMMAND} +# call ParsePHCCode to split code into channel / function, find function name and options +# call ParseOptions and then set readings / create events +sub ParseCommands { + my $hash = shift; # reference to Fhem device hash + my $command = shift; # reference to command hash containing ADR and CMD + my $name = $hash->{NAME}; # Fhem device name - return if (!PHC_ParseCode($hash, $command)); - PHC_ParseOptions($hash, $command); - PHC_LogCommand($hash, $command, "", ($command->{MTYPE} eq "CLK" ? 5:4)); + my $err = ParsePHCCode($hash, $command) // ''; + ParseOptions($hash, $command) if (!$err); + my $lvl = ($err ? 3 : ($command->{MTYPE} eq "CLK" ? 5:4)); + LogCommand($hash, $command, $err, $lvl); + + # todo: new mode to set on/off depending on command instead of bits in ack message + # to avoid multiple events when a group of outputs on the same module is switched + # and every output creates a redundant event in every command - readingsBeginUpdate($hash); - - if ($command->{MTYPE} ne "CLK") { - readingsBulkUpdate($hash, 'LastCommand', PHC_CmdText($hash, $command)); - DoTrigger($name, PHC_ChannelText($hash, $command, $command->{CHANNEL}) . ": " . $command->{FNAME}); + return if ($command->{MTYPE} eq "CLK"); # don't handle this noisy message + + my $busEvents = AttrVal($name, "BusEvents", 'short'); # can be short, long or none + my $longChName = ChannelLongName($hash, $command, $command->{CHANNEL}); + my $shortChName = ChannelShortName($hash, $command, $command->{CHANNEL}); + my $cmd = $command->{FNAME}; + my $event; + if ($busEvents eq 'long') { + $event = $longChName; + } + elsif ($busEvents eq 'short') { + $event = $shortChName; + } # if attr was set to none then $event stays undef + if ($event) { + $event .= ': ' . $cmd if ($cmd); + DoTrigger($name, $event); + Log3 $name, 5, "$name: ParseCommands create Event $event"; + } + if (AttrVal($name, "EMDReadings", 0) && $command->{MTYPE} eq "EMD") { + readingsSingleUpdate($hash, $longChName, $cmd, 0); # descriptive reading of EMD command using the long name of the input channel but don't trigger event here + Log3 $name, 5, "$name: ParseCommands sets EMD reading $longChName to $cmd without event"; } + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'LastCommand', CmdDetailText($hash, $command)); # reading with full Log-Details of command + # channel bits aus Feedback / Ack verarbeiten - if ($command->{PARSEOPTS}{'cbm'} || $command->{PARSEOPTS}{'cba'}) { + if ($command->{PARSEOPTS}{'cbm'} || $command->{PARSEOPTS}{'cba'}) { # 8 channel bits in command message or in ACK my $bin = unpack ("B8", pack ("C", ($command->{PARSEOPTS}{'cbm'} ? $command->{DATA}[1] : $command->{ACKDATA}[1]))); - Log3 $name, 5, "$name: ParseCommand channel map = $bin"; + Log3 $name, 5, "$name: ParseCommands channel map = $bin"; my $channelBit = 7; foreach my $c (split //, $bin) { - my $bitName = PHC_ChannelDesc($hash, $command, $channelBit); - Log3 $name, 5, "$name: ParseCommand Reading for channel $channelBit is $c ($bitName)"; + my $bitName = ChannelLongName($hash, $command, $channelBit); + Log3 $name, 5, "$name: ParseCommands sets reading $bitName for channel $channelBit to $c"; + readingsBulkUpdate($hash, $bitName, $c) if ($bitName); + $channelBit --; + } + } + elsif ($command->{PARSEOPTS}{'cb2'}) { # 2 channel bits in ACKData (last two) + my $bin = substr (unpack ("B8", pack ("C", $command->{ACKDATA}[1])), -2); + Log3 $name, 5, "$name: ParseCommands channel map = $bin"; + my $channelBit = 1; + foreach my $c (split //, $bin) { + my $bitName = ChannelLongName($hash, $command, $channelBit); + Log3 $name, 5, "$name: ParseCommands sets reading $bitName for channel $channelBit to $c"; readingsBulkUpdate($hash, $bitName, $c) if ($bitName); $channelBit --; } } my @data = @{$command->{DATA}}; - if ($command->{PARSEOPTS}{'i'} && @data > 1) { + if ($command->{PARSEOPTS}{'i'} && @data > 1) { # input with more data -> more commands my $codeIdx = 1; # second code while ($codeIdx < @data) { - Log3 $name, 5, "$name: ParseCommand now handles additional code at Index $codeIdx"; + Log3 $name, 5, "$name: ParseCommands now handles additional code at Index $codeIdx"; $command->{CODE} = $data[$codeIdx]; - PHC_ParseCode($hash, $command); - PHC_LogCommand($hash, $command, "", 4); - DoTrigger($name, PHC_ChannelText($hash, $command, $command->{CHANNEL}) . ": " . $command->{FNAME}); + my $err = ParsePHCCode($hash, $command) // ''; + my $lvl = ($err ? 3 : 4); + LogCommand($hash, $command, $err, $lvl); + DoTrigger($name, ChannelShortName($hash, $command, $command->{CHANNEL}) . ": " . $command->{FNAME}); $codeIdx++; } - Log3 $name, 5, "$name: ParseCommand done"; + Log3 $name, 5, "$name: ParseCommands done"; } readingsEndUpdate($hash, 1); + return; } ##################################### # Called from the read functions -sub PHC_ParseFrames($) -{ +sub ParseFrames { my $hash = shift; my $name = $hash->{NAME}; - my $frame; - my $hFrame; - my ($adr, $xAdr, $len, $tog, $rest, $pld, $crc, $crc1); - my $rLen; - my @data; - + #Log3 $name, 5, "$name: Parseframes called"; use bytes; if (!$hash->{skipReason}) { - $hash->{skipBytes} = ""; - $hash->{skipReason} = ""; + $hash->{skipBytes} = ''; + $hash->{skipReason} = ''; }; + BUFLOOP: while ($hash->{helper}{buffer}) { $hash->{RAWBUFFER} = unpack ('H*', $hash->{helper}{buffer}); Log3 $name, 5, "$name: Parseframes: loop with raw buffer: $hash->{RAWBUFFER}" if (!$hash->{skipReason}); - $rLen = length($hash->{helper}{buffer}); + my $rLen = length($hash->{helper}{buffer}); return if ($rLen < 4); - ($adr, $len, $rest) = unpack ('CCa*', $hash->{helper}{buffer}); - $xAdr = unpack('H2', $hash->{helper}{buffer}); - $tog = $len >> 7; - $len = $len & 0x7F; + my ($adr, $lUTog, $rest) = unpack ('CCa*', $hash->{helper}{buffer}); + my $xAdr = unpack('H2', $hash->{helper}{buffer}); + my $tog = $lUTog >> 7; # toggle bit + my $len = $lUTog & 0x7F; # length if ($len > 30) { - Log3 $name, 5, "$name: Parseframes: len > 30, skip first byte of buffer $hash->{RAWBUFFER}"; - $hash->{skipBytes} .= substr ($hash->{helper}{buffer},0,1); - $hash->{skipReason} = "Len > 30" if (!$hash->{skipReason}); + #Log3 $name, 5, "$name: Parseframes: len > 30, skip first byte of buffer $hash->{RAWBUFFER}"; + $hash->{skipBytes} .= substr ($hash->{helper}{buffer}, 0, 1); # add, will be logged later + $hash->{skipReason} .= ($hash->{skipReason} ? ', ' : '') . 'Len > 30'; $hash->{helper}{buffer} = substr ($hash->{helper}{buffer}, 1); - next; + next BUFLOOP; } if (($rLen < 20) && ($rLen < $len + 4)) { Log3 $name, 5, "$name: Parseframes: len is $len so frame shoud be " . ($len + 4) . " but only $rLen read. wait for more"; return; } - $frame = substr($hash->{helper}{buffer},0,$len+2); # the frame (adr, tog/len, cmd/data) without crc - $hFrame = unpack ('H*', $frame); + my $frame = substr($hash->{helper}{buffer}, 0, $len + 2); # the frame (adr, tog/len, cmd/data) without crc + my $hFrame = unpack ('H*', $frame); - ($pld, $crc, $rest) = unpack ("a[$len]va*", $rest); # v = little endian unsigned short, n would be big endian - @data = unpack ('C*', $pld); - $crc = 0 if (!$crc); - $crc1 = crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0); - my $fcrc = unpack ("H*", pack ("v", $crc)); - my $fcrc1 = unpack ("H*", pack ("v", $crc1)); + # extract real pdu + my ($pld, $crc, $rest2) = unpack ("a[$len]va*", $rest); # v = little endian unsigned short, n would be big endian + my @data = unpack ('C*', $pld); + $crc = 0 if (!$crc); + # calculate CRC + my $crc1 = crc($frame, 16, 0xffff, 0xffff, 1, 0x1021, 1, 0); + my $fcrc = unpack ("H*", pack ("v", $crc)); # formatted crc as received + my $fcrc1 = unpack ("H*", pack ("v", $crc1)); # formatted crc as calculated + + # check CRC if ($crc != $crc1) { - #my $skip = $len + 4; my $skip = 1; - Log3 $name, 5, "$name: Parseframes: CRC error for $hFrame $fcrc, calc $fcrc1) - skip $skip bytes of buffer $hash->{RAWBUFFER}"; - $hash->{skipBytes} .= substr ($hash->{helper}{buffer},0,$skip); - $hash->{skipReason} = "CRC error" if (!$hash->{skipReason}); + #Log3 $name, 5, "$name: Parseframes: CRC error for $hFrame $fcrc, calc $fcrc1) - skip $skip bytes of buffer $hash->{RAWBUFFER}"; + $hash->{skipBytes} .= substr ($hash->{helper}{buffer}, 0, $skip); + $hash->{skipReason} .= ($hash->{skipReason} ? ', ' : '') . 'CRC Error'; $hash->{helper}{buffer} = substr ($hash->{helper}{buffer}, $skip); - next; + next BUFLOOP; } - Log3 $name, 4, "$name: Parseframes: skipped " . - unpack ("H*", $hash->{skipBytes}) . " reason: $hash->{skipReason}" + unpack ("H*", $hash->{skipBytes}) . " reason: $hash->{skipReason}" if $hash->{skipReason}; - $hash->{skipBytes} = ""; - $hash->{skipReason} = ""; - $hash->{helper}{buffer} = $rest; - Log3 $name, 5, "$name: Parseframes: Adr $adr/x$xAdr Len $len T$tog Data " . unpack ('H*', $pld) . " (Frame $hFrame $fcrc) Rest " . unpack ('H*', $rest) + $hash->{skipBytes} = ''; + $hash->{skipReason} = ''; + $hash->{helper}{buffer} = $rest2; + #Log3 $name, 5, "$name: Parseframes: Adr $adr/x$xAdr Len $len T$tog Data " . unpack ('H*', $pld) . " (Frame $hFrame $fcrc) Rest " . unpack ('H*', $rest2) + Log3 $name, 5, "$name: Parseframes: Adr $adr/x$xAdr Len $len T$tog Data " . unpack ('H*', $pld) . " (Frame $hFrame $fcrc)" if ($adr != 224); # todo: remove this filter later (hide noisy stuff) $hash->{Toggle}{$adr} = ($tog ? 's' : 'c'); # save toggle for potential own sending of data if ($hash->{COMMAND} && $hFrame eq $hash->{COMMAND}{FRAME}) { Log3 $name, 4, "$name: Parseframes: Resend of $hFrame $fcrc detected"; - next; + next BUFLOOP; } my $cmd = $data[0]; - if ($cmd == 1) { - # Ping / Ping response - if (!$hash->{COMMAND}) { + if ($cmd == 1) { # Ping / Ping response + if ($hash->{COMMAND} && $hash->{COMMAND}{CODE} == 1 + && $hash->{COMMAND}{ADR} == $adr) { # ping response + # response to a previous ping + Log3 $name, 5, "$name: Parseframes: Ping response received"; + $hash->{COMMAND}{ACKDATA} = \@data; + $hash->{COMMAND}{ACKLEN} = $len; + ParseCommands($hash, $hash->{COMMAND}); + delete $hash->{COMMAND}; # done with this command + next BUFLOOP; + } + if (!$hash->{COMMAND}) { # new ping request Log3 $name, 5, "$name: Parseframes: Ping request received"; - # fall through until $hash->{COMMAND} is set - } else { - if ($hash->{COMMAND}{CODE} == 1 && $hash->{COMMAND}{ADR} == $adr) { - # this must be the response - Log3 $name, 5, "$name: Parseframes: Ping response received"; - $hash->{COMMAND}{ACKDATA} = \@data; - $hash->{COMMAND}{ACKLEN} = $len; - PHC_ParseCommands($hash, $hash->{COMMAND}); - next; - } else { - # no reply to last command - ping or something else - now we seem to have a new ping request - Log3 $name, 4, "$name: Parseframes: new Frame $hFrame $fcrc but no ACK for valid last Frame $hash->{COMMAND}{FRAME} - dropping last one"; - delete $hash->{COMMAND}; # done with this command - # fall through until $hash->{COMMAND} is set - } + } + else { + Log3 $name, 4, "$name: Parseframes: new Frame $hFrame $fcrc but no ACK for valid last Frame $hash->{COMMAND}{FRAME} - dropping last one"; + delete $hash->{COMMAND}; # done with this command } - } elsif ($cmd == 254) { - # reset - # todo: get module name / type and show real type / adr in Log, add to comand reading or go through parsecommand with simulated acl len 0 ... - + my @oldData = @data; # save data in a new array that can be referenced by the command hash + $hash->{COMMAND} = {CODE => $data[0], ADR => $adr, LEN => $len, TOGGLE => $tog, DATA => \@oldData, FRAME => $hFrame}; + next BUFLOOP; + } + elsif ($cmd == 254) { # reset + # todo: get module name / type and show real type / adr in Log, add to comand reading or go through ParseCommands with simulated acl len 0 ... # parse payload in parsecommand # por byte, many channel/ function bytes - Log3 $name, 4, "$name: Parseframes: configuration request for adr $adr received - frame is $hFrame $fcrc"; delete $hash->{COMMAND}; # done with this command - next; - - } elsif ($cmd == 255) { - # reset + next BUFLOOP; + } + elsif ($cmd == 255) { # reset Log3 $name, 4, "$name: Parseframes: reset for adr $adr received - frame is $hFrame $fcrc"; delete $hash->{COMMAND}; # done with this command - next; - - } elsif ($cmd == 0) { - # ACK received + next BUFLOOP; + } + elsif ($cmd == 0) { # ACK received Log3 $name, 5, "$name: Parseframes: Ack received"; if ($hash->{COMMAND}) { if ($hash->{COMMAND}{ADR} != $adr) { Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc does not match adr of last Frame $hash->{COMMAND}{FRAME}"; - } elsif ($hash->{COMMAND}{TOGGLE} != $tog) { + } + elsif ($hash->{COMMAND}{TOGGLE} != $tog) { Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc does not match toggle of last Frame $hash->{COMMAND}{FRAME}"; - } else { # this ack is fine + } + else { # this ack is fine $hash->{COMMAND}{ACKDATA} = \@data; $hash->{COMMAND}{ACKLEN} = $len; - PHC_ParseCommands($hash, $hash->{COMMAND}); + ParseCommands($hash, $hash->{COMMAND}); } - delete $hash->{COMMAND}; # done with this command - next; - } else { + delete $hash->{COMMAND}; # done with this command + } + else { Log3 $name, 4, "$name: Parseframes: ACK frame $hFrame $fcrc without a preceeding request - dropping"; - next; } - } else { - # normal command - no ack, ping etc. + next BUFLOOP; + } + else { # normal command - no ack, ping etc. if ($hash->{COMMAND}) { Log3 $name, 4, "$name: Parseframes: new Frame $hFrame $fcrc but no ACK for valid last Frame $hash->{COMMAND}{FRAME} - dropping last one"; } Log3 $name, 5, "$name: Parseframes: $hFrame $fcrc is not an Ack frame, wait for ack to follow"; + my @oldData = @data; # save data in a new array that can be referenced by the command hash + $hash->{COMMAND} = {CODE => $data[0], ADR => $adr, LEN => $len, TOGGLE => $tog, DATA => \@oldData, FRAME => $hFrame}; # todo: set timeout timer if not ACK received } - my @oldData = @data; - $hash->{COMMAND}{ADR} = $adr; - $hash->{COMMAND}{LEN} = $len; - $hash->{COMMAND}{TOGGLE} = $tog; - $hash->{COMMAND}{DATA} = \@oldData; - $hash->{COMMAND}{CODE} = $oldData[0]; - $hash->{COMMAND}{FRAME} = $hFrame; - } + } # BUFLOOP + return; } ##################################### # Called from the global loop, when the select for hash->{FD} reports data -sub PHC_Read($) -{ +sub ReadFn { my $hash = shift; my $name = $hash->{NAME}; my $now = gettimeofday(); @@ -884,161 +1193,99 @@ sub PHC_Read($) $hash->{helper}{buffer} .= $buf; - PHC_ParseFrames($hash); + ParseFrames($hash); + return; } ##################################### -# Called from get / set to get a direct answer -sub PHC_ReadAnswer($$$) -{ - my ($hash, $arg, $expectReply) = @_; - my $name = $hash->{NAME}; - - return ("No FD", undef) - if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); - - my ($buf, $framedata, $cmd); - my $rin = ''; - my $to = AttrVal($name, "timeout", 2); # default is 2 seconds timeout - - Log3 $name, 5, "$name: ReadAnswer called for get $arg"; - for(;;) { - - if($^O =~ m/Win/ && $hash->{USBDev}) { - $hash->{USBDev}->read_const_time($to*1000); # set timeout (ms) - $buf = $hash->{USBDev}->read(999); - if(length($buf) == 0) { - Log3 $name, 3, "$name: Timeout in ReadAnswer for get $arg"; - return ("Timeout reading answer for $arg", undef); - } - } else { - if(!$hash->{FD}) { - Log3 $name, 3, "$name: Device lost in ReadAnswer for get $arg"; - return ("Device lost when reading answer for get $arg", undef); - } - - vec($rin, $hash->{FD}, 1) = 1; # setze entsprechendes Bit in rin - my $nfound = select($rin, undef, undef, $to); - if($nfound < 0) { - next if ($! == EAGAIN() || $! == EINTR() || $! == 0); - my $err = $!; - DevIo_Disconnected($hash); - Log3 $name, 3, "$name: ReadAnswer $arg: error $err"; - return("PHC_ReadAnswer $arg: $err", undef); - } - if($nfound == 0) { - Log3 $name, 3, "$name: Timeout2 in ReadAnswer for $arg"; - return ("Timeout reading answer for $arg", undef); - } - - $buf = DevIo_SimpleRead($hash); - if(!defined($buf)) { - Log3 $name, 3, "$name: ReadAnswer for $arg got no data"; - return ("No data", undef); - } - } - - if($buf) { - $hash->{helper}{buffer} .= $buf; - Log3 $name, 5, "$name: ReadAnswer got: " . unpack ("H*", $hash->{helper}{buffer}); - } - - $framedata = PHC_ParseFrames($hash); - } -} - - -##################################### -sub PHC_Ready($) -{ - my ($hash) = @_; +sub ReadyFn { + my $hash = shift; if ($hash->{STATE} eq "disconnected") { $hash->{devioLoglevel} = (AttrVal($hash->{NAME}, "silentReconnect", 0) ? 4 : 3); return DevIo_OpenDev($hash, 1, undef); } - # This is relevant for windows/USB only my $po = $hash->{USBDev}; my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; - return ($InBytes>0); } -####################################### -sub PHC_TimeoutSend($) -{ - my $param = shift; - my (undef,$name) = split(':',$param); - my $hash = $defs{$name}; - - Log3 $name, 3, "$name: timeout waiting for reply" . - ($hash->{EXPECT} ? " expecting " . $hash->{EXPECT} : "") . - " Request was " . $hash->{LASTREQUEST}; - $hash->{BUSY} = 0; - $hash->{EXPECT} = ""; -}; - - - ############################################################### # split code into channel and function depending on module type -sub PHC_SplitCode($$$) -{ - my ($hash, $mType, $code) = @_; - #Log3 $hash->{NAME}, 5, "$hash->{NAME}: PHC_SplitCode called with code $code and type $mType"; - my @splitArr = @{$PHC_CodeSplit{$mType}}; - Log3 $hash->{NAME}, 5, "$hash->{NAME}: SplitCode splits code " . - sprintf ('%02d', $code) . " for type $mType into " . - " channel " . ($code >> $splitArr[0]) . " / function " . ($code & $splitArr[1]); +sub SplitPHCCode { + my $hash = shift; # phc device hash ref + my $mType = shift; # module type (AMD, EMD, ...) + my $code = shift; # code byte in protocol to be split into channel and function + + #Log3 $hash->{NAME}, 5, "$hash->{NAME}: SplitPHCCode called with code $code and type $mType"; + my @splitArr = @{$CodeSplit{$mType}}; + #Log3 $hash->{NAME}, 5, "$hash->{NAME}: SplitCode splits code " . + # sprintf ('%02d', $code) . " for type $mType into " . + # " channel " . ($code >> $splitArr[0]) . " / function " . ($code & $splitArr[1]); return ($code >> $splitArr[0], $code & $splitArr[1]); # channel, function } - - ############################################################### # log message with command parse data -sub PHC_LogCommand($$$$) -{ +sub LogCommand { my ($hash, $command, $msg, $level) = @_; - Log3 $hash->{NAME}, $level, "$hash->{NAME}: " . PHC_Caller() . ' ' . PHC_CmdText($hash, $command) . " $msg"; + Log3 $hash->{NAME}, $level, "$hash->{NAME}: " . FhemCaller() . ' ' . CmdDetailText($hash, $command) . " $msg"; + return; } ############################################################### # get Text like EMD12i01 -sub PHC_ChannelText($$$) -{ - my ($hash, $command, $channel) = @_; +sub ChannelShortName { + my $hash = shift; # device hash + my $command = shift; # reference to command hash with ADR, MTYPE, CTYPE + my $channel = shift; # channel number + my $fmAdr = sprintf('%02d', ($command->{ADR} & 0x1F)); # relative module address formatted with two digits my $mType = $command->{MTYPE}; - return ($mType ? $mType . $fmAdr : 'Module' . sprintf ("x%02X", $command->{ADR})) . - ($command->{CTYPE} ? $command->{CTYPE} : "") . + my $cText = ($mType ? $mType . $fmAdr : 'Module' . sprintf ("x%02X", $command->{ADR})) . + ($command->{CTYPE} ? $command->{CTYPE} : "?") . (defined($channel) ? sprintf('%02d', $channel) : ""); + #Log3 $hash->{NAME}, 5, "$hash->{NAME}: ChannelText is $cText"; + return $cText; +} + + +############################################################### +# channel description if attr is defined +# or internal mod/chan text like EMD12i01 +sub ChannelLongName { + my $hash = shift; # device hash + my $command = shift; # reference to command hash with ADR, MTYPE, CTYPE + my $channel = shift; # channel number + my $name = $hash->{NAME}; # Fhem device name + my $cName = ChannelShortName($hash, $command, $channel); + my $descr = AttrVal($name, "channel${cName}description", ''); + my $bitName = SanitizeReadingName( $descr ? $descr : $cName); + #Log3 $hash->{NAME}, 5, "$hash->{NAME}: ChannelDesc is looking for $aName or $cName, Result name is $bitName"; + return $bitName; } ############################################################### # full detail of a command for logging -sub PHC_CmdText($$) -{ - my ($hash, $command) = @_; - - my $adr = $command->{ADR}; - my $mAdr = $adr & 0x1F; # relative module address - my $fmAdr = sprintf('%02d', $mAdr); - my $mType = $command->{MTYPE}; - my $channel = $command->{CHANNEL}; - my $cDesc = PHC_ChannelDesc($hash, $command, $channel); - my $start = PHC_ChannelText($hash, $command, $channel); +sub CmdDetailText { + my $hash = shift; # device hash + my $command = shift; # reference to command hash with ADR, MTYPE, CTYPE + my $channel = $command->{CHANNEL}; # channel on PHC module + my $cDesc = ChannelLongName($hash, $command, $channel); + my $start = ChannelShortName($hash, $command, $channel); return ($start ? $start : "") . - ($command->{FUNCTION} ? " F$command->{FUNCTION}" : "") . + (defined($command->{CHANNEL}) ? " Ch$command->{CHANNEL}" : "") . + (defined($command->{FUNCTION}) ? " F$command->{FUNCTION}" : "") . ($command->{FNAME} ? " $command->{FNAME}" : "") . (defined($command->{PRIO}) ? " P$command->{PRIO}" : "") . (defined($command->{PRIO}) ? ($command->{PSET} ? " (Set)" : " (no Set)") : "") . + (defined($command->{VALUE}) ? " Value $command->{VALUE}" : "") . (defined($command->{TIME1}) ? " Time1 $command->{TIME1}" : "") . (defined($command->{TIME2}) ? " Time2 $command->{TIME2}" : "") . (defined($command->{TIME3}) ? " Time3 $command->{TIME3}" : "") . @@ -1049,28 +1296,10 @@ sub PHC_CmdText($$) } -############################################################### -# channel description or internal mod/chan text -sub PHC_ChannelDesc($$$) -{ - my ($hash, $command, $channel) = @_; - my $name = $hash->{NAME}; - my $mAdr = $command->{ADR} & 0x1F; # relative module address - my $fmAdr = sprintf('%02d', $mAdr); - my $mType = $command->{MTYPE}; - - my $aName = "channel" . PHC_ChannelText($hash, $command, $channel) . "description"; - my $bName = PHC_ChannelText($hash, $command, $channel); - my $bitName = PHC_SanitizeReadingName(AttrVal($name, $aName, $bName)); - return $bitName; -} - - ############################################################### # convert description into a usable reading name -sub PHC_SanitizeReadingName($) -{ - my ($bitName) = @_; +sub SanitizeReadingName { + my $bitName = shift; $bitName =~ s/ä/ae/g; $bitName =~ s/ö/oe/g; $bitName =~ s/ü/ue/g; @@ -1089,18 +1318,6 @@ sub PHC_SanitizeReadingName($) } -########################################################### -# return the name of the caling function for debug output -# todo: remove main from caller function -sub PHC_Caller() -{ - my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash) = caller 2; - return $1 if ($subroutine =~ /main::PHC_(.*)/); - return $1 if ($subroutine =~ /main::(.*)/); - return "$subroutine"; -} - - 1; =pod @@ -1109,7 +1326,7 @@ sub PHC_Caller() =item summary_DE hört den PHC-Bus ab, erzeugt Events / Readings und simuliert EMDs =begin html - +

PHC

    PHC provides a way to communicate with the PHC bus from Peha. It listens to the communication on the PHC bus, tracks the state of output modules in readings and can send events to the bus / "Steuermodul" by simulating PHC input modules.
    @@ -1129,7 +1346,7 @@ sub PHC_Caller()

- + Define
    define <name> PHC <Device>
    @@ -1141,7 +1358,7 @@ sub PHC_Caller()

- + Configuration of the module

    The module doesn't need configuration to listen to the bus and create readings.
    @@ -1162,7 +1379,7 @@ sub PHC_Caller()

- + Set-Commands
    @@ -1179,37 +1396,72 @@ sub PHC_Caller() Every input channel for which an attribute like virtEMDxyCcName is defined will create a valid set option with name specified in the attribute.

- + Get-Commands
    none so far

- + Attributes

  • do_not_notify
  • readingFnAttributes
  • -
    -
  • virtEMDxyCcName
  • + +
  • BusEvents
    + this attribute controls what kind of events are generated when messages are received from the PHC bus regardless of any readings that might be created. + If set to simple (which is the default) then short events like EMD12i01: Ein > 0 will be generated. + If set to long then instead of EMD12i01 the module will use a name specified with a channel description attribue (see above). + If set to none then no events will be created except the ones that result from readings that are created when output modules send their channel state. +
  • +
  • HTTPMOD
    + Name of an HTTPMOD-device which is defined as http://your-stm-ip:6680/ 0 +
  • +
  • STM_ADR
    + Address of the control module (typically 0) to be used in the XMLRPC communication to directly set an output (STM Version 3 with ethernet connection only) +
  • +
  • EMDReadings
    + if this attribute is set to 1 then the module will create readings for each input channel. + These readings will contain the last command received from an input, e.g. ein>0
    + These readings will not create events because by default any input message on the bus will create an event anyway (see BusEvents). +
  • +
  • sendEcho
    + controls if bus commands sent should be fed back to the read function. +
  • +
  • virtEMD[0-9]+C[0-9]+Name Defines a virtual input module with a given address and a name for a channel of that input module.
    For example:
                 attr MyPHC virtEMD25C1Name LivingRoomLightSwitch
                 attr MyPHC virtEMD25C2Name KitchenLightSwitch
                 
    -
  • module[0-9]+description
  • + +
  • module[0-9]+description
    this attribute is typically created when you import a channel list with set MyPHCDevice importChannelList.
    It gives a name to a module. This name is used for better readability when logging at verbose level 4 or 5. -
  • module[0-9]+type
  • + +
  • module[0-9]+type
    this attribute is typically created when you import a channel list with set MyPHCDevice importChannelList.
    It defines the type of a module. This information is needed since some module types (e.g. EMD and JRM) use the same address space but a different protocol interpretation so parsing is only correct if the module type is known. -
  • channel(EMD|AMD|JRM|DIM|UIM|MCC|MFN)[0-9]+[io]?[0-9]+description
  • + +
  • channel(EMD|AMD|JRM|DIM|UIM|MCC|MFN)[0-9]+[io]?[0-9]+description
    this attribute is typically created when you import a channel list with set MyPHCDevice importChannelList.
    It defines names for channels of modules. These names are used for better readability when logging at verbose level 4 or 5. They also define the names of readings that are automatically created when the module listens to the PHC bus. +
  • +
  • channel(EMD|AMD|JRM|DIM|UIM|MCC|MFN)[0-9]+[io]?[0-9]+set
    + Only for STM version 3! This allows sending commands to output modules (so far tested with AMD, JRM or DIM) through the XML-RPC interface of a version 3 STM. + To work this feature needs an HTTPMOD device which is defined as http://your-stm-ip:6680/ 0
    + The name of this HTTPMOD device then needs to be linked here via an attr named HTTPMOD. +
  • +
  • silentReconnect
    + this attribute controls at what loglevel reconnect messages from devIO will be logged. Without this attribute they will be logged at level 3. + If this attribute is set to 1 then such messages will be logged at level 4. +


@@ -1218,3 +1470,4 @@ sub PHC_Caller() =end html =cut +