# $Id$ # This modules handles the communication with a TCM 120 or TCM 310 / TCM 400J / # TCM 515 EnOcean transceiver chip. As the protocols are radically different, # this is actually 2 drivers in one. # See also: # TCM_120_User_Manual_V1.53_02.pdf # EnOcean Serial Protocol 3 (ESP3) (for the TCM 310, TCM 400J, TCM 515) package main; use strict; use warnings; use DevIo; use Time::HiRes qw(gettimeofday usleep); my $dupTimeout = 0.6; my $modulesHash = \%modules; my $modulesType = 'TCM'; sub TCM_Read($); sub TCM_ReadAnswer($$); sub TCM_Ready($); sub TCM_Write($$$$); sub TCM_Parse120($$$); sub TCM_Parse310($$$); sub TCM_CRC8($); sub TCM_CSUM($); sub TCM_Initialize($) { my ($hash) = @_; # Provider my %matchList= ( "1:EnOcean" => "^EnOcean:", ); $hash->{Clients} = "EnOcean"; $hash->{MatchList} = \%matchList; $hash->{ReadFn} = "TCM_Read"; $hash->{ReadyFn} = "TCM_Ready"; $hash->{WriteFn} = "TCM_Write"; # Normal devices $hash->{AttrFn} = "TCM_Attr"; $hash->{DefFn} = "TCM_Define"; $hash->{FingerprintFn} = "TCM_Fingerprint"; $hash->{NotifyFn} = "TCM_Notify"; $hash->{NotifyOrderPrefix} = "45-"; $hash->{GetFn} = "TCM_Get"; $hash->{SetFn} = "TCM_Set"; $hash->{ShutdownFn} = "TCM_Shutdown"; $hash->{UndefFn} = "TCM_Undef"; $hash->{AttrList} = "assignIODev:select,no,yes baseID .baseIDSaved blockSenderID:own,no .chipIDSaved comModeUTE:auto,biDir,uniDir comType:TCM,RS485 do_not_notify:1,0 " . "dummy:1,0 fingerprint:off,on learningDev:all,teachMsg learningMode:always,demand,nearfield " . "msgCounter:select,off,on rcvIDShift sendInterval:0,25,40,50,100,150,200,250 smartAckMailboxMax:slider,0,1,20 " . "smartAckLearnMode:simple,advance,advanceSelectRep"; return undef; } # Define sub TCM_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $a[0]; my $model = $a[2]; return "TCM: wrong syntax, correct is: define TCM [ESP2|ESP3] {devicename[\@baudrate]|ip:port|none}" if (@a != 4 || $model !~ m/^(ESP2|ESP3|120|310)$/); DevIo_CloseDev($hash); my $dev = $a[3]; $hash->{DeviceName} = $dev; # old model names replaced $model = "ESP2" if ($model eq "120"); $model = "ESP3" if ($model eq "310"); $hash->{MODEL} = $model; $hash->{BaseID} = "00000000"; $hash->{LastID} = "00000000"; $hash->{NOTIFYDEV} = "global"; $modules{$modulesType}{devHash}{$name} = $hash; if($dev eq "none") { Log3 $name, 1, "TCM $name device is none, commands will be echoed only"; $attr{$name}{dummy} = 1; return undef; } InternalTimer(time() + 60, 'TCM_msgCounter', $hash, 0); my $ret = DevIo_OpenDev($hash, 0, undef); return $ret; } # Initialize serial communication sub TCM_InitSerialCom($) { my ($hash) = @_; my $name = $hash->{NAME}; delete $hash->{helper}{init_done}; if ($hash->{STATE} eq "disconnected") { Log3 $name, 2, "TCM $name not initialized"; return undef; } my $attrVal; my $comType = AttrVal($name, "comType", "TCM"); my $setCmdVal = ""; my @setCmd = ("set", "reset", $setCmdVal); # read and discard receive buffer, modem reset if ($hash->{MODEL} eq "ESP2") { if ($comType eq "TCM") { TCM_ReadAnswer($hash, "set reset"); #TCM_Read($hash); $hash->{PARTIAL} = ''; TCM_Set($hash, @setCmd); } } else { #TCM_ReadAnswer($hash, "set reset"); #TCM_Read($hash); $hash->{PARTIAL} = ''; delete $hash->{helper}{awaitCmdResp}; if (TCM_Set($hash, @setCmd) ne '') { $hash->{PARTIAL} = ''; delete $hash->{helper}{awaitCmdResp}; TCM_Set($hash, @setCmd); } } # default attributes my %setAttrInit; if ($comType eq "RS485" || $hash->{DeviceName} eq "none") { %setAttrInit = (sendInterval => {ESP2 => 100, ESP3 => 0}, learningMode => {ESP2 => "always", ESP3 => "always"} ); }else { %setAttrInit = ("sendInterval" => {ESP2 => 100, ESP3 => 0}); } foreach(keys %setAttrInit) { $attrVal = AttrVal($name, $_, undef); if(!defined $attrVal && defined $setAttrInit{$_}{$hash->{MODEL}}) { $attr{$name}{$_} = $setAttrInit{$_}{$hash->{MODEL}}; Log3 $name, 2, "TCM $name Attribute $_ $setAttrInit{$_}{$hash->{MODEL}} initialized"; } } # 750 ms pause usleep(750 * 1000); # read transceiver IDs my $baseID = AttrVal($name, "baseID", undef); if (defined $baseID) { $hash->{BaseID} = $baseID; $hash->{LastID} = sprintf "%08X", (hex $baseID) + 127; } elsif ($comType ne "RS485" && $hash->{DeviceName} ne "none") { my @getBaseID = ("get", "baseID"); if (TCM_Get($hash, @getBaseID) =~ /[Ff]{2}[\dA-Fa-f]{6}/) { $baseID = sprintf "%08X", hex $&; $baseID = $baseID eq 'F' x 8 ? '0' x 8 : $baseID; $attr{$name}{".baseIDSaved"} = $baseID; $hash->{BaseID} = $baseID; $hash->{LastID} = sprintf "%08X", (hex $baseID) + 127; } else { $baseID = AttrVal($name, ".baseIDSaved", '0' x 8); $hash->{BaseID} = $baseID; $hash->{LastID} = sprintf("%08X", (hex $baseID) + 127); } } if (defined $baseID) { push(@{$modules{"$hash->{TYPE}"}{BaseID}}, $baseID) if (!grep(/^$baseID$/, @{$modules{"$hash->{TYPE}"}{BaseID}})); @{$hash->{helper}{BaseID}} = @{$modules{"$hash->{TYPE}"}{BaseID}}; } my $chipID; if ($hash->{MODEL} eq "ESP3" && $comType ne "RS485" && $hash->{DeviceName} ne "none") { # get chipID my @getChipID = ('get', 'version'); if (TCM_Get($hash, @getChipID) =~ m/ChipID:.([\dA-Fa-f]{8})/) { $chipID = sprintf "%08X", hex $1; $attr{$name}{".chipIDSaved"} = $chipID; } else { $chipID = AttrVal($name, ".chipIDSaved", undef); } } if (defined $chipID) { $hash->{ChipID} = $chipID; push(@{$modules{"$hash->{TYPE}"}{ChipID}}, $hash->{ChipID}) if (!grep(/^$chipID$/, @{$modules{"$hash->{TYPE}"}{ChipID}})); @{$hash->{helper}{ChipID}} = @{$modules{"$hash->{TYPE}"}{ChipID}}; } # default transceiver parameter if ($comType ne "RS485" && $hash->{DeviceName} ne "none") { my %setCmdRestore = (mode => "00", maturity => "01", repeater => "RepEnable: 00 RepLevel: 00", smartAckMailboxMax => 0 ); foreach(keys %setCmdRestore) { $setCmdVal = ReadingsVal($name, $_, AttrVal($name, $_, undef)); if (defined $setCmdVal) { if ($_ eq "repeater") { $setCmdVal = substr($setCmdVal, 11, 2) . substr($setCmdVal, 24, 2); $setCmdVal = "0000" if ($setCmdVal eq "0001"); } @setCmd = ("set", $_, $setCmdVal); TCM_Set($hash, @setCmd); Log3 $name, 2, "TCM $name $_ $setCmdVal restored"; } else { if ($hash->{MODEL} eq "ESP2") { } else { if ($_ eq "repeater") { $setCmdVal = substr($setCmdRestore{$_}, 11, 2) . substr($setCmdRestore{$_}, 24, 2); } else { $setCmdVal = $setCmdRestore{$_}; } @setCmd = ("set", $_, $setCmdVal); my $msg = TCM_Set($hash, @setCmd); Log3 $name, 2, "TCM $name $_ $setCmdVal initialized" if ($msg eq ""); } } } } #CommandSave(undef, undef); $hash->{helper}{init_done} = 1; readingsSingleUpdate($hash, "state", "initialized", 1); Log3 $name, 2, "TCM $name initialized"; return undef; } sub TCM_Fingerprint($$) { my ($IODev, $msg) = @_; return ($IODev, $msg) if (AttrVal($IODev, "fingerprint", 'off') eq 'off'); my @msg = split(":", $msg); if ($msg[1] == 1) { # RADIO_ERP1 #EnOcean:PacketType:RORG:MessageData:SourceID:Status:OptionalData substr($msg[5], 1, 1, "0"); substr($msg[6], 0, 2, "01"); substr($msg[6], 10, 4, "0000"); } elsif ($msg[1] == 2) { # RESPONSE #EnOcean:PacketType:ResposeCode:MessageData:OptionalData # no dispatch } elsif ($msg[1] == 3) { # RADIO_SUB_TEL # no dispatch } elsif ($msg[1] == 4) { # EVENT #EnOcean:PacketType:eventCode:MessageData # no manipulation of the data necessary } elsif ($msg[1] == 5) { # COMMON_COMMAND # no dispatch } elsif ($msg[1] == 6) { # SMART_ACK_COMMAND #EnOcean:PacketType:smartAckCode:MessageData # no manipulation of the data necessary } elsif ($msg[1] == 7) { # REMOTE_MAN_COMMAND #EnOcean:PacketType:RORG:MessageData:SourceID:DestinationID:FunctionNumber:ManufacturerID:RSSI:Delay substr($msg[8], 0, 2, "00"); substr($msg[9], 0, 2, "00"); } elsif ($msg[1] == 9) { # RADIO_MESSAGE # no dispatch } elsif ($msg[1] == 10) { # RADIO_ERP2 # no dispatch } else { } $msg = join(":", @msg); #Log3 $IODev, 2, "TCM $IODev PacketType: $msg[1] Data: $msg"; return ($IODev, $msg); } # Write sub TCM_Write($$$$) { # Input is header and data (HEX), without CRC my ($hash, $shash, $header, $msg) = @_; #return if (!exists($hash->{helper}{init_done}) && $hash != $shash); # return if (!defined($header)); my $name = $hash->{NAME}; my $bstring; if ($hash->{MODEL} eq "ESP2") { # TCM 120 (ESP2) if (!$header) { # command with ESP2 format $bstring = $msg; } else { # command with ESP3 format my $packetType = hex(substr($header, 6, 2)); if ($packetType != 1) { Log3 $name, 1, "TCM $name Packet Type not supported."; return; } my $odataLen = hex(substr($header, 4, 2)); if ($odataLen != 0) { Log3 $name, 1, "TCM $name Radio Telegram with optional Data not supported."; return; } #my $mdataLen = hex(substr($header, 0, 4)); my $rorg = substr ($msg, 0, 2); # translate the RORG to ORG my %rorgmap = ("F6"=>"05", "D5"=>"06", "A5"=>"07", ); if($rorgmap{$rorg}) { $rorg = $rorgmap{$rorg}; } else { Log3 $name, 1, "TCM $name unknown RORG mapping for $rorg"; } if ($rorg eq "05" || $rorg eq "06") { $bstring = "6B" . $rorg . substr ($msg, 2, 2) . "000000" . substr ($msg, 4); } else { $bstring = "6B" . $rorg . substr ($msg, 2); } } $bstring = "A55A" . $bstring . TCM_CSUM($bstring); } else { # TCM 310 (ESP3) $bstring = "55" . $header . TCM_CRC8($header) . $msg . TCM_CRC8($msg); if (exists($hash->{helper}{telegramSentTimeLast}) && $hash->{helper}{telegramSentTimeLast} < gettimeofday() - 6) { # clear outdated response control list delete $hash->{helper}{awaitCmdResp}; } $hash->{helper}{telegramSentTimeLast} = gettimeofday(); if (exists $hash->{helper}{SetAwaitCmdResp}) { push(@{$hash->{helper}{awaitCmdResp}}, 1); delete $hash->{helper}{SetAwaitCmdResp}; } else { push(@{$hash->{helper}{awaitCmdResp}}, 0); } #Log3 $name, 5, "TCM $name awaitCmdResp: " . join(' ', @{$hash->{helper}{awaitCmdResp}}); } Log3 $name, 5, "TCM $name sent ESP: $bstring"; push(@{$hash->{helper}{sndCounter}}, time() + 0) if (AttrVal($hash->{NAME}, 'msgCounter', 'off') eq 'on'); DevIo_SimpleWrite($hash, $bstring, 1); # next commands will be sent with a delay usleep(int(AttrVal($name, "sendInterval", 100)) * 1000); } # ESP2 CRC # Used in the TCM120 sub TCM_CSUM($) { my $msg = shift; my $ml = length($msg); my @data; for(my $i = 0; $i < $ml; $i += 2) { push(@data, ord(pack('H*', substr($msg, $i, 2)))); } my $sum = 0; map { $sum += $_; } @data; return sprintf("%02X", $sum & 0xFF); } # ESP3 CRC-Table my @u8CRC8Table = ( 0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31, 0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65, 0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9, 0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd, 0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1, 0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2, 0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe, 0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a, 0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42, 0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80, 0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4, 0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8, 0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c, 0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10, 0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34, 0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f, 0x6A, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b, 0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7, 0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8D, 0x84, 0x83, 0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef, 0xfa, 0xfd, 0xf4, 0xf3 ); # ESP3 CRC # Used in the TCM310 sub TCM_CRC8($) { my $msg = shift; my $ml = length($msg); my @data; for(my $i = 0; $i < $ml; $i += 2) { push(@data, ord(pack('H*', substr($msg, $i, 2)))); } my $crc = 0; map { $crc = $u8CRC8Table[$crc ^ $_]; } @data; return sprintf("%02X", $crc); } # Read # called from the global loop, when the select for hash->{FD} reports data sub TCM_Read($) { my ($hash) = @_; my $buf = DevIo_SimpleRead($hash); return "" if (!defined($buf)); my $name = $hash->{NAME}; my $blockSenderID = AttrVal($name, "blockSenderID", "own"); my $data = $hash->{PARTIAL} . uc(unpack('H*', $buf)); Log3 $name, 5, "TCM $name received ESP: $data"; if($hash->{MODEL} eq "ESP2") { # TCM 120 while($data =~ m/^A55A(.B.{20})(..)/) { my ($net, $crc) = ($1, $2); my $mycrc = TCM_CSUM($net); my $rest = substr($data, 28); if($crc ne $mycrc) { Log3 $name, 2, "TCM $name wrong data checksum: got $crc, computed $mycrc" ; $data = $rest; next; } if($net =~ m/^0B(..)(........)(........)(..)/) { # Receive Radio Telegram (RRT) my ($org, $d1, $id, $status) = ($1, $2, $3, $4); my $packetType = 1; # shift rcvID range $id = sprintf("%08X", hex($id) + hex($attr{$name}{rcvIdShift})) if (defined $attr{$name}{rcvIdShift}); # Re-translate the ORG to RadioORG / TCM310 equivalent my %orgmap = ("05" => "F6", "06" => "D5", "07" => "A5"); if($orgmap{$org}) { $org = $orgmap{$org}; } else { Log3 $name, 2, "TCM $name unknown ORG mapping for $org"; } if ($org ne "A5") { # extract db_0 $d1 = substr($d1, 0, 2); } if (!defined TCM_BlockSenderID($hash, $blockSenderID, $id)) { Dispatch($hash, "EnOcean:$packetType:$org:$d1:$id:$status:01FFFFFFFF0000", undef) if (exists $hash->{helper}{init_done}); } } else { # Receive Message Telegram (RMT) my $msg = TCM_Parse120($hash, $net, 1); if (($msg eq 'OK') && ($net =~ m/^8B(..)(........)(........)(..)/)){ my ($org, $d1, $id, $status) = ($1, $2, $3, $4); my $packetType = 1; # shift rcvID range $id = sprintf("%08X", hex($id) + hex($attr{$name}{rcvIDShift})) if (defined $attr{$name}{rcvIDShift}); # Re-translate the ORG to RadioORG / TCM310 equivalent my %orgmap = ("05" => "F6", "06" => "D5", "07" => "A5"); if($orgmap{$org}) { $org = $orgmap{$org}; } else { Log3 $name, 2, "TCM $name unknown ORG mapping for $org"; } if ($org ne "A5") { # extract db_0 $d1 = substr($d1, 0, 2); } if (!defined TCM_BlockSenderID($hash, $blockSenderID, $id)) { Dispatch($hash, "EnOcean:$packetType:$org:$d1:$id:$status:01FFFFFFFF0000", undef) if (exists $hash->{helper}{init_done}); } } } $data = $rest; push(@{$hash->{helper}{rcvCounter}}, time() + 0) if (AttrVal($hash->{NAME}, 'msgCounter', 'off') eq 'on'); } if(length($data) >= 4) { $data =~ s/.*A55A/A55A/ if($data !~ m/^A55A/); $data = "" if($data !~ m/^A55A/); } } else { # TCM310 / ESP3 while($data =~ m/^55(....)(..)(..)(..)/) { my ($ldata, $lodata, $packetType, $crc) = (hex($1), hex($2), hex($3), $4); my $tlen = 2*(7+$ldata+$lodata); # data telegram incomplete last if(length($data) < $tlen); my $rest = substr($data, $tlen); $data = substr($data, 0, $tlen); my $hdr = substr($data, 2, 8); my $mdata = substr($data, 12, $ldata * 2); my $odata = substr($data, 12 + $ldata * 2, $lodata * 2); my $mycrc = TCM_CRC8($hdr); if($mycrc ne $crc) { Log3 $name, 2, "TCM $name wrong header checksum: got $crc, computed $mycrc" ; $data = $rest; next; } $mycrc = TCM_CRC8($mdata . $odata); $crc = substr($data, -2); if($mycrc ne $crc) { Log3 $name, 2, "TCM $name wrong data checksum: got $crc, computed $mycrc" ; $data = $rest; next; } if ($packetType == 1) { # packet type RADIO $mdata =~ m/^(..)(.*)(........)(..)$/; my ($org, $d1, $id, $status) = ($1,$2,$3,$4); my $repeatingCounter = hex substr($status, 1, 1); $odata =~ m/^(..)(........)(..)(..)$/; my ($RSSI, $receivingQuality) = (hex($3), "excellent"); if ($RSSI > 87) { $receivingQuality = "bad"; } elsif ($RSSI > 75) { $receivingQuality = "good"; } my %addvals = ( PacketType => $packetType, SubTelNum => hex($1), DestinationID => $2, RSSI => -$RSSI, ReceivingQuality => $receivingQuality, RepeatingCounter => $repeatingCounter, ); $hash->{RSSI} = -$RSSI; if (!defined TCM_BlockSenderID($hash, $blockSenderID, $id)) { #EnOcean:PacketType:RORG:MessageData:SourceID:Status:OptionalData Dispatch($hash, "EnOcean:$packetType:$org:$d1:$id:$status:$odata", \%addvals) if (exists $hash->{helper}{init_done}); } } elsif ($packetType == 2) { # packet type RESPONSE if (defined $hash->{helper}{awaitCmdResp}[0] && $hash->{helper}{awaitCmdResp}[0]) { # do not execute if transceiver command answer is expected $data .= $rest; last; } shift(@{$hash->{helper}{awaitCmdResp}}); $mdata =~ m/^(..)(.*)$/; my $rc = $1; my %codes = ( "00" => "OK", "01" => "ERROR", "02" => "NOT_SUPPORTED", "03" => "WRONG_PARAM", "04" => "OPERATION_DENIED", "05" => "LOCK_SET", "82" => "FLASH_HW_ERROR", "90" => "BASEID_OUT_OF_RANGE", "91" => "BASEID_MAX_REACHED", ); my $rcTxt = $codes{$rc} if($codes{$rc}); Log3 $name, $rc eq "00" ? 5 : 2, "TCM $name RESPONSE: $rcTxt"; #$packetType = sprintf "%01X", $packetType; #EnOcean:PacketType:ResposeCode:MessageData:OptionalData #Dispatch($hash, "EnOcean:$packetType:$1:$2:$odata", undef); } elsif ($packetType == 3) { # packet type RADIO_SUB_TEL Log3 $name, 2, "TCM $name packet type RADIO_SUB_TEL not supported: $data"; } elsif ($packetType == 4) { # packet type EVENT $mdata =~ m/^(..)(.*)$/; $packetType = sprintf "%01X", $packetType; my $eventCode = $1; my $messageData = $2; if (hex($eventCode) <= 3) { #EnOcean:PacketType:eventCode:messageData Dispatch($hash, "EnOcean:$packetType:$eventCode:$messageData", undef); } elsif (hex($eventCode) == 4) { # CO_READY my @resetCause = ('voltage_supply_drop', 'reset_pin', 'watchdog', 'flywheel', 'parity_error', 'hw_parity_error', 'memory_request_error', 'wake_up', 'wake_up', 'unknown'); my @secureMode = ('standard', 'extended'); $hash->{RESET_CAUSE} = $resetCause[hex($messageData)]; $hash->{SECURE_MODE} = $secureMode[hex($odata)]; DoTrigger($name, "EVENT: RESET_CAUSE: $hash->{RESET_CAUSE}"); DoTrigger($name, "EVENT: SECURE_MODE: $hash->{SECURE_MODE}"); Log3 $name, 2, "TCM $name EVENT RESET_CAUSE: $hash->{RESET_CAUSE} SECURE_MODE: $hash->{SECURE_MODE}"; } elsif (hex($eventCode) == 5) { # CO_EVENT_SECUREDEVICES } elsif (hex($eventCode) == 6) { # CO_DUTYCYCLE_LIMIT my @dutycycleLimit = ('released', 'reached'); $hash->{DUTYCYCLE_LIMIT} = $dutycycleLimit[hex($messageData)]; DoTrigger($name, "EVENT: DUTYCYCLE_LIMIT: $hash->{DUTYCYCLE_LIMIT}"); Log3 $name, 2, "TCM $name EVENT DUTYCYCLE_LIMIT: $hash->{DUTYCYCLE_LIMIT}"; } elsif (hex($eventCode) == 7) { # CO_TRANSMIT_FAILED my @transmitFailed = ('CSMA_failed', 'no_ack_received'); $hash->{TRANSMIT_FAILED} = $transmitFailed[hex($messageData)]; DoTrigger($name, "EVENT: TRANSMIT_FAILED: $hash->{TRANSMIT_FAILED}"); Log3 $name, 2, "TCM $name EVENT TRANSMIT_FAILED: $hash->{TRANSMIT_FAILED}"; } else { } } elsif ($packetType == 5) { # packet type COMMON_COMMAND Log3 $name, 2, "TCM $name packet type COMMON_COMMAND not supported: $data"; } elsif ($packetType == 6) { # packet type SMART_ACK_COMMAND $mdata =~ m/^(..)(.*)$/; $packetType = sprintf "%01X", $packetType; #EnOcean:PacketType:smartAckCode:MessageData Dispatch($hash, "EnOcean:$packetType:$1:$2", undef) if (exists $hash->{helper}{init_done}); } elsif ($packetType == 7) { # packet type REMOTE_MAN_COMMAND $mdata =~ m/^(....)(....)(.*)$/; my ($function, $manufID, $messageData) = ($1, $2, $3); $odata =~ m/^(........)(........)(..)(..)$/; my ($RSSI, $receivingQuality) = ($3, "excellent"); if (hex($RSSI) > 87) { $receivingQuality = "bad"; } elsif (hex($RSSI) > 75) { $receivingQuality = "good"; } my %addvals = ( PacketType => $packetType, DestinationID => $1, RSSI => -hex($RSSI), ReceivingQuality => $receivingQuality, ); $hash->{RSSI} = -hex($RSSI); $packetType = sprintf "%01X", $packetType; if (!defined TCM_BlockSenderID($hash, $blockSenderID, $2)) { #EnOcean:PacketType:RORG:MessageData:SourceID:DestinationID:FunctionNumber:ManufacturerID:RSSI:Delay Dispatch($hash, "EnOcean:$packetType:C5:$messageData:$2:$1:$function:$manufID:$RSSI:$4", \%addvals) if (exists $hash->{helper}{init_done}); } } elsif ($packetType == 9) { # packet type RADIO_MESSAGE Log3 $name, 2, "TCM: $name packet type RADIO_MESSAGE not supported: $data"; } elsif ($packetType == 10) { # packet type RADIO_ERP2 Log3 $name, 2, "TCM $name packet type RADIO_ADVANCED not supported: $data"; } else { Log3 $name, 2, "TCM $name unknown packet type $packetType: $data"; } $data = $rest; push(@{$hash->{helper}{rcvCounter}}, time() + 0) if (AttrVal($hash->{NAME}, 'msgCounter', 'off') eq 'on'); } if(length($data) >= 4) { $data =~ s/.*55/55/ if($data !~ m/^55/); $data = "" if($data !~ m/^55/); } } $hash->{PARTIAL} = $data; } # Parse Table TCM 120 my %parsetbl120 = ( "8B05" => { msg=>"OK" }, "8B06" => { msg=>"OK" }, "8B07" => { msg=>"OK" }, "8B08" => { msg=>"ERR_SYNTAX_H_SEQ" }, "8B09" => { msg=>"ERR_SYNTAX_LENGTH" }, "8B0A" => { msg=>"ERR_SYNTAX_CHKSUM" }, "8B0B" => { msg=>"ERR_SYNTAX_ORG" }, "8B0C" => { msg=>"ERR_MODEM_DUP_ID" }, "8B19" => { msg=>"ERR" }, "8B1A" => { msg=>"ERR_IDRANGE" }, "8B22" => { msg=>"ERR_TX_IDRANGE" }, "8B28" => { msg=>"ERR_MODEM_NOTWANTEDACK" }, "8B29" => { msg=>"ERR_MODEM_NOTACK" }, "8B58" => { msg=>"OK" }, "8B8C" => { msg=>"INF_SW_VER", expr=>'"$a[2].$a[3].$a[4].$a[5]"' }, "8B88" => { msg=>"INF_RX_SENSIVITY", expr=>'$a[2] ? "High (01)":"Low (00)"' }, "8B89" => { msg=>"INFO", expr=>'substr($rawstr,2,9)' }, "8B98" => { msg=>"INF_IDBASE", expr=>'sprintf("%02x%02x%02x%02x", $a[2], $a[3], $a[4], $a[5])' }, "8BA8" => { msg=>"INF_MODEM_STATUS", expr=>'sprintf("%s, ID:%02x%02x", $a[2]?"on":"off", $a[3], $a[4])' }, ); # Parse TCM 120 sub TCM_Parse120($$$) { my ($hash,$rawmsg,$ret) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "TCM $name Parse $rawmsg"; my $msg = ""; my $cmd = $parsetbl120{substr($rawmsg, 0, 4)}; if(!$cmd) { $msg ="Unknown command: $rawmsg"; } else { if($cmd->{expr}) { $msg = $cmd->{msg}." " if(!$ret); my $rawstr = pack('H*', $rawmsg); $rawstr =~ s/[\r\n]//g; my @a = map { ord($_) } split("", $rawstr); $msg .= eval $cmd->{expr}; } else { return "" if($cmd ->{msg} eq "OK" && !$ret); # SKIP Ok $msg = $cmd->{msg}; } } Log3 $name, 2, "TCM $name RESPONSE: $msg" if(!$ret); return $msg; } # Parse Table TCM 310 my %rc310 = ( "00" => "OK", "01" => "ERROR", "02" => "NOT_SUPPORTED", "03" => "WRONG_PARAM", "04" => "OPERATION_DENIED", "05" => "LOCK_SET", "82" => "FLASH_HW_ERROR", "90" => "BASEID_OUT_OF_RANGE", "91" => "BASEID_MAX_REACHED", ); # Parse TCM 310 sub TCM_Parse310($$$) { my ($hash,$rawmsg,$ptr) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "TCM $name TCM_Parse $rawmsg"; my $rc = substr($rawmsg, 0, 2); my $msg = ""; if($rc ne "00") { $msg = $rc310{$rc}; $msg = "Unknown return code $rc" if(!$msg); } else { my @ans; foreach my $k (sort keys %{$ptr}) { next if($k eq "cmd" || $k eq "oCmd" || $k eq "arg" || $k eq "packetType"); my ($off, $len, $type) = split(",", $ptr->{$k}); my $data; if ($len == 0) { $data = substr($rawmsg, $off*2); } else { $data = substr($rawmsg, $off*2, $len*2); } if($type) { if ($type eq "STR") { $data = pack('H*', $data); #### # remove trailing 0x00 #$data =~ s/[^A-Za-z0-9#\.\-_]//g; $data =~ tr/A-Za-z0-9#.-_//cd; } else { my $dataLen = length($data); my $dataOut = ''; my ($part1, $part2, $part3) = split(":", $type); $part1 *= 2; $part2 *= 2; if (defined $part3) { $part3 *= 2; while ($dataLen > 0) { $data =~ m/^(.{$part1})(.{$part2})(.{$part3})(.*)$/; $dataOut .= $1 . ':' . $2 . ':' . $3 . ' '; $data = $4; $dataLen -= $part1 + $part2 + $part3; } } else { while ($dataLen > 0) { $data =~ m/^(.{$part1})(.{$part2})(.*)$/; $dataOut .= $1 . ':' . $2 . ' '; $data = $3; $dataLen -= $part1 + $part2; } } chop($dataOut); $data = $dataOut; } } push @ans, "$k: $data"; } $msg = join(" ", @ans); } if ($msg eq "") { Log3 $name, 5, "TCM $name RESPONSE: OK"; } else { Log3 $name, 2, "TCM $name RESPONSE: $msg"; } return $msg; } # Ready sub TCM_Ready($) { my ($hash) = @_; my $ret; if ($hash->{STATE} eq "disconnected") { #if($hash->{STATE} ne "opened") { $ret = DevIo_OpenDev($hash, 1, undef); return $ret if (defined $ret); TCM_InitSerialCom($hash) if (DevIo_IsOpen($hash)); return $ret; } # This is relevant for windows/USB only my $po = $hash->{USBDev}; return undef if(!$po); my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; return ($InBytes>0); } # Get commands TCM 120 my %gets120 = ( "sensitivity" => "AB48", "baseID" => "AB58", "modem_status" => "AB68", "version" => "AB4B", ); # Get commands TCM 310 my %gets310 = ( "baseID" => {packetType => 5, cmd => "08", BaseID => "1,4", RemainingWriteCycles => "5,1"}, "dutycycleLimit" => {packetType => 5, cmd => "23", DutyCycle => "1,1", Slots => "2,1", SlotPeriod => "3,2", ActualSlotLeft => "5,2", LoadAfterActual => "7,1"}, "filter" => {packetType => 5, cmd => "0F", "Type:Value" => "1,0,1:4"}, "frequencyInfo" => {packetType => 5, cmd => "25", Frequency => "1,1", Protocol => "2,1"}, "noiseThreshold" => {packetType => 5, cmd => "33", NoiseThreshold => "1,1"}, "numSecureDevicesIn" => {packetType => 5, cmd => "1D", oCmd => "00", Number => "1,1"}, "numSecureDevicesOut" => {packetType => 5, cmd => "1D",oCmd => "01", Number => "1,1"}, "remanRepeating" => {packetType => 5, cmd => "31", Repeated => "1,1"}, "repeater" => {packetType => 5, cmd => "0A", RepEnable => "1,1", RepLevel => "2,1"}, "stepCode" => {packetType => 5, cmd => "27", HWRevision => "1,1", Stepcode => "2,1"}, "smartAckLearnMode" => {packetType => 6, cmd => "02", Enable => "1,1", Extended => "2,1"}, "smartAckLearnedClients" => {packetType => 6, cmd => "06", "ClientID:CtrlID:Mailbox" => "1,0,4:4:1"}, "version" => {packetType => 5, cmd => "03", APPVersion => "1,4", APIVersion => "5,4", ChipID => "9,4", ChipVersion => "13,4", Desc => "17,16,STR"}, ); # Get sub TCM_Get($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; return if (AttrVal($name, "comType", "TCM") eq "RS485" || $hash->{DeviceName} eq "none"); return "\"get $name\" needs one parameter" if(@a != 2); my $cmd = $a[1]; my ($err, $msg, $packetType); if($hash->{MODEL} eq "ESP2") { # TCM 120 my $rawcmd = $gets120{$cmd}; return "Unknown argument $cmd, choose one of " . join(':noArg ', sort keys %gets120) . ':noArg' if(!defined($rawcmd)); Log3 $name, 3, "TCM $name get $cmd"; $rawcmd .= "000000000000000000"; TCM_Write($hash, $hash, "", $rawcmd); ($err, $msg) = TCM_ReadAnswer($hash, "get $cmd"); $msg = TCM_Parse120($hash, $msg, 1) if(!$err); } else { # TCM 310 my $cmdhash = $gets310{$cmd}; if (!defined($cmdhash) && $cmd !~ m/^getFreeID|getUsedID$/) { return "Unknown argument $cmd, choose one of getFreeID:noArg getUsedID:noArg " . join(':noArg ', sort keys %gets310) . ':noArg'; } Log3 $name, 3, "TCM $name get $cmd"; if ($cmd eq 'getFreeID') { $msg = substr(EnOcean_CheckSenderID('getFreeID', $name, 8 x '0'), 1); } elsif ($cmd eq 'getUsedID') { $msg = EnOcean_CheckSenderID('getUsedID', $name, 8 x '0'); } else { my $cmdHex = $cmdhash->{cmd}; my $oCmdHex = ''; $oCmdHex = $cmdhash->{oCmd} if (exists $cmdhash->{oCmd}); $hash->{helper}{SetAwaitCmdResp} = 1; TCM_Write($hash, $hash, sprintf("%04X%02X%02X", length($cmdHex)/2, length($oCmdHex)/2, $cmdhash->{packetType}), $cmdHex . $oCmdHex); ($err, $msg) = TCM_ReadAnswer($hash, "get $cmd"); $msg = TCM_Parse310($hash, $msg, $cmdhash) if(!$err); } } if($err) { Log3 $name, 2, "TCM $name $err"; return $err; } readingsSingleUpdate($hash, $cmd, $msg, 1); return $msg; } # Set commands TCM 120 my %sets120 = ( # Name, Data to send to the CUL, Regexp for the answer "teach" => {cmd => "AB18", arg => "\\d+"}, "baseID" => {cmd => "AB18", arg => "FF[8-9A-F][0-9A-F]{5}"}, "sensitivity" => {cmd => "AB08", arg => "0[01]"}, "sleep" => {cmd => "AB09"}, "wake" => {cmd => ""}, # Special "reset" => {cmd => "AB0A"}, "modem_on" => {cmd => "AB28", arg => "[0-9A-F]{4}"}, "modem_off" => {cmd => "AB2A"}, ); # Set commands TCM 310 my %sets310 = ( "baseID" => {packetType => 5, cmd => "07", arg => "FF[8-9A-F][0-9A-F]{5}"}, "baudrate" => {packetType => 5, cmd => "24", arg => "0[0-3]"}, "bist" => {packetType => 5, cmd => "06", BIST_Result => "1,1"}, "filterAdd" => {packetType => 5, cmd => "0B", arg => "0[0-3][0-9A-F]{8}[048C]0"}, "filterDel" => {packetType => 5, cmd => "0C", arg => "0[0-3][0-9A-F]{8}"}, "filterDelAll" => {packetType => 5, cmd => "0D"}, "filterEnable" => {packetType => 5, cmd => "0E", arg => "0[01]0[0189]"}, "init" => {}, "maturity" => {packetType => 5, cmd => "10", arg => "0[0-1]"}, "mode" => {packetType => 5, cmd => "1C", arg => "0[0-1]"}, "noiseThreshold" => {packetType => 5, cmd => "32", arg => "2E|2F|3[0-8]"}, "remanCode" => {packetType => 5, cmd => "2E", arg => "[0-9A-F]{8}"}, "remanRepeating" => {packetType => 5, cmd => "30", arg => "0[0-1]"}, "reset" => {packetType => 5, cmd => "02"}, "resetEvents" => {}, "repeater" => {packetType => 5, cmd => "09", arg => "0[0-1]0[0-2]"}, "sleep" => {packetType => 5, cmd => "01", arg => "00[0-9A-F]{6}"}, "smartAckLearn" => {packetType => 6, cmd => "01", arg => "\\d+"}, "smartAckMailboxMax" => {packetType => 6, cmd => "08", arg => "\\d+"}, "startupDelay" => {packetType => 5, cmd => "2F", arg => "[0-9A-F]{2}"}, "subtel" => {packetType => 5, cmd => "11", arg => "0[0-1]"}, "teach" => {packetType => 1, arg => "\\d+"}, ); # clear teach in flag sub TCM_ClearTeach($) { my $hash = shift; foreach my $iName (keys %defs) { delete $defs{$iName}{Teach} if ($defs{$iName}{TYPE} eq 'TCM'); } delete($modules{"$hash->{TYPE}"}{Teach}); Log3 $hash->{NAME}, 3, "TCM $hash->{NAME} set teach 0"; if($hash->{MODEL} ne 'ESP2') { # signal telegram learn mode status my $cmdhex = 'D011B' . 'F' x 17 . '0' x 10; my ($err, $msg); $hash->{helper}{SetAwaitCmdResp} = 1; TCM_Write($hash, $hash, sprintf("%04X00%02X", length($cmdhex)/2, 1), $cmdhex); ($err, $msg) = TCM_ReadAnswer($hash, "set teach"); if(!$err) { $msg = TCM_Parse310($hash, $msg, $sets310{'teach'}); } } return; } # clear Smart ACK teach in flag sub TCM_ClearSmartAckLearn($) { my $hash = shift; delete($hash->{SmartAckLearn}); delete($modules{"$hash->{TYPE}"}{SmartAckLearn}); readingsSingleUpdate($hash, "smartAckLearnMode", "Enable: 00 Extended: 00", 1); } # Set sub TCM_Set($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; return if (AttrVal($name, "comType", "TCM") eq "RS485" || $hash->{DeviceName} eq "none"); return "\"set $name\" needs at least one parameter" if(@a < 2); my $cmd = $a[1]; my $arg = $a[2]; my ($err, $msg); my $chash = ($hash->{MODEL} eq "ESP2" ? \%sets120 : \%sets310); my $cmdhash = $chash->{$cmd}; return "Unknown argument $cmd, choose one of ".join(" ",sort keys %{$chash}) if(!defined($cmdhash)); my $cmdHex = $cmdhash->{cmd}; my $argre = $cmdhash->{arg}; my $logArg = defined($arg) ? $arg : ''; if($argre) { return "Argument needed for set $name $cmd ($argre)" if (!defined($arg)); return "Argument does not match the regexp ($argre)" if ($arg !~ m/$argre/i); if ($cmd eq "smartAckLearn") { if (($arg + 0) >= 0 && ($arg + 0) <= 4294967) { if ($arg == 0) { $arg = '0' x 12; readingsSingleUpdate($hash, "smartAckLearnMode", "Enable: 00 Extended: 00", 1); } else { my $smartAckLearnMode = AttrVal($name, "smartAckLearnMode", "simple"); my %smartAckLearnMode = (simple => 0, advance => 1, advanceSelectRep => 2); $arg = sprintf "01%02X%08X", $smartAckLearnMode{$smartAckLearnMode}, $arg * 1000; readingsSingleUpdate($hash, "smartAckLearnMode", "Enable: 01 Extended: " . sprintf("%02X", $smartAckLearnMode{$smartAckLearnMode}), 1); } } else { return "Argument wrong, choose 0...4294967"; } } elsif ($cmd eq "smartAckMailboxMax") { if (($arg + 0) >= 0 && ($arg + 0) <= 20) { $attr{$name}{smartAckMailboxMax} = $arg; $arg = sprintf "%02X", $arg; } else { return "Argument wrong, choose 0...20"; } } $cmdHex .= $arg; } Log3 $name, 3, "TCM $name set $cmd $logArg"; if($cmd eq "teach") { if ($arg == 0) { RemoveInternalTimer($hash, "TCM_ClearTeach"); while (my ($iDev, $iHash) = each (%{$modules{"$hash->{TYPE}"}{devHash}})) { #Log3 $name, 3, "TCM $name clear Teach flag ioDev: $iDev ioHash: $iHash ioDevName: " . $defs{"$iHash->{NAME}"}->{NAME}; delete $defs{"$iHash->{NAME}"}->{Teach}; } delete $modules{"$hash->{TYPE}"}{Teach}; return if ($hash->{MODEL} eq "ESP2"); # signal telegram learn mode status $cmdHex = 'D011B' . 'F' x 17 . '0' x 10; } else { while (my ($iDev, $iHash) = each (%{$modules{"$hash->{TYPE}"}{devHash}})) { #Log3 $name, 2, "TCM $name clear Teach flag ioDev: $iDev ioHash: $iHash ioDevName: " . $defs{"$iHash->{NAME}"}->{NAME}; delete $defs{"$iHash->{NAME}"}->{Teach}; } $hash->{Teach} = 1; $modules{"$hash->{TYPE}"}{Teach} = $hash; #Log3 $name, 2, "TCM $name set Teach flag ioHash: " . $modules{"$hash->{TYPE}"}{Teach}; RemoveInternalTimer($hash, "TCM_ClearTeach"); InternalTimer(gettimeofday() + $arg, "TCM_ClearTeach", $hash, 1); return if ($hash->{MODEL} eq "ESP2"); # signal telegram learn mode status my $remainTime = $arg < 10 ? 1 : int($arg / 10); $remainTime = $remainTime > 254 ? $remainTime : 254; $cmdHex = 'D0114F' . sprintf("%02X", $remainTime) . 'F' x 14 . '0' x 10; } } if($hash->{MODEL} eq "ESP2") { # TCM 120 if($cmdHex eq "") { # wake is very special DevIo_SimpleWrite($hash, "AA", 1); return ""; } $cmdHex .= "0" x (22 - length($cmdHex)); # Padding with 0 TCM_Write($hash, $hash, "", $cmdHex); ($err, $msg) = TCM_ReadAnswer($hash, "get $cmd"); $msg = TCM_Parse120($hash, $msg, 1) if(!$err); } else { # TCM310 if($cmd eq "init") { TCM_InitSerialCom($hash); return; } if($cmd eq "resetEvents") { delete $hash->{RESET_CAUSE}; delete $hash->{SECURE_MODE}; delete $hash->{DUTYCYCLE_LIMIT}; delete $hash->{TRANSMIT_FAILED}; return; } $hash->{helper}{SetAwaitCmdResp} = 1; TCM_Write($hash, $hash, sprintf("%04X00%02X", length($cmdHex)/2, $cmdhash->{packetType}), $cmdHex); ($err, $msg) = TCM_ReadAnswer($hash, "set $cmd"); if(!$err) { $msg = TCM_Parse310($hash, $msg, $cmdhash); if ($cmd eq "smartAckLearn") { if (substr($arg, 0, 2) eq '00') { # end Smart ACK learnmode RemoveInternalTimer($hash, "TCM_ClearSmartAckLearn"); delete $hash->{SmartAckLearn}; delete $modules{"$hash->{TYPE}"}{SmartAckLearn}; } else { RemoveInternalTimer($hash, "TCM_ClearSmartAckLearn"); InternalTimer(gettimeofday() + hex(substr($arg, 4, 8)) * 0.001, "TCM_ClearSmartAckLearn", $hash, 1); $hash->{SmartAckLearn} = 1; $modules{"$hash->{TYPE}"}{SmartAckLearn} = $hash; Log3 $name, 3, "TCM $name set SmartAckLearn flag ioHash: " . $modules{"$hash->{TYPE}"}{SmartAckLearn}; } } } } if($err) { Log3 $name, 2, "TCM $name $err"; return $err; } my @setCmdReadingsUpdate = ("repeater", "maturity", "mode"); foreach(@setCmdReadingsUpdate) { if ($_ eq $cmd && $msg eq "") { if ($_ eq "repeater") { $arg = "RepEnable: " . substr($arg, 0, 2) . " RepLevel: " . substr($arg, 2, 2); } readingsSingleUpdate($hash, $cmd, $arg, 1); } } return $msg; } # read command response data sub TCM_ReadAnswer($$) { my ($hash, $arg) = @_; return ("No FD", undef) if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); my $name = $hash->{NAME}; my $blockSenderID = AttrVal($name, "blockSenderID", "own"); my ($data, $rin, $buf) = ($hash->{PARTIAL}, "", ""); # 2 seconds timeout my $to = 2; for (;;) { if ($^O =~ m/Win/ && $hash->{USBDev}) { $hash->{USBDev}->read_const_time($to*1000); # set timeout (ms) # Read anstatt input sonst funzt read_const_time nicht. $buf = $hash->{USBDev}->read(999); if (length($buf) == 0) { if (length($data) == 0) { shift(@{$hash->{helper}{awaitCmdResp}}); return ("Timeout reading answer for $arg", undef); } } else { $data .= uc(unpack('H*', $buf)); } } else { if (!$hash->{FD}) { shift(@{$hash->{helper}{awaitCmdResp}}); return ("Device lost when reading answer for $arg", undef); } vec($rin, $hash->{FD}, 1) = 1; my $nfound = select($rin, undef, undef, $to); if ($nfound < 0) { next if ($! == EAGAIN() || $! == EINTR() || $! == 0); my $err = $!; DevIo_Disconnected($hash); shift(@{$hash->{helper}{awaitCmdResp}}); return("Device error: $err", undef); } elsif ($nfound == 0) { if (length($data) == 0) { shift(@{$hash->{helper}{awaitCmdResp}}); return ("Timeout reading response for $arg", undef); } } else { $buf = DevIo_SimpleRead($hash); if(!defined($buf)) { shift(@{$hash->{helper}{awaitCmdResp}}); return ("No response data for $arg", undef); } $data .= uc(unpack('H*', $buf)); } } if (length($data) > 4) { Log3 $name, 5, "TCM $name received ESP: $data"; if ($hash->{MODEL} eq "ESP2") { # TCM 120 if (length($data) >= 28) { if ($data !~ m/^A55A(.B.{20})(..)/) { $hash->{PARTIAL} = ''; return ("$arg: Bogus answer received: $data", undef); } my ($net, $crc) = ($1, $2); my $mycrc = TCM_CSUM($net); $hash->{PARTIAL} = substr($data, 28); if ($crc ne $mycrc) { return ("wrong data checksum: got $crc, computed $mycrc", undef); } return (undef, $net); } } else { # TCM 310 if($data !~ m/^55/) { $data =~ s/.*55/55/; if ($data !~ m/^55/) { #$data = ''; $hash->{PARTIAL} = ''; shift(@{$hash->{helper}{awaitCmdResp}}); return ("$arg: Bogus answer received: $data", undef); } $hash->{PARTIAL} = $data; } next if ($data !~ m/^55(....)(..)(..)(..)/); my ($ldata, $lodata, $packetType, $crc) = (hex($1), hex($2), hex($3), $4); my $tlen = 2 * (7 + $ldata + $lodata); # data telegram incomplete next if (length($data) < $tlen); my $rest = substr($data, $tlen); $data = substr($data, 0, $tlen); my $hdr = substr($data, 2, 8); my $mdata = substr($data, 12, $ldata * 2); my $odata = substr($data, 12 + $ldata * 2, $lodata * 2); my $mycrc = TCM_CRC8($hdr); if ($crc ne $mycrc) { $hash->{PARTIAL} = $rest; shift(@{$hash->{helper}{awaitCmdResp}}); return ("wrong header checksum: got $crc, computed $mycrc", undef); } $mycrc = TCM_CRC8($mdata . $odata); $crc = substr($data, -2); if ($crc ne $mycrc) { $hash->{PARTIAL} = $rest; shift(@{$hash->{helper}{awaitCmdResp}}); return ("wrong data checksum: got $crc, computed $mycrc", undef); } if ($packetType == 1) { # packet type RADIO $mdata =~ m/^(..)(.*)(........)(..)$/; my ($org, $d1, $id, $status) = ($1, $2, $3, $4); my $repeatingCounter = hex substr($status, 1, 1); $odata =~ m/^(..)(........)(..)(..)$/; my ($RSSI, $receivingQuality) = (hex($3), "excellent"); if ($RSSI > 87) { $receivingQuality = "bad"; } elsif ($RSSI > 75) { $receivingQuality = "good"; } my %addvals = ( PacketType => $packetType, SubTelNum => hex($1), DestinationID => $2, RSSI => -$RSSI, ReceivingQuality => $receivingQuality, RepeatingCounter => $repeatingCounter, ); $hash->{RSSI} = -$RSSI; if (!defined TCM_BlockSenderID($hash, $blockSenderID, $id)) { #EnOcean:PacketType:RORG:MessageData:SourceID:Status:OptionalData Dispatch($hash, "EnOcean:$packetType:$org:$d1:$id:$status:$odata", \%addvals) if (exists $hash->{helper}{init_done}); } $data = $rest; $hash->{PARTIAL} = $rest; next; } elsif($packetType == 2) { # packet type RESPONSE $hash->{PARTIAL} = $rest; if (defined $hash->{helper}{awaitCmdResp}[0] && $hash->{helper}{awaitCmdResp}[0]) { shift(@{$hash->{helper}{awaitCmdResp}}); return (undef, $mdata . $odata); } else { shift(@{$hash->{helper}{awaitCmdResp}}); $mdata =~ m/^(..)(.*)$/; my $rc = $1; my %codes = ( "00" => "OK", "01" => "ERROR", "02" => "NOT_SUPPORTED", "03" => "WRONG_PARAM", "04" => "OPERATION_DENIED", "05" => "LOCK_SET", "82" => "FLASH_HW_ERROR", "90" => "BASEID_OUT_OF_RANGE", "91" => "BASEID_MAX_REACHED", ); my $rcTxt = $codes{$rc} if($codes{$rc}); Log3 $name, $rc eq "00" ? 5 : 2, "TCM $name RESPONSE: $rcTxt"; #$packetType = sprintf "%01X", $packetType; #EnOcean:PacketType:ResposeCode:MessageData:OptionalData #Dispatch($hash, "EnOcean:$packetType:$1:$2:$odata", undef); $data = $rest; next; } } elsif ($packetType == 4) { ##### # packet type EVENT $mdata =~ m/^(..)(.*)$/; my $eventCode = $1; my $messageData = $2; if (hex($eventCode) <= 3) { my $packetTypeHex = sprintf "%01X", $packetType; #EnOcean:PacketType:eventCode:messageData Dispatch($hash, "EnOcean:$packetTypeHex:$eventCode:$messageData", undef); } elsif (hex($eventCode) == 4) { # CO_READY my @resetCause = ('voltage_supply_drop', 'reset_pin', 'watchdog', 'flywheel', 'parity_error', 'hw_parity_error', 'memory_request_error', 'wake_up', 'wake_up', 'unknown'); my @secureMode = ('standard', 'extended'); $hash->{RESET_CAUSE} = $resetCause[hex($messageData)]; $hash->{SECURE_MODE} = $secureMode[hex($odata)]; Log3 $name, 2, "TCM $name EVENT RESET_CAUSE: $hash->{RESET_CAUSE} SECURE_MODE: $hash->{SECURE_MODE}"; } elsif (hex($eventCode) == 5) { # CO_EVENT_SECUREDEVICES } elsif (hex($eventCode) == 6) { # CO_DUTYCYCLE_LIMIT my @dutycycleLimit = ('released', 'reached'); $hash->{DUTYCYCLE_LIMIT} = $dutycycleLimit[hex($messageData)]; Log3 $name, 2, "TCM $name EVENT DUTYCYCLE_LIMIT: $hash->{DUTYCYCLE_LIMIT}"; } elsif (hex($eventCode) == 7) { # CO_TRANSMIT_FAILED my @transmitFailed = ('CSMA_failed', 'no_ack_received'); $hash->{TRANSMIT_FAILED} = $transmitFailed[hex($messageData)]; Log3 $name, 2, "TCM $name EVENT TRANSMIT_FAILED: $hash->{TRANSMIT_FAILED}"; } $data = $rest; $hash->{PARTIAL} = $rest; next; } else { return ("$arg ERROR: received data telegram PacketType: $packetType Data: $mdata not supported.", undef) } } } } } sub TCM_BlockSenderID($$$) { my ($hash, $blockSenderID, $senderID) = @_; return undef if ($blockSenderID eq 'no'); return undef if (!exists $modules{"$hash->{TYPE}"}{BaseID}); foreach (@{$modules{"$hash->{TYPE}"}{BaseID}}) { if (hex($_) == (hex($senderID) & 0xFFFFFF80)) { Log3 $hash->{NAME}, 4, "TCM $hash->{NAME} received own telegram from SenderID $senderID blocked."; return 1; } } return undef if (!exists $modules{"$hash->{TYPE}"}{ChipID}); foreach (@{$modules{"$hash->{TYPE}"}{ChipID}}) { if (hex($_) == hex($senderID)) { Log3 $hash->{NAME}, 4, "TCM $hash->{NAME} received own telegram from $senderID blocked."; return 1; } } return undef; } # sub TCM_msgCounter($) { my $hash = shift(@_); my $timeNow = time(); my ($count, $countPerDay, $countPerHour, $countPerMin); RemoveInternalTimer($hash, 'TCM_msgCounter'); if (AttrVal($hash->{NAME}, 'msgCounter', 'off') eq 'off') { delete $hash->{MsgRcvPerDay}; delete $hash->{MsgRcvPerHour}; delete $hash->{MsgRcvPerMin}; delete $hash->{MsgSndPerDay}; delete $hash->{MsgSndPerHour}; delete $hash->{MsgSndPerMin}; return undef; } # receive counter if (exists $hash->{helper}{rcvCounter}) { ($count, $countPerDay, $countPerHour, $countPerMin) = (0, 0, 0, 0); foreach my $timestamp (@{$hash->{helper}{rcvCounter}}) { $countPerDay = $count if ($timestamp < $timeNow - 86400); $countPerHour = $count if ($timestamp < $timeNow - 3600); $countPerMin = $count if ($timestamp < $timeNow - 60); $count ++; } splice(@{$hash->{helper}{rcvCounter}}, 0, $countPerDay); $hash->{MsgRcvPerDay} = $#{$hash->{helper}{rcvCounter}}; $hash->{MsgRcvPerHour} = $hash->{MsgRcvPerDay} + $countPerDay - $countPerHour; $hash->{MsgRcvPerMin} = $hash->{MsgRcvPerDay} + $countPerDay - $countPerMin; } # send counter if (exists $hash->{helper}{sndCounter}) { ($count, $countPerDay, $countPerHour, $countPerMin) = (0, 0, 0, 0); foreach my $timestamp (@{$hash->{helper}{sndCounter}}) { $countPerDay = $count if ($timestamp < $timeNow - 86400); $countPerHour = $count if ($timestamp < $timeNow - 3600); $countPerMin = $count if ($timestamp < $timeNow - 60); $count ++; } splice(@{$hash->{helper}{sndCounter}}, 0, $countPerDay); $hash->{MsgSndPerDay} = $#{$hash->{helper}{sndCounter}}; $hash->{MsgSndPerHour} = $hash->{MsgSndPerDay} + $countPerDay - $countPerHour; $hash->{MsgSndPerMin} = $hash->{MsgSndPerDay} + $countPerDay - $countPerMin; } InternalTimer(time() + 60, 'TCM_msgCounter', $hash, 0); return undef; } # sub TCM_Attr(@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; # return if attribute list is incomplete return undef if (!$init_done); if ($attrName eq "assignIODev") { if (!defined $attrVal) { if (exists $modules{TCM}{assignIODev}) { delete($modules{TCM}{assignIODev}) if ($modules{TCM}{assignIODev} eq $hash); } } elsif ($attrVal eq 'no') { if (exists $modules{TCM}{assignIODev}) { delete($modules{TCM}{assignIODev}) if ($modules{TCM}{assignIODev} eq $hash); } } elsif ($attrVal eq 'yes') { $modules{TCM}{assignIODev} = $hash; } else { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); if (exists $modules{TCM}{assignIODev}) { delete($modules{TCM}{assignIODev}) if ($modules{TCM}{assignIODev} eq $hash); } } } elsif ($attrName eq "baseID") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^[Ff]{2}[\dA-Fa-f]{4}[08]0$/ || $attrVal =~ m/^[Ff]{8}$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } else { $hash->{BaseID} = $attrVal; $hash->{LastID} = sprintf "%08X", (hex $attrVal) + 127; } } elsif ($attrName =~ m/^\.(base|chip)IDSaved$/) { if (!defined $attrVal){ } elsif ($attrVal !~ m/^[Ff]{2}[\dA-Fa-f]{6}$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "blockSenderID") { if (!defined $attrVal) { } elsif ($attrVal !~ m/^own|no$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "comModeUTE") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^auto|biDir|uniDir$/) { Log3 $name, 2, "EnOcean $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "comType") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^TCM|RS485$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "fingerprint") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^off|on$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "learningDev") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^all|teachMsg$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "learningMode") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^always|demand|nearfield$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "msgCounter") { if (!defined $attrVal){ RemoveInternalTimer($hash, 'TCM_msgCounter'); } elsif ($attrVal eq 'off') { RemoveInternalTimer($hash, 'TCM_msgCounter'); } elsif ($attrVal eq 'on') { RemoveInternalTimer($hash, 'TCM_msgCounter'); InternalTimer(time() + 60, 'TCM_msgCounter', $hash, 0); } else { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "rcvIDShift") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^[\dA-Fa-f]{6}[08]0$/ || $attrVal =~ m/^[Ff]{8}$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "sendInterval") { if (!defined $attrVal){ } elsif (($attrVal + 0) < 0 || ($attrVal + 0) > 250) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong or out of range"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "smartAckLearnMode") { if (!defined $attrVal){ } elsif ($attrVal !~ m/^simple|advance|advanceSelectRep$/) { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong"; CommandDeleteAttr(undef, "$name $attrName"); } } elsif ($attrName eq "smartAckMailboxMax") { if (!defined $attrVal){ } elsif (($attrVal + 0) >= 0 && ($attrVal + 0) <= 20) { TCM_Set($hash, ("set", "smartAckMailboxMax", $attrVal)); } else { Log3 $name, 2, "TCM $name attribute-value [$attrName] = $attrVal wrong or out of range"; CommandDeleteAttr(undef, "$name $attrName"); } } return undef; } # sub TCM_Notify(@) { my ($hash, $dev) = @_; if ($dev->{TYPE} eq 'Global' && grep (m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})){ #if ($dev->{NAME} eq "global" && grep (m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})){ RemoveInternalTimer($hash, 'TCM_msgCounter'); InternalTimer(time() + 60, 'TCM_msgCounter', $hash, 0) if (AttrVal($hash->{NAME}, 'msgCounter', 'off') eq 'on'); TCM_InitSerialCom($hash); my $assignIODevFlag = AttrVal($hash->{NAME}, 'assignIODev', undef); if (defined $assignIODevFlag) { if ($assignIODevFlag eq 'yes') { $modules{TCM}{assignIODev} = $hash; } else { if (exists $modules{TCM}{assignIODev}) { delete($modules{TCM}{assignIODev}) if ($modules{TCM}{assignIODev} eq $hash); } } } if (exists $modules{$modulesType}{ChipID}) { if (@{$modules{$modulesType}{ChipID}} <= 1) { # one transmitter currently registered #$attr{$hash->{NAME}}{fingerprint} = 'off'; #Log3 $hash->{NAME}, 2, "TCM $hash->{NAME} Atribute fingerprint: off"; } else { # more then one transmitter currently registered if (!exists $attr{$dev->{NAME}}{dupTimeout}) { $attr{$dev->{NAME}}{dupTimeout} = $dupTimeout; Log3 $dev->{NAME}, 2, "$dev->{TYPE} $dev->{NAME} Attribute dupTimeout: $attr{$dev->{NAME}}{dupTimeout}"; } while (my ($iDev, $iHash) = each (%{$modules{$modulesType}{devHash}})) { if (!exists $attr{$iDev}{fingerprint}) { $attr{$iDev}{fingerprint} = 'on'; Log3 $iDev, 2, "TCM $iDev Attribute fingerprint: $attr{$iDev}{fingerprint}"; } } } if (exists($modules{"$hash->{TYPE}"}{BaseID}) && exists($modules{"$hash->{TYPE}"}{ChipID})) { Log3 $hash->{NAME}, 2, "TCM registered transceiver BaseID: " . join(':', @{$modules{"$hash->{TYPE}"}{BaseID}}) . " ChipID: " . join(':', @{$modules{"$hash->{TYPE}"}{ChipID}}); } } } return undef; } # Undef sub TCM_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); Log3 $name, $lev, "TCM $name deleting port for $d"; delete $defs{$d}{IODev}; } } DevIo_CloseDev($hash); if (exists $modules{"$hash->{TYPE}"}{BaseID}) { for (my $i = 0; $i <= @{$modules{"$hash->{TYPE}"}{BaseID}}; $i++) { if (${$modules{"$hash->{TYPE}"}{BaseID}}[$i] eq $hash->{BaseID}) { Log3 $name, 4, "TCM $name remove module BaseID: " . ${$modules{"$hash->{TYPE}"}{BaseID}}[$i]; splice(@{$modules{"$hash->{TYPE}"}{BaseID}}, $i, 1); last; } } } if (exists $modules{"$hash->{TYPE}"}{ChipID}) { for (my $i = 0; $i <= @{$modules{"$hash->{TYPE}"}{ChipID}}; $i++) { if (${$modules{"$hash->{TYPE}"}{ChipID}}[$i] eq $hash->{ChipID}) { Log3 $name, 4, "TCM $name remove module ChipID: " . ${$modules{"$hash->{TYPE}"}{ChipID}}[$i]; splice(@{$modules{"$hash->{TYPE}"}{ChipID}}, $i, 1); last; } } } RemoveInternalTimer($hash, 'TCM_msgCounter'); delete $hash->{helper}{init_done}; return undef; } # Shutdown sub TCM_Shutdown($) { my ($hash) = @_; DevIo_CloseDev($hash); return undef; } 1; =pod =item summary EnOcean Serial Protocol Inferface (ESP2/ESP3) =item summary_DE EnOcean Serial Protocol Interface (ESP2/ESP3) =begin html

TCM

=end html =cut