############################################## # $Id$ # noansi: modified for better timing compatibility with CUL # # HMUARTLGW provides support for the eQ-3 HomeMatic Wireless LAN Gateway # (HM-LGW-O-TW-W-EU) and the eQ-3 HomeMatic UART module (HM-MOD-UART), which # is part of the HomeMatic wireless module for the Raspberry Pi # (HM-MOD-RPI-PCB). # # TODO: # - Filter out "A112" from CUL_HM and synthesize response package main; use strict; use warnings; use DevIo; use Digest::MD5; use Time::HiRes qw(gettimeofday time); use Time::Local; eval "use Crypt::Rijndael"; my $cryptFunc = ($@)?0:1; use constant { HMUARTLGW_OS_GET_APP => "00", HMUARTLGW_OS_GET_FIRMWARE => "02", HMUARTLGW_OS_CHANGE_APP => "03", HMUARTLGW_OS_ACK => "04", HMUARTLGW_OS_UPDATE_FIRMWARE => "05", HMUARTLGW_OS_UNSOL_CREDITS => "05", HMUARTLGW_OS_NORMAL_MODE => "06", HMUARTLGW_OS_UPDATE_MODE => "07", HMUARTLGW_OS_GET_CREDITS => "08", HMUARTLGW_OS_ENABLE_CREDITS => "09", HMUARTLGW_OS_ENABLE_CSMACA => "0A", HMUARTLGW_OS_GET_SERIAL => "0B", HMUARTLGW_OS_SET_TIME => "0E", HMUARTLGW_APP_SET_HMID => "00", HMUARTLGW_APP_GET_HMID => "01", HMUARTLGW_APP_SEND => "02", HMUARTLGW_APP_SET_CURRENT_KEY => "03", #key index, 00x17 when no key HMUARTLGW_APP_ACK => "04", HMUARTLGW_APP_RECV => "05", HMUARTLGW_APP_ADD_PEER => "06", HMUARTLGW_APP_REMOVE_PEER => "07", HMUARTLGW_APP_GET_PEERS => "08", HMUARTLGW_APP_PEER_ADD_AES => "09", HMUARTLGW_APP_PEER_REMOVE_AES => "0A", HMUARTLGW_APP_SET_TEMP_KEY => "0B", #key index, 00x17 when no key HMUARTLGW_APP_SET_PREVIOUS_KEY => "0F", #key index, 00x17 when no key HMUARTLGW_APP_DEFAULT_HMID => "10", HMUARTLGW_DUAL_GET_APP => "01", HMUARTLGW_DUAL_CHANGE_APP => "02", HMUARTLGW_ACK_NACK => "00", HMUARTLGW_ACK => "01", HMUARTLGW_ACK_INFO => "02", HMUARTLGW_ACK_WITH_RESPONSE => "03", HMUARTLGW_ACK_EUNKNOWN => "04", HMUARTLGW_ACK_ENOCREDITS => "05", HMUARTLGW_ACK_ECSMACA => "06", HMUARTLGW_ACK_WITH_MULTIPART_DATA => "07", #04 07 XX YY: part XX of YY HMUARTLGW_ACK_EINPROGRESS => "08", HMUARTLGW_ACK_WITH_RESPONSE_AES_OK => "0C", HMUARTLGW_ACK_WITH_RESPONSE_AES_KO => "0D", HMUARTLGW_RECV_RESP => "01", HMUARTLGW_RECV_RESP_WITH_AES_OK => "02", HMUARTLGW_RECV_RESP_WITH_AES_KO => "03", HMUARTLGW_RECV_TRIG => "11", HMUARTLGW_RECV_TRIG_WITH_AES_OK => "12", HMUARTLGW_DST_OS => 0, HMUARTLGW_DST_APP => 1, HMUARTLGW_DST_DUAL => 254, HMUARTLGW_DST_DUAL_ERR => 255, HMUARTLGW_STATE_NONE => 0, HMUARTLGW_STATE_QUERY_APP => 1, HMUARTLGW_STATE_ENTER_APP => 2, HMUARTLGW_STATE_GETSET_PARAMETERS => 3, HMUARTLGW_STATE_SET_HMID => 4, HMUARTLGW_STATE_GET_HMID => 5, HMUARTLGW_STATE_GET_DEFAULT_HMID => 6, HMUARTLGW_STATE_SET_TIME => 7, HMUARTLGW_STATE_GET_FIRMWARE => 8, HMUARTLGW_STATE_GET_SERIAL => 9, HMUARTLGW_STATE_SET_NORMAL_MODE => 10, HMUARTLGW_STATE_ENABLE_CSMACA => 11, HMUARTLGW_STATE_ENABLE_CREDITS => 12, HMUARTLGW_STATE_GET_INIT_CREDITS => 13, HMUARTLGW_STATE_SET_CURRENT_KEY => 14, HMUARTLGW_STATE_SET_PREVIOUS_KEY => 15, HMUARTLGW_STATE_SET_TEMP_KEY => 16, HMUARTLGW_STATE_UPDATE_PEER => 90, HMUARTLGW_STATE_UPDATE_PEER_AES1 => 91, HMUARTLGW_STATE_UPDATE_PEER_AES2 => 92, HMUARTLGW_STATE_UPDATE_PEER_CFG => 93, HMUARTLGW_STATE_SET_UPDATE_MODE => 95, HMUARTLGW_STATE_KEEPALIVE_INIT => 96, HMUARTLGW_STATE_KEEPALIVE_SENT => 97, HMUARTLGW_STATE_GET_CREDITS => 98, HMUARTLGW_STATE_RUNNING => 99, HMUARTLGW_STATE_SEND => 100, HMUARTLGW_STATE_SEND_NOACK => 101, HMUARTLGW_STATE_SEND_TIMED => 102, HMUARTLGW_STATE_UPDATE_COPRO => 200, HMUARTLGW_STATE_UNSUPPORTED_FW => 999, HMUARTLGW_CMD_TIMEOUT => 3, HMUARTLGW_CMD_RETRY_CNT => 3, HMUARTLGW_FIRMWARE_TIMEOUT => 10, HMUARTLGW_SEND_TIMEOUT => 10, HMUARTLGW_SEND_RETRY_SECONDS => 3, HMUARTLGW_BUSY_RETRY_MS => 50, HMUARTLGW_CSMACA_RETRY_MS => 200, HMUARTLGW_KEEPALIVE_SECONDS => 10, HMUARTLGW_KEEPALIVE_WARN_LATE_S => 4, }; my %sets = ( "hmPairForSec" => "", "hmPairSerial" => "", "reopen" => "noArg", "open" => "noArg", "close" => "noArg", "restart" => "noArg", "updateCoPro" => "", ); my %gets = ( "assignIDs" => "noArg", ); sub HMUARTLGW_Initialize($) { my ($hash) = @_; $hash->{ReadyFn} = "HMUARTLGW_Ready"; $hash->{ReadFn} = "HMUARTLGW_Read"; $hash->{WriteFn} = "HMUARTLGW_Write"; $hash->{DefFn} = "HMUARTLGW_Define"; $hash->{UndefFn} = "HMUARTLGW_Undefine"; $hash->{SetFn} = "HMUARTLGW_Set"; $hash->{GetFn} = "HMUARTLGW_Get"; $hash->{AttrFn} = "HMUARTLGW_Attr"; $hash->{RenameFn} = "HMUARTLGW_Rename"; $hash->{ShutdownFn}= "HMUARTLGW_Shutdown"; $hash->{NotifyFn} = "HMUARTLGW_Notify"; $hash->{NotifyOrderPrefix} = "47-"; #make sure, HMUARTLGW_InitConnection is called once prior to CUL_HM initialisation $hash->{AttrList}= "hmId " . "lgwPw " . "hmKey hmKey2 hmKey3 " . "dutyCycle:1,0 " . "csmaCa:0,1 " . "qLen " . "logIDs ". "dummy:1 ". "loadEvents:0,1 ". $readingFnAttributes; } sub HMUARTLGW_InitConnection($); sub HMUARTLGW_Connect($$); sub HMUARTLGW_Reopen($;$); sub HMUARTLGW_Dummy($); sub HMUARTLGW_SendPendingCmd($); sub HMUARTLGW_SendCmd($$); sub HMUARTLGW_GetSetParameterReq($); sub HMUARTLGW_getAesKeys($); sub HMUARTLGW_updateMsgLoad($$); sub HMUARTLGW_Read($); sub HMUARTLGW_RemoveHMPair($); sub HMUARTLGW_send($$$;$); sub HMUARTLGW_send_frame($$); sub HMUARTLGW_crc16($;$); sub HMUARTLGW_encrypt($$); sub HMUARTLGW_decrypt($$); sub HMUARTLGW_getVerbLvl($$$$); sub HMUARTLGW_firmwareGetBlock($$$); sub HMUARTLGW_updateCoPro($$); sub HMUARTLGW_DoInit($) { my ($hash) = @_; my $name = $hash->{NAME}; $hash->{CNT} = 0x00; delete($hash->{DEVCNT}); delete($hash->{'.crypto'}); delete($hash->{keepAlive}); delete($hash->{Helper}); delete($hash->{AssignedPeerCnt}); delete($hash->{msgLoadCurrent}); delete($hash->{msgLoadHistory}); delete($hash->{msgLoadHistoryAbs}); delete($hash->{owner}); $hash->{DevState} = HMUARTLGW_STATE_NONE; $hash->{XmitOpen} = 0; $hash->{LastOpen} = gettimeofday(); $hash->{LGW_Init} = 1 if ($hash->{DevType} =~ m/^LGW/); $hash->{Helper}{Log}{IDs} = [ split(/,/, AttrVal($name, "logIDs", "")) ]; $hash->{Helper}{Log}{Resolve} = 1; RemoveInternalTimer($hash); if ($hash->{DevType} eq "LGW") { my $keepAlive = { NR => $devcount++, NAME => "${name}:keepAlive", STATE => "uninitialized", TYPE => $hash->{TYPE}, TEMPORARY => 1, directReadFn => \&HMUARTLGW_Read, DevType => "LGW-KeepAlive", '.lgwHash' => $hash, }; $attr{$keepAlive->{NAME}}{room} = "hidden"; $attr{$keepAlive->{NAME}}{verbose} = AttrVal($name, "verbose", undef); $defs{$keepAlive->{NAME}} = $keepAlive; DevIo_CloseDev($keepAlive); my ($ip, $port) = split(/:/, $hash->{DeviceName}); $keepAlive->{DeviceName} = "${ip}:" . ($port + 1); DevIo_OpenDev($keepAlive, 0, "HMUARTLGW_DoInit", \&HMUARTLGW_Connect); $hash->{keepAlive} = $keepAlive; } InternalTimer(gettimeofday()+1, "HMUARTLGW_StartInit", $hash, 0); return; } sub HMUARTLGW_Connect($$) { my ($hash, $err) = @_; my $name = $hash->{NAME}; if (defined(AttrVal($name, "dummy", undef))) { HMUARTLGW_Dummy($hash); return; } if ($err) { my $retry; if(defined($hash->{NEXT_OPEN})) { $retry = ", retrying in " . sprintf("%.2f", ($hash->{NEXT_OPEN} - time())) . "s"; } Log3($hash, 3, "HMUARTLGW $hash->{NAME}: ${err}".(defined($retry)?$retry:"")); if (!defined($hash->{NEXT_OPEN})) { Log3($hash, 0, "DevIO giving up on ${err}, retrying anyway"); HMUARTLGW_Reopen($hash); } } } sub HMUARTLGW_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); if (@a != 3) { return "wrong syntax: define HMUARTLGW /path/to/port|hostname"; } my $name = $a[0]; my $dev = $a[2]; HMUARTLGW_Undefine($hash, $name); if ($dev !~ m/\//) { $dev .= ":2000" if ($dev !~ m/:/); $hash->{DevType} = "LGW"; } else { if ($dev =~ m/^uart:\/\/(.*)$/) { $dev = $1; } elsif ($dev !~ m/\@/) { $dev .= "\@115200"; } $hash->{DevType} = "UART"; $hash->{model} = "HM-MOD-UART"; readingsBeginUpdate($hash); delete($hash->{READINGS}{"D-LANfirmware"}); readingsBulkUpdate($hash, "D-type", $hash->{model}); readingsEndUpdate($hash, 1); } $hash->{DeviceName} = $dev; $hash->{Clients} = ":CUL_HM:"; my %ml = ( "1:CUL_HM" => "^A......................" ); $hash->{MatchList} = \%ml; $hash->{NOTIFYDEV} = "global"; HMUARTLGW_InitConnection($hash) if ($init_done); return; } sub HMUARTLGW_InitConnection($) { my ($hash) = @_; my $name = $hash->{NAME}; if (defined(AttrVal($name, "dummy", undef))) { readingsSingleUpdate($hash, "state", "dummy", 1); HMUARTLGW_updateCondition($hash); return; } if (!$init_done) { #handle rereadcfg InternalTimer(gettimeofday()+15, "HMUARTLGW_InitConnection", $hash, 0); return; } DevIo_OpenDev($hash, 0, "HMUARTLGW_DoInit", \&HMUARTLGW_Connect); return; } sub HMUARTLGW_Notify($$) { my ($hash, $source) = @_; my $name = $hash->{NAME}; #We are only interested in events from global concerning the general #system state or this module in particular return if($source->{NAME} ne "global"); #return if (!grep(m/^INITIALIZED|REREADCFG|(MODIFIED $name)|(DEFINED $name)$/, @{$source->{CHANGED}})); if (grep(m/^INITIALIZED|REREADCFG$/, @{$source->{CHANGED}})) { HMUARTLGW_InitConnection($hash); } return; } sub HMUARTLGW_Undefine($$;$) { my ($hash, $name, $noclose) = @_; RemoveInternalTimer($hash); RemoveInternalTimer("HMUARTLGW_CheckCredits:$name"); RemoveInternalTimer("hmPairForSec:$name"); if ($hash->{keepAlive}) { RemoveInternalTimer($hash->{keepAlive}); DevIo_CloseDev($hash->{keepAlive}); delete($attr{$hash->{keepAlive}->{NAME}}); delete($defs{$hash->{keepAlive}->{NAME}}); delete($hash->{keepAlive}); } if (!$noclose) { my $oldFD = $hash->{FD}; DevIo_CloseDev($hash); Log3($hash, 3, "${name} device closed") if (defined($oldFD) && $oldFD && (!defined($hash->{FD}))); } $hash->{DevState} = HMUARTLGW_STATE_NONE; $hash->{XmitOpen} = 0; HMUARTLGW_updateCondition($hash); } sub HMUARTLGW_Reopen($;$) { my ($hash, $noclose) = @_; $hash = $hash->{'.lgwHash'} if ($hash->{'.lgwHash'}); my $name = $hash->{NAME}; Log3($hash, 4, "HMUARTLGW ${name} Reopen"); HMUARTLGW_Undefine($hash, $name, $noclose); return DevIo_OpenDev($hash, 1, "HMUARTLGW_DoInit", \&HMUARTLGW_Connect); } sub HMUARTLGW_Ready($) { my ($hash) = @_; my $name = $hash->{NAME}; my $state = ReadingsVal($name, "state", "unknown"); Log3($hash, 4, "HMUARTLGW ${name} ready: ${state}"); if ((!$hash->{'.lgwHash'}) && $state eq "disconnected") { #don't immediately reconnect when we just connected, delay #for 5s because remote closed the connection on us if (defined($hash->{LastOpen}) && $hash->{LastOpen} + 5 >= gettimeofday()) { return 0; } return HMUARTLGW_Reopen($hash, 1); } return 0; } sub HMUARTLGW_Rename($$) { my ($name, $old_name) = @_; my $hash = $defs{$name}; if (defined($hash->{Helper}{Initialized})) { RemoveInternalTimer("HMUARTLGW_CheckCredits:${old_name}"); InternalTimer(gettimeofday()+1, "HMUARTLGW_CheckCredits", "HMUARTLGW_CheckCredits:${name}", 0); } if ($hash->{hmPair}) { HMUARTLGW_RemoveHMPair("hmPairForSec:${old_name}"); } } sub HMUARTLGW_Shutdown($) { my ($hash) = @_; my $name = $hash->{NAME}; #switch to bootloader to stop the module from interfering HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS) if ($hash->{DevState} > HMUARTLGW_STATE_ENTER_APP); DevIo_CloseDev($hash->{keepAlive}) if ($hash->{keepAlive}); DevIo_CloseDev($hash); return undef; } sub HMUARTLGW_Dummy($) { my ($hash) = @_; my $name = $hash->{NAME}; #switch to bootloader to stop the module from interfering HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS) if ($hash->{DevState} > HMUARTLGW_STATE_ENTER_APP); HMUARTLGW_Undefine($hash, $name); readingsSingleUpdate($hash, "state", "dummy", 1); HMUARTLGW_updateCondition($hash); $hash->{XmitOpen} = 0; return; } #HM-LGW communicates line-based during init sub HMUARTLGW_LGW_Init($) { my ($hash) = @_; my $name = $hash->{NAME}; my $p = pack("H*", $hash->{PARTIAL}); while($p =~ m/\n/) { (my $line, $p) = split(/\n/, $p, 2); $line =~ s/\r$//; Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} read (".length($line)."): ${line}"); my $msg; if ($line =~ m/^H(..),01,([^,]*),([^,]*),([^,]*)$/) { $hash->{DEVCNT} = hex($1); $hash->{CNT} = hex($1); if ($hash->{DevType} eq "LGW") { $hash->{model} = $2; readingsBeginUpdate($hash); readingsBulkUpdate($hash, "D-type", $2); readingsBulkUpdate($hash, "D-serialNr", $4); my $fw = $3; if ($fw =~ m/^(\d+)\.(\d+)\.(\d+)$/) { my $fwver = (int($1) << 16) | (int($2) << 8) | int($3); $fw .= " (outdated)" if ($fwver < 0x010105); } readingsBulkUpdate($hash, "D-LANfirmware", $fw); readingsEndUpdate($hash, 1); } } elsif ($line =~ m/^V(..),(................................)$/) { $hash->{DEVCNT} = hex($1); $hash->{CNT} = hex($1); my $lgwName = $name; $lgwName = $hash->{'.lgwHash'}->{NAME} if ($hash->{'.lgwHash'}); my $lgwPw = AttrVal($lgwName, "lgwPw", undef); if (!$cryptFunc) { Log3($hash, 1, "HMUARTLGW ${name} wants to initiate encrypted communication, but Crypt::Rijndael is not installed."); } elsif (!$lgwPw) { Log3($hash, 1, "HMUARTLGW ${name} wants to initiate encrypted communication, but no lgwPw set!"); } else { my($s,$us) = gettimeofday(); my $myiv = sprintf("%08x%06x%s", ($s & 0xffffffff), ($us & 0xffffff), scalar(reverse(substr($2, 14)))); #FIXME... my $key = Digest::MD5::md5($lgwPw); $hash->{'.crypto'}{cipher} = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_ECB()); $hash->{'.crypto'}{encrypt}{keystream} = ''; $hash->{'.crypto'}{encrypt}{ciphertext} = pack("H*", $2); $hash->{'.crypto'}{decrypt}{keystream} = ''; $hash->{'.crypto'}{decrypt}{ciphertext} = pack("H*", $myiv); $msg = "V%02x,${myiv}\r\n"; } } elsif ($line =~ m/^S(..),([^-]*)-/) { $hash->{DEVCNT} = hex($1); $hash->{CNT} = hex($1); if ($2 eq "BidCoS") { Log3($hash, 3, "HMUARTLGW ${name} BidCoS-port opened"); } elsif ($2 eq "SysCom") { Log3($hash, 3, "HMUARTLGW ${name} KeepAlive-port opened"); } else { Log3($hash, 1, "HMUARTLGW ${name} Unknown port identification received: ${2}, reopening"); HMUARTLGW_Reopen($hash); return; } $msg = ">%02x,0000\r\n"; delete($hash->{LGW_Init}); } HMUARTLGW_sendAscii($hash, $msg) if ($msg); } $hash->{PARTIAL} = unpack("H*", $p); } #LGW KeepAlive sub HMUARTLGW_LGW_HandleKeepAlive($) { my ($hash) = @_; my $name = $hash->{NAME}; my $p = pack("H*", $hash->{PARTIAL}); while($p =~ m/\n/) { (my $line, $p) = split(/\n/, $p, 2); $line =~ s/\r$//; Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} read (".length($line)."): ${line}"); my $msg; if ($line =~ m/^>L(..)/) { $hash->{DEVCNT} = hex($1); RemoveInternalTimer($hash); $hash->{DevState} = HMUARTLGW_STATE_KEEPALIVE_SENT; $msg = "K%02x\r\n"; InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } elsif ($line =~ m/^>K(..)/) { $hash->{DEVCNT} = hex($1); RemoveInternalTimer($hash); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; #now we have 15s $hash->{Helper}{NextKeepAlive} = gettimeofday() + HMUARTLGW_KEEPALIVE_SECONDS; InternalTimer($hash->{Helper}{NextKeepAlive}, "HMUARTLGW_SendKeepAlive", $hash, 0); } HMUARTLGW_sendAscii($hash, $msg) if ($msg); } $hash->{PARTIAL} = unpack("H*", $p); return; } sub HMUARTLGW_SendKeepAlive($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); $hash->{DevState} = HMUARTLGW_STATE_KEEPALIVE_SENT; HMUARTLGW_sendAscii($hash, "K%02x\r\n"); my $diff = gettimeofday() - $hash->{Helper}{NextKeepAlive}; Log3($hash, 1, "HMUARTLGW ${name} KeepAlive sent " . sprintf("%.3f", $diff) . "s too late, this might cause a disconnect!") if ($diff > HMUARTLGW_KEEPALIVE_WARN_LATE_S); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } sub HMUARTLGW_CheckCredits($) { my ($in) = shift; my (undef, $name) = split(':',$in); my $hash = $defs{$name}; my $next = 15; if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING) { Log3($hash, 5, "HMUARTLGW ${name} checking credits (from timer)"); $hash->{Helper}{OneParameterOnly} = 1; if (++$hash->{Helper}{CreditTimer} % (4*60*2)) { #about every 2h $hash->{DevState} = HMUARTLGW_STATE_GET_CREDITS; } else { $hash->{DevState} = HMUARTLGW_STATE_SET_TIME; $next = 1; } HMUARTLGW_GetSetParameterReq($hash); } else { $next = 1; } RemoveInternalTimer("HMUARTLGW_CheckCredits:$name"); InternalTimer(gettimeofday()+$next, "HMUARTLGW_CheckCredits", "HMUARTLGW_CheckCredits:$name", 0); } sub HMUARTLGW_SendPendingCmd($) { my ($hash) = @_; my $name = $hash->{NAME}; if (defined($hash->{XmitOpen}) && $hash->{XmitOpen} == 2) { if ($hash->{Helper}{PendingCMD}) { my $qLen = AttrVal($name, "qLen", 60); if (scalar(@{$hash->{Helper}{PendingCMD}}) < $qLen) { $hash->{XmitOpen} = 1; } } else { $hash->{XmitOpen} = 1; } } if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING && defined($hash->{Helper}{PendingCMD}) && @{$hash->{Helper}{PendingCMD}}) { my $cmd = $hash->{Helper}{PendingCMD}->[0]; if ($cmd->{cmd} eq "AESkeys") { Log3($hash, 5, "HMUARTLGW ${name} setting keys"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_SET_CURRENT_KEY; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "Credits") { Log3($hash, 5, "HMUARTLGW ${name} checking credits (from send)"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_GET_CREDITS; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "HMID") { Log3($hash, 5, "HMUARTLGW ${name} setting hmId"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_SET_HMID; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "DutyCycle") { Log3($hash, 5, "HMUARTLGW ${name} Enabling/Disabling DutyCycle"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_ENABLE_CREDITS; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "CSMACA") { Log3($hash, 5, "HMUARTLGW ${name} Enabling/Disabling CSMA/CA"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_ENABLE_CSMACA; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "UpdateMode") { Log3($hash, 5, "HMUARTLGW ${name} Entering HM update mode (100k)"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_SET_UPDATE_MODE; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } elsif ($cmd->{cmd} eq "NormalMode") { Log3($hash, 5, "HMUARTLGW ${name} Entering HM normal mode (10k)"); $hash->{Helper}{OneParameterOnly} = 1; $hash->{DevState} = HMUARTLGW_STATE_SET_NORMAL_MODE; HMUARTLGW_GetSetParameterReq($hash); shift(@{$hash->{Helper}{PendingCMD}}); #retry will be handled by GetSetParameter } else { #try for HMUARTLGW_SEND_RETRY_SECONDS, packet was not sent wirelessly yet! if (defined($cmd->{RetryStart}) && $cmd->{RetryStart} + HMUARTLGW_SEND_RETRY_SECONDS <= gettimeofday()) { my $oldmsg = shift(@{$hash->{Helper}{PendingCMD}}); Log3($hash, 1, "HMUARTLGW ${name} resend failed too often, dropping packet: 01 $oldmsg->{cmd}"); #try next command return HMUARTLGW_SendPendingCmd($hash); } elsif ($cmd->{RetryStart}) { Log3($hash, 5, "HMUARTLGW ${name} Retry, initial retry initiated at: ".$cmd->{RetryStart}); } RemoveInternalTimer($hash); my $dst = substr($cmd->{cmd}, 20, 6); if ((!defined($cmd->{delayed})) && $modules{CUL_HM}{defptr}{$dst}{helper}{io}{nextSend}){ my $tn = gettimeofday(); my $dDly = $modules{CUL_HM}{defptr}{$dst}{helper}{io}{nextSend} - $tn; #$dDly -= 0.05 if ($typ eq "02");# delay at least 50ms for ACK, but not 100 if ($dDly > 0.01) { Log3($hash, 5, "HMUARTLGW ${name} delaying send to ${dst} for ${dDly}"); $hash->{DevState} = HMUARTLGW_STATE_SEND_TIMED; InternalTimer($tn + $dDly, "HMUARTLGW_SendPendingTimer", $hash, 0); $cmd->{delayed} = 1; return; } } delete($cmd->{delayed}) if (defined($cmd->{delayed})); if (hex(substr($cmd->{cmd}, 10, 2)) & (1 << 5)) { #BIDI InternalTimer(gettimeofday()+HMUARTLGW_SEND_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); $hash->{DevState} = HMUARTLGW_STATE_SEND; } else { Log3($hash, 5, "HMUARTLGW ${name} !BIDI"); InternalTimer(gettimeofday()+0.3, "HMUARTLGW_CheckCmdResp", $hash, 0); $hash->{DevState} = HMUARTLGW_STATE_SEND_NOACK; } $cmd->{CNT} = HMUARTLGW_send($hash, $cmd->{cmd}, HMUARTLGW_DST_APP); } } } sub HMUARTLGW_SendPendingTimer($) { my ($hash) = @_; $hash->{DevState} = HMUARTLGW_STATE_RUNNING; return HMUARTLGW_SendPendingCmd($hash); } sub HMUARTLGW_SendCmd($$) { my ($hash, $cmd) = @_; #Drop commands when device is not active return if ($hash->{DevState} == HMUARTLGW_STATE_NONE); push @{$hash->{Helper}{PendingCMD}}, { cmd => $cmd }; return HMUARTLGW_SendPendingCmd($hash); } sub HMUARTLGW_UpdatePeerReq($;$) { my ($hash, $peer) = @_; my $name = $hash->{NAME}; $peer = $hash->{Helper}{UpdatePeer} if (!$peer); Log3($hash, 4, "HMUARTLGW ${name} UpdatePeerReq: ".$peer->{id}.", state ".$hash->{DevState}); my $msg; if ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER) { $hash->{Helper}{UpdatePeer} = $peer; if ($peer->{operation} eq "+") { my $flags = hex($peer->{flags}); $msg = HMUARTLGW_APP_ADD_PEER . $peer->{id} . $peer->{kNo} . (($flags & 0x02) ? "01" : "00") . #Wakeup? "00"; #setting this causes "0013" messages for thermostats on wakeup ?! } else { $msg = HMUARTLGW_APP_REMOVE_PEER . $peer->{id}; } $hash->{Helper}{UpdatePeer}{msg} = $msg; } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_AES1) { my $offset = 0; foreach my $c (reverse(unpack "(A2)*", $hash->{Helper}{UpdatePeer}{aes})) { $c = ~hex($c); for (my $chan = 0; $chan < 8; $chan++) { if ($c & (1 << $chan)) { Log3($hash, 4, "HMUARTLGW ${name} Disabling AES for channel " . ($chan+$offset)); $msg .= sprintf("%02x", $chan+$offset); } } $offset += 8; } if (defined($msg)) { $msg = HMUARTLGW_APP_PEER_REMOVE_AES . $hash->{Helper}{UpdatePeer}{id} . ${msg}; } else { return HMUARTLGW_GetSetParameters($hash); } } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_AES2) { if ($peer->{operation} eq "+" && defined($peer->{aesChannels})) { Log3($hash, 4, "HMUARTLGW ${name} AESchannels: " . $peer->{aesChannels}); my $offset = 0; foreach my $c (unpack "(A2)*", $peer->{aesChannels}) { $c = hex($c); for (my $chan = 0; $chan < 8; $chan++) { if ($c & (1 << $chan)) { Log3($hash, 4, "HMUARTLGW ${name} Enabling AES for channel " . ($chan+$offset)); $msg .= sprintf("%02x", $chan+$offset); } } $offset += 8; } } if (defined($msg)) { $msg = HMUARTLGW_APP_PEER_ADD_AES . $peer->{id} . $msg; } else { return HMUARTLGW_GetSetParameters($hash); } } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_CFG) { $msg = $hash->{Helper}{UpdatePeer}{msg}; } if ($msg) { HMUARTLGW_send($hash, $msg, HMUARTLGW_DST_APP, $peer->{id}); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } } sub HMUARTLGW_UpdatePeer($$) { my ($hash, $peer) = @_; if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING) { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER; HMUARTLGW_UpdatePeerReq($hash, $peer); } else { #enqueue for next update push @{$hash->{PeerQueue}}, $peer; } } sub HMUARTLGW_UpdateQueuedPeer($) { my ($hash) = @_; if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING && $hash->{PeerQueue} && @{$hash->{PeerQueue}}) { HMUARTLGW_UpdatePeer($hash, shift(@{$hash->{PeerQueue}})); delete ($hash->{PeerQueue}) if (!@{$hash->{PeerQueue}}); return; } } sub HMUARTLGW_ParsePeer($$) { my ($hash, $msg) = @_; #040701010002fffffffffffffff9 $hash->{AssignedPeerCnt} = hex(substr($msg, 8, 4)); if (length($msg) > 12) { $hash->{Peers}{$hash->{Helper}{UpdatePeer}->{id}} = $hash->{Helper}{UpdatePeer}->{config}; $hash->{Helper}{UpdatePeer}{aes} = substr($msg, 12); Log3($hash, HMUARTLGW_getVerbLvl($hash, $hash->{Helper}{UpdatePeer}->{id}, $hash->{Helper}{UpdatePeer}->{id}, 4), "HMUARTLGW $hash->{NAME} added peer: " . $hash->{Helper}{UpdatePeer}->{id} . ", aesChannels: " . $hash->{Helper}{UpdatePeer}{aes}); } else { delete($hash->{Peers}{$hash->{Helper}{UpdatePeer}->{id}}); Log3($hash, HMUARTLGW_getVerbLvl($hash, $hash->{Helper}{UpdatePeer}->{id}, $hash->{Helper}{UpdatePeer}->{id}, 4), "HMUARTLGW $hash->{NAME} remove peer: ". $hash->{Helper}{UpdatePeer}->{id}); } } sub HMUARTLGW_GetSetParameterReq($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); if ($hash->{DevState} == HMUARTLGW_STATE_SET_HMID) { my $hmId = AttrVal($name, "hmId", undef); if (!defined($hmId)) { $hash->{DevState} = HMUARTLGW_STATE_GET_HMID; return HMUARTLGW_GetSetParameterReq($hash); } HMUARTLGW_send($hash, HMUARTLGW_APP_SET_HMID . $hmId, HMUARTLGW_DST_APP); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_HMID) { HMUARTLGW_send($hash, HMUARTLGW_APP_GET_HMID, HMUARTLGW_DST_APP); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_DEFAULT_HMID) { HMUARTLGW_send($hash, HMUARTLGW_APP_DEFAULT_HMID, HMUARTLGW_DST_APP); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_TIME) { my $tmsg = HMUARTLGW_OS_SET_TIME; my $t = time(); my @l = localtime($t); my $off = (timegm(@l) - timelocal(@l)) / 1800; $tmsg .= sprintf("%04x%02x", $t, $off & 0xff); HMUARTLGW_send($hash, $tmsg, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_FIRMWARE) { HMUARTLGW_send($hash, HMUARTLGW_OS_GET_FIRMWARE, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_SERIAL) { HMUARTLGW_send($hash, HMUARTLGW_OS_GET_SERIAL, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_NORMAL_MODE) { HMUARTLGW_send($hash, HMUARTLGW_OS_NORMAL_MODE, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_ENABLE_CSMACA) { my $csma_ca = AttrVal($name, "csmaCa", 0); HMUARTLGW_send($hash, HMUARTLGW_OS_ENABLE_CSMACA . sprintf("%02x", $csma_ca), HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_ENABLE_CREDITS) { my $dutyCycle = AttrVal($name, "dutyCycle", 1); HMUARTLGW_send($hash, HMUARTLGW_OS_ENABLE_CREDITS . sprintf("%02x", $dutyCycle), HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_INIT_CREDITS) { HMUARTLGW_send($hash, HMUARTLGW_OS_GET_CREDITS, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_CURRENT_KEY) { #current key is key with highest idx @{$hash->{Helper}{AESKeyQueue}} = HMUARTLGW_getAesKeys($hash); my $key = shift(@{$hash->{Helper}{AESKeyQueue}}); HMUARTLGW_send($hash, HMUARTLGW_APP_SET_CURRENT_KEY . ($key?$key:"00"x17), HMUARTLGW_DST_APP); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_PREVIOUS_KEY) { #previous key has second highest index my $key = shift(@{$hash->{Helper}{AESKeyQueue}}); HMUARTLGW_send($hash, HMUARTLGW_APP_SET_PREVIOUS_KEY . ($key?$key:"00"x17), HMUARTLGW_DST_APP); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_TEMP_KEY) { #temp key has third highest index my $key = shift(@{$hash->{Helper}{AESKeyQueue}}); delete($hash->{Helper}{AESKeyQueue}); HMUARTLGW_send($hash, HMUARTLGW_APP_SET_TEMP_KEY . ($key?$key:"00"x17), HMUARTLGW_DST_APP); } elsif ($hash->{DevState} >= HMUARTLGW_STATE_UPDATE_PEER && $hash->{DevState} <= HMUARTLGW_STATE_UPDATE_PEER_CFG) { HMUARTLGW_UpdatePeerReq($hash); return; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_CREDITS) { $hash->{Helper}{RoundTrip}{Calc} = 1; HMUARTLGW_send($hash, HMUARTLGW_OS_GET_CREDITS, HMUARTLGW_DST_OS); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_UPDATE_MODE) { #E9CA is magic HMUARTLGW_send($hash, HMUARTLGW_OS_UPDATE_MODE . "E9CA", HMUARTLGW_DST_OS); } else { return; } InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } sub HMUARTLGW_GetSetParameters($;$$) { my ($hash, $msg, $recvtime) = @_; my $name = $hash->{NAME}; my $oldState = $hash->{DevState}; my $hmId = AttrVal($name, "hmId", undef); my $ack = substr($msg, 2, 2) if ($msg); RemoveInternalTimer($hash); Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} GetSet Ack: ${ack}, state ".$hash->{DevState}) if ($ack); Log3($hash, 1, "HMUARTLGW ${name} GetSet NACK: ${ack}, state ".$hash->{DevState}) if ($ack && $ack =~ m/^0400/); if ($ack && ($ack eq HMUARTLGW_ACK_EINPROGRESS)) { if (defined($hash->{Helper}{GetSetRetry}) && $hash->{Helper}{GetSetRetry} > 10) { delete($hash->{Helper}{GetSetRetry}); #Reboot device HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); return; } $hash->{Helper}{GetSetRetry}++; #Retry InternalTimer(gettimeofday()+0.5, "HMUARTLGW_GetSetParameterReq", $hash, 0); return; } delete($hash->{Helper}{GetSetRetry}); if ($hash->{DevState} == HMUARTLGW_STATE_GETSET_PARAMETERS) { if ($hmId) { $hash->{DevState} = HMUARTLGW_STATE_SET_HMID; } else { $hash->{DevState} = HMUARTLGW_STATE_GET_HMID; } } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_HMID) { $hash->{DevState} = HMUARTLGW_STATE_GET_HMID; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_HMID) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { readingsSingleUpdate($hash, "D-HMIdAssigned", uc(substr($msg, 8)), 1); $hash->{owner} = uc(substr($msg, 8)); } $hash->{DevState} = HMUARTLGW_STATE_GET_DEFAULT_HMID; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_DEFAULT_HMID) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { readingsSingleUpdate($hash, "D-HMIdOriginal", uc(substr($msg, 8)), 1); } $hash->{DevState} = HMUARTLGW_STATE_SET_TIME; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_TIME) { $hash->{DevState} = HMUARTLGW_STATE_GET_FIRMWARE; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_FIRMWARE) { if ($ack eq HMUARTLGW_ACK_INFO) { my $fw = hex(substr($msg, 10, 2)).".". hex(substr($msg, 12, 2)).".". hex(substr($msg, 14, 2)); $hash->{Helper}{FW} = hex((substr($msg, 10, 6))); $fw .= " (outdated)" if ($hash->{Helper}{FW} < 0x010401); readingsSingleUpdate($hash, "D-firmware", $fw, 1); } $hash->{DevState} = HMUARTLGW_STATE_SET_NORMAL_MODE; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_NORMAL_MODE) { $hash->{DevState} = HMUARTLGW_STATE_GET_SERIAL; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_SERIAL) { if ($ack eq HMUARTLGW_ACK_INFO && $hash->{DevType} eq "UART") { readingsSingleUpdate($hash, "D-serialNr", pack("H*", substr($msg, 4)), 1); } $hash->{DevState} = HMUARTLGW_STATE_ENABLE_CSMACA; } elsif ($hash->{DevState} == HMUARTLGW_STATE_ENABLE_CSMACA) { $hash->{DevState} = HMUARTLGW_STATE_ENABLE_CREDITS; } elsif ($hash->{DevState} == HMUARTLGW_STATE_ENABLE_CREDITS) { $hash->{DevState} = HMUARTLGW_STATE_GET_INIT_CREDITS; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_INIT_CREDITS) { if ($ack eq HMUARTLGW_ACK_INFO) { HMUARTLGW_updateMsgLoad($hash, hex(substr($msg, 4))); } $hash->{DevState} = HMUARTLGW_STATE_SET_CURRENT_KEY; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_CURRENT_KEY) { $hash->{DevState} = HMUARTLGW_STATE_SET_PREVIOUS_KEY; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_PREVIOUS_KEY) { $hash->{DevState} = HMUARTLGW_STATE_SET_TEMP_KEY; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_TEMP_KEY) { $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_CREDITS) { if (defined($recvtime) && defined($hash->{Helper}{AckPending}{$hash->{DEVCNT}}) && defined($hash->{Helper}{RoundTrip}{Calc})) { delete($hash->{Helper}{RoundTrip}{Calc}); my $delay = $recvtime - $hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{time}; $hash->{Helper}{RoundTrip}{Delay} = $delay if ($delay < 0.2); Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} roundtrip delay: " . sprintf("%.4f", ${delay})); } if ($ack eq HMUARTLGW_ACK_INFO) { HMUARTLGW_updateMsgLoad($hash, hex(substr($msg, 4))); } delete($hash->{Helper}{CreditFailed}); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } elsif ($hash->{DevState} == HMUARTLGW_STATE_SET_UPDATE_MODE) { $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING && $oldState != HMUARTLGW_STATE_RUNNING && (!$hash->{Helper}{OneParameterOnly})) { #Init sequence over, add known peers $hash->{AssignedPeerCnt} = 0; foreach my $peer (keys(%{$hash->{Peers}})) { if ($modules{CUL_HM}{defptr}{$peer} && $modules{CUL_HM}{defptr}{$peer}{helper}{io}{newChn}) { my ($id, $flags, $kNo, $aesChannels) = split(/,/, $modules{CUL_HM}{defptr}{$peer}{helper}{io}{newChn}); my $p = { id => substr($id, 1), operation => substr($id, 0, 1), flags => $flags, kNo => $kNo, aesChannels => $aesChannels, config => $modules{CUL_HM}{defptr}{$peer}{helper}{io}{newChn}, }; #enqueue for later if ($p->{operation} eq "+") { $hash->{Peers}{$peer} = "pending"; push @{$hash->{PeerQueue}}, $p; } else { delete($hash->{Peers}{$peer}); } } else { delete($hash->{Peers}{$peer}); } } #start credit checker RemoveInternalTimer("HMUARTLGW_CheckCredits:$name"); InternalTimer(gettimeofday()+1, "HMUARTLGW_CheckCredits", "HMUARTLGW_CheckCredits:$name", 0); $hash->{Helper}{Initialized} = 1; HMUARTLGW_updateCondition($hash); } if ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { HMUARTLGW_ParsePeer($hash, $msg); } else { if ($hash->{Helper}{UpdatePeer}{operation} eq "+") { Log3($hash, 1, "HMUARTLGW ${name} Adding peer $hash->{Helper}{UpdatePeer}{id} failed! " . "You have probably forced an unknown aesKey for this device."); } else { Log3($hash, HMUARTLGW_getVerbLvl($hash, $hash->{Helper}{UpdatePeer}{id}, $hash->{Helper}{UpdatePeer}{id}, 4), "HMUARTLGW ${name} Removing peer $hash->{Helper}{UpdatePeer}{id} failed!"); } $hash->{Helper}{UpdatePeer}{operation} = ""; } if ($hash->{Helper}{UpdatePeer}{operation} eq "+") { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_AES1; } else { delete($hash->{Helper}{UpdatePeer}); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_AES1) { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_AES2; } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_AES2) { if ($hash->{Helper}{UpdatePeer}->{operation} eq "+") { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_CFG; } else { delete($hash->{Helper}{UpdatePeer}); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_CFG) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { HMUARTLGW_ParsePeer($hash, $msg); } delete($hash->{Helper}{UpdatePeer}); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } #Don't continue in state-machine if only one parameter should be #set/queried, SET_HMID is special, as we have to query it again #to update readings. SET_CURRENT_KEY is always followed by #SET_PREVIOUS_KEY and SET_TEMP_KEY. if ($hash->{Helper}{OneParameterOnly} && $oldState != $hash->{DevState} && $oldState != HMUARTLGW_STATE_SET_HMID && $oldState != HMUARTLGW_STATE_SET_CURRENT_KEY && $oldState != HMUARTLGW_STATE_SET_PREVIOUS_KEY) { $hash->{DevState} = HMUARTLGW_STATE_RUNNING; delete($hash->{Helper}{OneParameterOnly}); } if ($hash->{DevState} != HMUARTLGW_STATE_RUNNING) { HMUARTLGW_GetSetParameterReq($hash); } else { HMUARTLGW_UpdateQueuedPeer($hash); HMUARTLGW_SendPendingCmd($hash); } } sub HMUARTLGW_Parse($$$$) { my ($hash, $msg, $dst, $recvtime) = @_; my $name = $hash->{NAME}; my $recv; my $CULinfo = ''; $hash->{RAWMSG} = $msg; Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} recv: ".sprintf("%02X", $dst)." ${msg}, state ".$hash->{DevState}) if ($dst == HMUARTLGW_DST_OS || $dst == HMUARTLGW_DST_DUAL || $dst == HMUARTLGW_DST_DUAL_ERR || ($msg !~ m/^05/ && $msg !~ m/^040[3C]/)); #Minimally handle DualCopro-Firmware if ($dst == HMUARTLGW_DST_DUAL) { if (($msg =~ m/^00(.*)$/ || $msg =~ m/^0501(.*)$/) && $hash->{DevState} <= HMUARTLGW_STATE_ENTER_APP) { if (pack("H*", $1) eq "DualCoPro_App") { $hash->{DevState} = HMUARTLGW_STATE_UNSUPPORTED_FW; readingsSingleUpdate($hash, "D-firmware", "unsupported", 1); HMUARTLGW_updateCondition($hash); RemoveInternalTimer($hash); Log3($hash, 0, "HMUARTLGW ${name} is running unsupported firmware, please install a supported version"); } } return; } #Re-send commands for DualCopro Firmware if ($dst == HMUARTLGW_DST_DUAL_ERR) { if ($hash->{DevState} == HMUARTLGW_STATE_QUERY_APP) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 4), "HMUARTLGW ${name} Re-sending app-query for unsupported firmware"); HMUARTLGW_send($hash, HMUARTLGW_DUAL_GET_APP, HMUARTLGW_DST_DUAL); } elsif (defined($hash->{Helper}{AckPending}{$hash->{DEVCNT}}) && $hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{dst} == HMUARTLGW_DST_OS && $hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{cmd} eq HMUARTLGW_OS_CHANGE_APP) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 4), "HMUARTLGW ${name} Re-sending switch to bootloader for unsupported firmare"); HMUARTLGW_send($hash, HMUARTLGW_DUAL_CHANGE_APP, HMUARTLGW_DST_DUAL); } return; } if ($msg =~ m/^04/ && $hash->{CNT} != $hash->{DEVCNT}) { if (defined($hash->{Helper}{AckPending}{$hash->{DEVCNT}})) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} got delayed ACK for request " . $hash->{DEVCNT}.": ".$hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{dst} . " " . $hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{cmd} . sprintf(" (%.3f", (gettimeofday() - $hash->{Helper}{AckPending}{$hash->{DEVCNT}}->{time})) . "s late)"); delete($hash->{Helper}{AckPending}{$hash->{DEVCNT}}); return; } #Firmware sometimes send additional ACK when receiving the #next frame from a device after a command, even if it has #already ACKed the command. Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} Ack with invalid/old counter received, dropping. We: $hash->{CNT}, device: $hash->{DEVCNT}, " . "state: $hash->{DevState}, msg: ${dst} ${msg}"); return; } if ($msg =~ m/^04/ && $hash->{DevState} >= HMUARTLGW_STATE_GETSET_PARAMETERS && $hash->{DevState} < HMUARTLGW_STATE_RUNNING) { HMUARTLGW_GetSetParameters($hash, $msg, $recvtime); return; } if (defined($hash->{Helper}{RoundTrip}{Calc})) { #We have received another message while calculating delay. #This will skew the calculation, so don't do it now delete($hash->{Helper}{RoundTrip}{Calc}); } if ($dst == HMUARTLGW_DST_OS) { if ($msg =~ m/^00(..)/) { my $running = pack("H*", substr($msg, 2)); if ($hash->{DevState} <= HMUARTLGW_STATE_ENTER_APP) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 4), "HMUARTLGW ${name} currently running ${running}"); if ($running eq "Co_CPU_App") { $hash->{DevState} = HMUARTLGW_STATE_GETSET_PARAMETERS; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+1, "HMUARTLGW_GetSetParameters", $hash, 0); } else { if ($hash->{DevState} == HMUARTLGW_STATE_QUERY_APP) { $hash->{DevState} = HMUARTLGW_STATE_ENTER_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } else { Log3($hash, 1, "HMUARTLGW ${name} failed to enter App!"); } } } elsif ($hash->{DevState} > HMUARTLGW_STATE_ENTER_APP) { Log3($hash, 1, "HMUARTLGW ${name} unexpected info about ${running} received (module crashed?), reopening") if (!defined($hash->{FirmwareFile})); HMUARTLGW_Reopen($hash); return; } } elsif ($msg =~ m/^04(..)/) { my $ack = $1; if ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_COPRO) { HMUARTLGW_updateCoPro($hash, $msg); return; } if ($ack eq HMUARTLGW_ACK_INFO && $hash->{DevState} == HMUARTLGW_STATE_QUERY_APP) { my $running = pack("H*", substr($msg, 4)); Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 4), "HMUARTLGW ${name} currently running ${running}"); if ($running eq "Co_CPU_App") { #Reset module HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } else { if (defined($hash->{FirmwareFile}) && $hash->{FirmwareFile} ne "") { Log3($hash, 1, "HMUARTLGW ${name} starting firmware upgrade"); $hash->{FirmwareBlock} = 0; $hash->{DevState} = HMUARTLGW_STATE_UPDATE_COPRO; HMUARTLGW_updateCondition($hash); HMUARTLGW_updateCoPro($hash, $msg); return; } $hash->{DevState} = HMUARTLGW_STATE_ENTER_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } } elsif ($ack eq HMUARTLGW_ACK_NACK && $hash->{DevState} == HMUARTLGW_STATE_ENTER_APP) { Log3($hash, 1, "HMUARTLGW ${name} application switch failed, application-firmware probably corrupted!"); HMUARTLGW_Reopen($hash); return; } } elsif ($msg =~ m/^05(..)$/) { HMUARTLGW_updateMsgLoad($hash, hex($1)); } } elsif ($dst == HMUARTLGW_DST_APP) { if ($msg =~ m/^04(..)(.*)$/) { my $ack = $1; my $oldMsg; if ($hash->{DevState} == HMUARTLGW_STATE_SEND || $hash->{DevState} == HMUARTLGW_STATE_SEND_NOACK) { RemoveInternalTimer($hash); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; $oldMsg = shift @{$hash->{Helper}{PendingCMD}}; } if ($ack eq HMUARTLGW_ACK_WITH_RESPONSE || $ack eq HMUARTLGW_ACK_WITH_RESPONSE_AES_OK) { $recv = $msg; } elsif ($ack eq HMUARTLGW_ACK_WITH_RESPONSE_AES_KO) { if ($2 =~ m/^FE/) { #challenge msg $recv = $msg; } elsif ($oldMsg) { #Need to produce our own "failed" challenge $recv = substr($msg, 0, 6) . "01" . substr($oldMsg->{cmd}, 8, 2) . "A002" . substr($oldMsg->{cmd}, 20, 6) . substr($oldMsg->{cmd}, 14, 6) . "04000000000000" . sprintf("%02X", hex(substr($msg, 4, 2))*2); } $CULinfo = "AESpending"; } elsif ($ack eq HMUARTLGW_ACK_EINPROGRESS && $oldMsg) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} IO currently busy, trying again in a bit"); if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING) { $oldMsg->{RetryStart} = gettimeofday() if (!defined($oldMsg->{RetryStart})); RemoveInternalTimer($hash); unshift @{$hash->{Helper}{PendingCMD}}, $oldMsg; $hash->{DevState} = HMUARTLGW_STATE_SEND_TIMED; InternalTimer(gettimeofday()+(HMUARTLGW_BUSY_RETRY_MS / 1000), "HMUARTLGW_SendPendingTimer", $hash, 0); } return; } elsif ($ack eq HMUARTLGW_ACK_ENOCREDITS) { Log3($hash, 1, "HMUARTLGW ${name} IO in overload!"); $hash->{XmitOpen} = 0; HMUARTLGW_updateCondition($hash); } elsif ($ack eq HMUARTLGW_ACK_ECSMACA && $oldMsg) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} can't send due to CSMA/CA, trying again in a bit"); if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING) { $oldMsg->{RetryStart} = gettimeofday() if (!defined($oldMsg->{RetryStart})); RemoveInternalTimer($hash); unshift @{$hash->{Helper}{PendingCMD}}, $oldMsg; $hash->{DevState} = HMUARTLGW_STATE_SEND_TIMED; InternalTimer(gettimeofday()+(HMUARTLGW_CSMACA_RETRY_MS / 1000), "HMUARTLGW_SendPendingTimer", $hash, 0); } return; } elsif ($ack eq HMUARTLGW_ACK_EUNKNOWN && $oldMsg) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} can't send due to unknown problem (no response?)"); } else { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} Ack: ${ack} ".(($2)?$2:"")); $recv = $msg; } } elsif ($msg =~ m/^(05.*)$/) { $recv = $1; } if ($recv && $recv =~ m/^(..)(..)(..)(..)(..)(..)(..)(......)(......)(.*)$/) { my ($type, $status, $info, $rssi, $mNr, $flags, $cmd, $src, $dst, $payload) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); Log3($hash, HMUARTLGW_getVerbLvl($hash, $src, $dst, 5), "HMUARTLGW ${name} recv: 01 ${type} ${status} ${info} ${rssi} msg: ${mNr} ${flags} ${cmd} ${src} ${dst} ${payload}"); return if (!$hash->{Helper}{Initialized}); $rssi = 0 - hex($rssi); my %addvals = (RAWMSG => $msg); if ($rssi < -1) { $addvals{RSSI} = $rssi; $hash->{RSSI} = $rssi; } else { $rssi = ""; } my $dmsg; my $m = $mNr . $flags . $cmd . $src . $dst . $payload; if ($type eq HMUARTLGW_APP_ACK && $status eq HMUARTLGW_ACK_WITH_RESPONSE_AES_OK) { #Fake AES challenge for CUL_HM my $kNo = sprintf("%02X", (hex($info) * 2)); my $c = "${mNr}A002${src}${dst}04000000000000${kNo}"; $dmsg = sprintf("A%02X%s:AESpending:${rssi}:${name}", length($c)/2, uc($c)); $CULinfo = "AESCom-ok"; } elsif ($type eq HMUARTLGW_APP_RECV && ($status eq HMUARTLGW_RECV_RESP_WITH_AES_OK || $status eq HMUARTLGW_RECV_TRIG_WITH_AES_OK)) { #Fake AES response for CUL_HM $dmsg = sprintf("A%02X%s:AESpending:${rssi}:${name}", length($m)/2, uc($m)); $CULinfo = "AESCom-ok"; } elsif ($type eq HMUARTLGW_APP_RECV && $status eq HMUARTLGW_RECV_RESP_WITH_AES_KO) { #Fake AES response for CUL_HM $dmsg = sprintf("A%02X%s:AESpending:${rssi}:${name}", length($m)/2, uc($m)); $CULinfo = "AESCom-fail"; } if ($dmsg) { Log3($hash, 5, "HMUARTLGW ${name} Dispatch: ${dmsg}"); Dispatch($hash, $dmsg, \%addvals); } $dmsg = sprintf("A%02X%s:${CULinfo}:${rssi}:${name}", length($m)/2, uc($m)); Log3($hash, 5, "HMUARTLGW ${name} Dispatch: ${dmsg}"); if ($modules{CUL_HM}{defptr}{$src}) { my $flgh = hex($flags); my $wait = 0.100; $wait += 0.200 if ($flgh & (1 << 5) && # BIDI $modules{CUL_HM}{defptr}{$src}->{IODev}->{TYPE} =~ m/^(?:TSCUL|HMUARTLGW)$/s); $wait -= 0.044 if ($flgh & (1 << 6)); # received from Repeater $wait -= $hash->{Helper}{RoundTrip}{Delay} if (defined($hash->{Helper}{RoundTrip}{Delay})); if ($wait > 0) { my $nextSend = $recvtime + $wait; $modules{CUL_HM}{defptr}{$src}{helper}{io}{nextSend} = $nextSend if (!defined($modules{CUL_HM}{defptr}{$src}{helper}{io}{nextSend}) || $nextSend < $modules{CUL_HM}{defptr}{$src}{helper}{io}{nextSend} || ($recvtime - $modules{CUL_HM}{defptr}{$src}{helper}{io}{nextSend}) > 0.09); # not allready set by previous IO } } Dispatch($hash, $dmsg, \%addvals); } } if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING) { HMUARTLGW_UpdateQueuedPeer($hash); HMUARTLGW_SendPendingCmd($hash); } return; } sub HMUARTLGW_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; my $recvtime = gettimeofday(); my $buf = DevIo_SimpleRead($hash); return "" if (!defined($buf)); $buf = HMUARTLGW_decrypt($hash, $buf) if ($hash->{'.crypto'}); Log3($hash, 5, "HMUARTLGW ${name} read raw (".length($buf)."): ".unpack("H*", $buf)); my $p = pack("H*", $hash->{PARTIAL}) . $buf; $hash->{PARTIAL} .= unpack("H*", $buf); return HMUARTLGW_LGW_Init($hash) if ($hash->{LGW_Init}); return HMUARTLGW_LGW_HandleKeepAlive($hash) if ($hash->{DevType} eq "LGW-KeepAlive"); #need at least one frame delimiter return if (!($p =~ m/\xfd/)); #garbage in the beginning? if (!($p =~ m/^\xfd/)) { $p = substr($p, index($p, chr(0xfd))); } my $unprocessed; while (defined($p) && $p =~ m/^\xfd/) { $unprocessed = $p; (undef, my $frame, $p) = split(/\xfd/, $unprocessed, 3); $p = chr(0xfd) . $p if (defined($p)); my $unescaped = ''; my $unescape_next = 0; foreach my $byte (split(//, $frame)) { if (ord($byte) == 0xfc) { $unescape_next = 1; next; } if ($unescape_next) { $byte = chr(ord($byte)|0x80); $unescape_next = 0; } $unescaped .= $byte; } next if (length($unescaped) < 6); #len len dst cnt crc crc (my $len) = unpack("n", substr($unescaped, 0, 2)); if (length($unescaped) > $len + 4) { Log3($hash, 1, "HMUARTLGW ${name} frame with wrong length received: ".length($unescaped).", should: ".($len + 4).": FD".uc(unpack("H*", $unescaped))); next; } next if (length($unescaped) < $len + 4); #short read my $crc = HMUARTLGW_crc16(chr(0xfd).$unescaped); if ($crc != 0x0000 && $hash->{DevState} != HMUARTLGW_STATE_RUNNING && defined($hash->{Helper}{LastSendLen})) { #When writing to the device while it prepares to write a frame to #the host, the device seems to initialize the crc with 0x827f or #0x8281 plus the length of the frame being received (firmware bug). foreach my $slen (reverse(@{$hash->{Helper}{LastSendLen}})) { $crc = HMUARTLGW_crc16(chr(0xfd).$unescaped, 0x827f + $slen); Log3($hash, 5, "HMUARTLGW ${name} invalid checksum received, recalculated with slen ${slen}: ${crc}"); last if ($crc == 0x0000); $crc = HMUARTLGW_crc16(chr(0xfd).$unescaped, 0x8281 + $slen); Log3($hash, 5, "HMUARTLGW ${name} invalid checksum received, recalculated with slen ${slen}: ${crc}"); last if ($crc == 0x0000); } } if ($crc != 0x0000) { Log3($hash, 1, "HMUARTLGW ${name} invalid checksum received, dropping frame (FD".uc(unpack("H*", $unescaped)).")!"); undef($unprocessed); next; } Log3($hash, 5, "HMUARTLGW ${name} read (".length($unescaped)."): fd".unpack("H*", $unescaped)." crc OK"); my $dst = ord(substr($unescaped, 2, 1)); $hash->{DEVCNT} = ord(substr($unescaped, 3, 1)); my $msg = uc(unpack("H*", substr($unescaped, 4, -2))); HMUARTLGW_Parse($hash, $msg, $dst, $recvtime); delete($hash->{Helper}{AckPending}{$hash->{DEVCNT}}) if (($msg =~ m/^04/) && defined($hash->{Helper}{AckPending}) && defined($hash->{Helper}{AckPending}{$hash->{DEVCNT}})); undef($unprocessed); } if (defined($unprocessed)) { $hash->{PARTIAL} = unpack("H*", $unprocessed); } else { $hash->{PARTIAL} = ''; } } sub HMUARTLGW_Write($$$) { my ($hash, $fn, $msg) = @_; my $name = $hash->{NAME}; Log3($hash, 5, "HMUARTLGW ${name} HMUARTLGW_Write: ${msg}"); if($msg =~ m/init:(......)/) { my $dst = $1; if ($modules{CUL_HM}{defptr}{$dst} && $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}) { my ($id, $flags, $kNo, $aesChannels) = split(/,/, $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}); my $peer = { id => substr($id, 1), operation => substr($id, 0, 1), flags => $flags, kNo => $kNo, aesChannels => $aesChannels, config => $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}, }; $hash->{Peers}{$peer->{id}} = "pending"; HMUARTLGW_UpdatePeer($hash, $peer); } return; } elsif ($msg =~ m/remove:(......)/) { my $peer = { id => $1, operation => "-", }; delete($hash->{Peers}{$peer->{id}}); HMUARTLGW_UpdatePeer($hash, $peer); } elsif ($msg =~ m/^([+-])(.*)$/) { my ($id, $flags, $kNo, $aesChannels) = split(/,/, $msg); my $peer = { id => substr($id, 1), operation => substr($id, 0, 1), flags => $flags, kNo => $kNo, aesChannels => $aesChannels, config => $msg, }; if ($peer->{operation} eq "+") { $hash->{Peers}{$peer->{id}} = "pending"; } else { delete($hash->{Peers}{$peer->{id}}); } HMUARTLGW_UpdatePeer($hash, $peer); return; } elsif ($msg =~ m/^writeAesKey:(.*)$/) { HMUARTLGW_writeAesKey($1); return; } elsif ($msg =~ /^G(..)$/) { my $speed = hex($1); if ($speed == 100) { HMUARTLGW_SendCmd($hash, "UpdateMode"); } else { HMUARTLGW_SendCmd($hash, "NormalMode"); } } elsif (length($msg) > 21) { my ($flags, $mtype,$src,$dst) = (substr($msg, 6, 2), substr($msg, 8, 2), substr($msg, 10, 6), substr($msg, 16, 6)); if (!defined($hash->{owner}) || !defined($hash->{Helper}{FW})) { Log3($hash, 1, "HMUARTLGW ${name}: Device not initialized (state: $hash->{DevState}, " . ReadingsVal($name, "cond", "").") but asked to send data. Dropping: ${msg}"); return; } if ($mtype eq "02" && $src eq $hash->{owner} && length($msg) == 24 && defined($hash->{Peers}{$dst})) { # Acks are generally send by HMUARTLGW autonomously # Special Log3($hash, 5, "HMUARTLGW ${name}: Skip ACK"); return; } elsif ($mtype eq "02" && $src ne $hash->{owner} && defined($hash->{Peers}{$dst})) { Log3($hash, 0, "HMUARTLGW ${name}: Can't send ACK not originating from my hmId (firmware bug), please use a VCCU virtual device!"); return; } elsif ($flags eq "A1" && $mtype eq "12") { Log3($hash, 5, "HMUARTLGW ${name}: FIXME: filter out A112 message (it's automatically generated by the device)"); #return; } my $qLen = AttrVal($name, "qLen", 60); #Queue full? if ($hash->{Helper}{PendingCMD} && scalar(@{$hash->{Helper}{PendingCMD}}) >= $qLen) { if ($hash->{XmitOpen} == 2) { Log3($hash, 1, "HMUARTLGW ${name}: queue is full, dropping packet"); return; } elsif ($hash->{XmitOpen} == 1) { $hash->{XmitOpen} = 2; } } if (!$hash->{Peers}{$dst} && $dst ne "000000"){ #add id and enqueue command my $peer = { id => $dst, operation => "+", flags => "00", kNo => "00", config => "+${dst}", }; if ($modules{CUL_HM}{defptr}{$dst} && $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}) { my (undef, $flags, $kNo, $aesChannels) = split(/,/, $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}); $peer->{flags} = $flags; $peer->{kNo} = $kNo; $peer->{aesChannels} = $aesChannels; $peer->{config} = $modules{CUL_HM}{defptr}{$dst}{helper}{io}{newChn}; } $hash->{Peers}{$dst} = "pending"; HMUARTLGW_UpdatePeer($hash, $peer); } my $cmd = HMUARTLGW_APP_SEND . "0000"; if ($hash->{Helper}{FW} > 0x010006) { #TODO: Find real version which adds this $cmd .= ((hex(substr($msg, 6, 2)) & 0x10) ? "01" : "00"); } $cmd .= substr($msg, 4); HMUARTLGW_SendCmd($hash, $cmd); HMUARTLGW_SendCmd($hash, "Credits") if ((++$hash->{Helper}{SendCnt} % 10) == 0); # Check queue again if ($hash->{Helper}{PendingCMD} && scalar(@{$hash->{Helper}{PendingCMD}}) >= $qLen) { $hash->{XmitOpen} = 2 if ($hash->{XmitOpen} == 1); } } else { Log3($hash, 1, "HMUARTLGW ${name} write:${fn} ${msg}"); } return; } sub HMUARTLGW_StartInit($) { my ($hash) = @_; my $name = $hash->{NAME}; if ($hash->{LGW_Init}) { if ($hash->{LGW_Init} >= 10) { Log3($hash, 1, "HMUARTLGW ${name} LGW init did not complete after 10s".($hash->{'.crypto'}?", probably wrong password":"")); HMUARTLGW_Reopen($hash); return; } $hash->{LGW_Init}++; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+1, "HMUARTLGW_StartInit", $hash, 0); return; } Log3($hash, 4, "HMUARTLGW ${name} StartInit"); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); if ($hash->{DevType} eq "LGW-KeepAlive") { $hash->{DevState} = HMUARTLGW_STATE_KEEPALIVE_INIT; HMUARTLGW_sendAscii($hash, "L%02x,02,00ff,00\r\n"); return; } $hash->{DevState} = HMUARTLGW_STATE_QUERY_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_GET_APP, HMUARTLGW_DST_OS); HMUARTLGW_updateCondition($hash); return; } sub HMUARTLGW_CheckCmdResp($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); #The data we wait for might have already been received but never #read from the FD. Do a last check now and process new data. if (defined($hash->{FD})) { my $rin = ''; vec($rin, $hash->{FD}, 1) = 1; my $n = select($rin, undef, undef, 0); if ($n > 0) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} HMUARTLGW_CheckCmdResp: FD is readable, this might be the data we are looking for!"); #We will be back very soon! InternalTimer(gettimeofday()+0, "HMUARTLGW_CheckCmdResp", $hash, 0); HMUARTLGW_Read($hash); return; } } if ($hash->{DevState} == HMUARTLGW_STATE_SEND) { $hash->{Helper}{PendingCMD}->[0]->{RetryStart} = gettimeofday() if (!defined($hash->{Helper}{PendingCMD}->[0]->{RetryStart})); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; return HMUARTLGW_SendPendingCmd($hash); } elsif ($hash->{DevState} == HMUARTLGW_STATE_SEND_NOACK) { shift(@{$hash->{Helper}{PendingCMD}}); $hash->{DevState} = HMUARTLGW_STATE_RUNNING; #try next command return HMUARTLGW_SendPendingCmd($hash); } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_CREDITS && (!defined($hash->{Helper}{CreditFailed}) || ($hash->{Helper}{CreditFailed} < 3))) { $hash->{Helper}{CreditFailed}++; $hash->{DevState} = HMUARTLGW_STATE_RUNNING; RemoveInternalTimer("HMUARTLGW_CheckCredits:$name"); InternalTimer(gettimeofday()+1, "HMUARTLGW_CheckCredits", "HMUARTLGW_CheckCredits:$name", 0); } elsif ($hash->{DevState} != HMUARTLGW_STATE_RUNNING) { if ((!defined($hash->{Helper}{AckPending}{$hash->{CNT}}{frame})) || (defined($hash->{Helper}{AckPending}{$hash->{CNT}}{resend}) && $hash->{Helper}{AckPending}{$hash->{CNT}}{resend} >= HMUARTLGW_CMD_RETRY_CNT)) { Log3($hash, 1, "HMUARTLGW ${name} did not respond after all, reopening"); HMUARTLGW_Reopen($hash); } else { $hash->{Helper}{AckPending}{$hash->{CNT}}{resend}++; Log3($hash, 1, "HMUARTLGW ${name} did not respond for the " . $hash->{Helper}{AckPending}{$hash->{CNT}}{resend} . ". time, resending"); HMUARTLGW_send_frame($hash, pack("H*", $hash->{Helper}{AckPending}{$hash->{CNT}}{frame})); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } } return; } sub HMUARTLGW_Get($@) { my ( $hash, $name, $cmd, @args ) = @_; my $ret = ""; return "Unknown argument ${cmd}, choose one of " if ($hash->{DevType} eq "LGW-KeepAlive"); if ($cmd eq "assignIDs") { foreach my $peer (keys(%{$hash->{Peers}})) { next if ($hash->{Peers}{$peer} !~ m/^\+/); $ret .= "\n${peer} : " . CUL_HM_id2Name($peer); } $ret = "assignedIDs: ". ($ret =~ tr/\n//) . $ret; } else { $ret = "Unknown argument ${cmd}, choose one of " . join(" ",map {"$_" . ($gets{$_} ? ":$gets{$_}" : "")} keys %gets); } return $ret; } sub HMUARTLGW_RemoveHMPair($) { my ($in) = shift; my (undef,$name) = split(':',$in); my $hash = $defs{$name}; RemoveInternalTimer("hmPairForSec:$name"); Log3($hash, 3, "HMUARTLGW ${name} left pairing-mode") if ($hash->{hmPair}); delete($hash->{hmPair}); delete($hash->{hmPairSerial}); } sub HMUARTLGW_Set($@) { my ($hash, $name, $cmd, @a) = @_; my $arg = join(" ", @a); return "\"set\" needs at least one parameter" if (!$cmd); return "Unknown argument ${cmd}, choose one of " if ($hash->{DevType} eq "LGW-KeepAlive"); if ($cmd eq "hmPairForSec") { $arg = 60 if(!$arg || $arg !~ m/^\d+$/); HMUARTLGW_RemoveHMPair("hmPairForSec:$name"); $hash->{hmPair} = 1; InternalTimer(gettimeofday()+$arg, "HMUARTLGW_RemoveHMPair", "hmPairForSec:$name", 0); Log3($hash, 3, "HMUARTLGW ${name} entered pairing-mode"); } elsif ($cmd eq "hmPairSerial") { return "Usage: set $name hmPairSerial <10-character-serialnumber>" if(!$arg || $arg !~ m/^.{10}$/); my $id = InternalVal($hash->{NAME}, "owner", "123456"); $hash->{HM_CMDNR} = $hash->{HM_CMDNR} ? ($hash->{HM_CMDNR}+1)%256 : 1; HMUARTLGW_Write($hash, undef, sprintf("As15%02X8401%s000000010A%s", $hash->{HM_CMDNR}, $id, unpack('H*', $arg))); HMUARTLGW_RemoveHMPair("hmPairForSec:$name"); $hash->{hmPair} = 1; $hash->{hmPairSerial} = $arg; InternalTimer(gettimeofday()+20, "HMUARTLGW_RemoveHMPair", "hmPairForSec:".$name, 0); } elsif ($cmd eq "reopen") { HMUARTLGW_Reopen($hash); } elsif ($cmd eq "close") { #switch to bootloader to stop the module from interfering HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS) if ($hash->{DevState} > HMUARTLGW_STATE_ENTER_APP); HMUARTLGW_Undefine($hash, $name); readingsSingleUpdate($hash, "state", "closed", 1); $hash->{XmitOpen} = 0; } elsif ($cmd eq "open") { HMUARTLGW_InitConnection($hash); } elsif ($cmd eq "restart") { HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); } elsif ($cmd eq "updateCoPro") { return "Usage: set $name updateCoPro " if(!$arg); my $block = HMUARTLGW_firmwareGetBlock($hash, $arg, 0); return "${arg} is not a valid firmware file!" if (!defined($block) || $block eq ""); $hash->{FirmwareFile} = $arg; HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); } else { return "Unknown argument ${cmd}, choose one of " . join(" ",map {"$_" . ($sets{$_} ? ":$sets{$_}" : "")} keys %sets); } return undef; } sub HMUARTLGW_Attr(@) { my ($cmd, $name, $aName, $aVal) = @_; my $hash = $defs{$name}; my $retVal; Log3($hash, 5, "HMUARTLGW ${name} Attr ${cmd} ${aName} ".(($aVal)?$aVal:"")); return "Attribute ${cmd} not supported on keepAlive-subdevice" if ($hash->{DevType} eq "LGW-KeepAlive"); if ($aName eq "hmId") { if ($cmd eq "set") { my $owner_ccu = InternalVal($name, "owner_CCU", undef); return "device owned by $owner_ccu" if ($owner_ccu); return "wrong syntax: hmId must be 6-digit-hex-code (3 byte)" if ($aVal !~ m/^[A-F0-9]{6}$/i); $attr{$name}{$aName} = $aVal; if ($init_done) { HMUARTLGW_SendCmd($hash, "HMID"); } } } elsif ($aName eq "lgwPw") { if ($init_done) { if ($hash->{DevType} eq "LGW") { HMUARTLGW_Reopen($hash); } } } elsif ($aName =~ m/^hmKey(.?)$/) { if ($cmd eq "set") { my $kNo = 1; $kNo = $1 if ($1); my ($no,$val) = (sprintf("%02X",$kNo),$aVal); if ($aVal =~ m/:/){#number given ($no,$val) = split ":",$aVal; return "illegal number:$no" if (hex($no) < 1 || hex($no) > 255 || length($no) != 2); } $attr{$name}{$aName} = "$no:". (($val =~ m /^[0-9A-Fa-f]{32}$/ ) ? $val : unpack('H*', Digest::MD5::md5($val))); $retVal = "$aName set to $attr{$name}{$aName}" if($aVal ne $attr{$name}{$aName}); } else { delete $attr{$name}{$aName}; } HMUARTLGW_writeAesKey($name) if ($init_done); } elsif ($aName eq "dutyCycle") { if ($cmd eq "set") { return "wrong syntax: dutyCycle must be 1 or 0" if ($aVal !~ m/^[01]$/); $attr{$name}{$aName} = $aVal; #$retVal = "Please make sure to be in compliance with local regulations when disabling dutyCycle!" # if (!($aVal)); } else { delete $attr{$name}{$aName}; } if ($init_done) { HMUARTLGW_SendCmd($hash, "DutyCycle"); } } elsif ($aName eq "csmaCa") { if ($cmd eq "set") { return "wrong syntax: csmaCa must be 1 or 0" if ($aVal !~ m/^[01]$/); $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; } if ($init_done) { HMUARTLGW_SendCmd($hash, "CSMACA"); } } elsif ($aName eq "qLen") { if ($cmd eq "set") { return "wrong syntax: qLen must be between 1 and 200" if ($aVal !~ m/^\d+$/ || $aVal < 1 || $aVal > 200); $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; } } elsif ($aName eq "logIDs") { if ($cmd eq "set") { my @ids = split(/,/, $aVal); $hash->{Helper}{Log}{IDs} = \@ids; $hash->{Helper}{Log}{Resolve} = 1; $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; delete $hash->{Helper}{Log}; } } elsif ($aName eq "verbose") { if ($hash->{keepAlive}) { if ($cmd eq "set") { $attr{$hash->{keepAlive}->{NAME}}{$aName} = $aVal; } else { delete $attr{$hash->{keepAlive}->{NAME}}{$aName}; } } } elsif ($aName eq "dummy") { if ($cmd eq "set") { if (!defined($attr{$name}{$aName})) { HMUARTLGW_Dummy($hash); } } else { if (defined($attr{$name}{$aName})) { delete $attr{$name}{$aName}; DevIo_OpenDev($hash, 0, "HMUARTLGW_DoInit", \&HMUARTLGW_Connect); } } } elsif ($aName eq "loadEvents") { if ($cmd eq "set") { return "wrong syntax: loadEvents must be 1 or 0" if ($aVal !~ m/^[01]$/); $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; } } return $retVal; } sub HMUARTLGW_getAesKeys($) { my ($hash) = @_; my $name = $hash->{NAME}; my @k; my %keys = (); my $vccu = InternalVal($name,"owner_CCU",$name); $vccu = $name if(!AttrVal($vccu,"hmKey","")); foreach my $i (1..3){ my ($kNo,$k) = split(":",AttrVal($vccu,"hmKey".($i== 1?"":$i),"")); if (defined($kNo) && defined($k)) { $keys{$kNo} = $k; } } my @kNos = reverse(sort(keys(%keys))); foreach my $kNo (@kNos) { Log3($hash, 4, "HMUARTLGW ${name} key: ".$keys{$kNo}.", idx: ".$kNo); push @k, $keys{$kNo} . $kNo; } return @k; } sub HMUARTLGW_writeAesKey($) { my ($name) = @_; return if (!$name || !$defs{$name} || $defs{$name}{TYPE} ne "HMUARTLGW"); my $hash = $defs{$name}; HMUARTLGW_SendCmd($hash, "AESkeys"); HMUARTLGW_SendPendingCmd($hash); } sub HMUARTLGW_updateCondition($) { my ($hash) = @_; my $name = $hash->{NAME}; my $cond = "disconnected"; my $loadLvl = "suspended"; my $oldLoad = ReadingsVal($name, "load", -1); if (defined($hash->{msgLoadCurrent})) { my $load = $hash->{msgLoadCurrent}; readingsSingleUpdate($hash, "load", $load, AttrVal($name, "loadEvents", 0)); $cond = "ok"; #FIXME: Dynamic levels if ($load >= 100) { $cond = "ERROR-Overload"; $loadLvl = "suspended"; } elsif ($oldLoad >= 100) { $cond = "Overload-released"; $loadLvl = "high"; } elsif ($load >= 90) { $cond = "Warning-HighLoad"; $loadLvl = "high"; } elsif ($load >= 40) { #FIXME: batchLevel != 40 needs to be in {helper}{loadLvl}{bl} $loadLvl = "batchLevel"; } else { $loadLvl = "low"; } } if ((!defined($hash->{XmitOpen})) || $hash->{XmitOpen} == 0) { $cond = "ERROR-Overload"; $loadLvl = "suspended"; } if (!defined($hash->{Helper}{Initialized})) { $cond = "init"; $loadLvl = "suspended"; } if ($hash->{DevState} == HMUARTLGW_STATE_NONE) { $cond = "disconnected"; $loadLvl = "suspended"; } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_COPRO) { $cond = "fwupdate"; $loadLvl = "suspended"; } elsif ($hash->{DevState} == HMUARTLGW_STATE_UNSUPPORTED_FW) { $cond = "unsupported firmware"; $loadLvl = "suspended"; } if ((defined($cond) && $cond ne ReadingsVal($name, "cond", "")) || (defined($loadLvl) && $loadLvl ne ReadingsVal($name, "loadLvl", ""))) { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "cond", $cond) if (defined($cond) && $cond ne ReadingsVal($name, "cond", "")); readingsBulkUpdate($hash, "loadLvl", $loadLvl) if (defined($loadLvl) && $loadLvl ne ReadingsVal($name, "loadLvl", "")); readingsEndUpdate($hash, 1); my $ccu = InternalVal($name,"owner_CCU",""); CUL_HM_UpdtCentralState($ccu) if ($ccu); } } sub HMUARTLGW_updateMsgLoad($$) { my ($hash, $load) = @_; if ($hash->{XmitOpen} != 2) { if ($load >= 199) { $hash->{XmitOpen} = 0; } else { $hash->{XmitOpen} = 1; } } my $adjustedLoad = int(($load + 1) / 2); my $histSlice = 5 * 60; my $histNo = 3600 / $histSlice; if ((!defined($hash->{Helper}{loadLvl}{lastHistory})) || ($hash->{Helper}{loadLvl}{lastHistory} + $histSlice) <= gettimeofday()) { my @abshist = ("-") x $histNo; unshift @abshist, split("/", $hash->{msgLoadHistoryAbs}) if (defined($hash->{msgLoadHistoryAbs})); unshift @abshist, $adjustedLoad; my $last; my @hist = ("-") x $histNo; foreach my $l (reverse(@abshist)) { next if ($l eq "-"); unshift @hist, $l - $last if (defined($last)); $last = $l; } $hash->{msgLoadHistory} = join("/", @hist[0..($histNo - 1)]); $hash->{msgLoadHistoryAbs} = join("/", @abshist[0..($histNo)]); if (!defined($hash->{Helper}{loadLvl}{lastHistory})) { $hash->{Helper}{loadLvl}{lastHistory} = gettimeofday(); } else { $hash->{Helper}{loadLvl}{lastHistory} += $histSlice; } } if ((!defined($hash->{msgLoadCurrent})) || $hash->{msgLoadCurrent} != $adjustedLoad) { $hash->{msgLoadCurrent} = $adjustedLoad; HMUARTLGW_updateCondition($hash); } } sub HMUARTLGW_send($$$;$) { my ($hash, $msg, $dst, $peer) = @_; my $name = $hash->{NAME}; my $log; my $v; if ($dst == HMUARTLGW_DST_APP && uc($msg) =~ m/^(02)(..)(..)(.*)$/) { $log = "01 ${1} ${2} ${3} "; my $m = $4; if ($hash->{Helper}{FW} > 0x010006) { $log .= substr($m, 0, 2, '') . " "; } else { $log .= "XX "; } if ($m =~ m/^(..)(..)(..)(......)(......)(.*)$/) { $log .= "msg: ${1} ${2} ${3} ${4} ${5} ${6}"; } else { $log .= $m; } $v = HMUARTLGW_getVerbLvl($hash, $4, $5, 5); } elsif ($dst == HMUARTLGW_DST_APP && uc($msg) =~ m/^(0[3BF]).*[^0].*(..)$/) { #Key, do not log $log = sprintf("%02X", $dst). " ${1}" . ("XX"x16) . $2; $v = HMUARTLGW_getVerbLvl($hash, undef, undef, 5); } else { $log = sprintf("%02X", $dst). " ".uc($msg); $v = HMUARTLGW_getVerbLvl($hash, $peer, $peer, 5); } Log3($hash, $v, "HMUARTLGW ${name} send: ${log}"); $hash->{CNT} = ($hash->{CNT} + 1) & 0xff; my $frame = pack("CnCCH*", 0xfd, (length($msg) / 2) + 2, $dst, $hash->{CNT}, $msg); $frame .= pack("n", HMUARTLGW_crc16($frame)); my $sendtime = HMUARTLGW_send_frame($hash, $frame); if (defined($hash->{Helper}{AckPending}{$hash->{CNT}})) { Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} never got an ACK for request ". $hash->{CNT}.": ".$hash->{Helper}{AckPending}{$hash->{CNT}}->{dst} . " " . $hash->{Helper}{AckPending}{$hash->{CNT}}->{cmd} . sprintf(" (%.3f", ($sendtime - $hash->{Helper}{AckPending}{$hash->{CNT}}->{time})). "s ago)"); } $hash->{Helper}{AckPending}{$hash->{CNT}} = { cmd => uc($msg), frame => uc(unpack("H*", $frame)), dst => $dst, time => $sendtime, }; push @{$hash->{Helper}{LastSendLen}}, (length($hash->{Helper}{AckPending}{$hash->{CNT}}->{cmd}) / 2) + 2; shift @{$hash->{Helper}{LastSendLen}} if (scalar(@{$hash->{Helper}{LastSendLen}}) > 2); delete($hash->{Helper}{Resend}); return $hash->{CNT}; } sub HMUARTLGW_send_frame($$) { my ($hash, $frame) = @_; my $name = $hash->{NAME}; Log3($hash, 5, "HMUARTLGW ${name} send: (".length($frame)."): ".unpack("H*", $frame)); my $escaped = substr($frame, 0, 1); foreach my $byte (split(//, substr($frame, 1))) { if (ord($byte) != 0xfc && ord($byte) != 0xfd) { $escaped .= $byte; next; } $escaped .= chr(0xfc); $escaped .= chr(ord($byte) & 0x7f); } $escaped = HMUARTLGW_encrypt($hash, $escaped) if ($hash->{'.crypto'}); my $sendtime = scalar(gettimeofday()); DevIo_SimpleWrite($hash, $escaped, 0); $sendtime; } sub HMUARTLGW_sendAscii($$) { my ($hash, $msg) = @_; my $name = $hash->{NAME}; $msg = sprintf($msg, $hash->{CNT}); my $logmsg = $msg; $logmsg =~ s/\r\n$//; Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} send (".length($logmsg)."): ". $logmsg); $msg = HMUARTLGW_encrypt($hash, $msg) if ($hash->{'.crypto'} && !($msg =~ m/^V/)); $hash->{CNT} = ($hash->{CNT} + 1) & 0xff; DevIo_SimpleWrite($hash, $msg, ($hash->{'.crypto'} && !($msg =~ m/^V/))? 0 : 2); } sub HMUARTLGW_crc16($;$) { my ($msg, $crc) = @_; $crc = 0xd77f if (!defined($crc)); foreach my $byte (split(//, $msg)) { $crc ^= (ord($byte) << 8) & 0xff00; for (my $i = 0; $i < 8; $i++) { if ($crc & 0x8000) { $crc = ($crc << 1) & 0xffff; $crc ^= 0x8005; } else { $crc = ($crc << 1) & 0xffff; } } } return $crc; } sub HMUARTLGW_encrypt($$) { my ($hash, $plaintext) = @_; my $ciphertext = ''; while(length($plaintext)) { if(length($hash->{'.crypto'}{encrypt}{keystream})) { my $len = length($plaintext); $len = length($hash->{'.crypto'}{encrypt}{keystream}) if (length($hash->{'.crypto'}{encrypt}{keystream}) < $len); my $ppart = substr($plaintext, 0, $len, ''); my $kpart = substr($hash->{'.crypto'}{encrypt}{keystream}, 0, $len, ''); $hash->{'.crypto'}{encrypt}{ciphertext} .= $ppart ^ $kpart; $ciphertext .= $ppart ^ $kpart; } else { $hash->{'.crypto'}{encrypt}{keystream} = $hash->{'.crypto'}{cipher}->encrypt($hash->{'.crypto'}{encrypt}{ciphertext}); $hash->{'.crypto'}{encrypt}{ciphertext} = ''; } } $ciphertext; } sub HMUARTLGW_decrypt($$) { my ($hash, $ciphertext) = @_; my $plaintext = ''; while(length($ciphertext)) { if(length($hash->{'.crypto'}{decrypt}{keystream})) { my $len = length($ciphertext); $len = length($hash->{'.crypto'}{decrypt}{keystream}) if (length($hash->{'.crypto'}{decrypt}{keystream}) < $len); my $cpart = substr($ciphertext, 0, $len, ''); my $kpart = substr($hash->{'.crypto'}{decrypt}{keystream}, 0, $len, ''); $hash->{'.crypto'}{decrypt}{ciphertext} .= $cpart; $plaintext .= $cpart ^ $kpart; } else { $hash->{'.crypto'}{decrypt}{keystream} = $hash->{'.crypto'}{cipher}->encrypt($hash->{'.crypto'}{decrypt}{ciphertext}); $hash->{'.crypto'}{decrypt}{ciphertext} = ''; } } $plaintext; } sub HMUARTLGW_firmwareGetBlock($$$) { my ($hash, $file, $id) = @_; my $name = $hash->{NAME}; my $block = ""; my $ret = open(my $fd, "<", $file); if (!$ret) { Log3($hash, 1, "HMUARTLGW ${name} can't open firmware file ${file}: $!"); return undef; } my $fw = ""; while(<$fd>) { $fw .= $_; } close($fd); my $n = 0; while(length($fw)) { my $len = unpack('n', pack('H4', $fw)); if ($n eq $id) { $block = substr($fw, 4, $len * 2); last; } $fw = substr($fw, 4 + ($len * 2)); $n++; } if ($n != $id) { Log3($hash, 1, "HMUARTLGW ${name} invalid block ${id} requested"); return undef; } $block; } sub HMUARTLGW_updateCoPro($$) { my ($hash, $msg) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); if (($hash->{FirmwareBlock} > 0) && ($msg !~ /^0401$/)) { Log3($hash, 1, "HMUARTLGW ${name} firmware flash failed on block " . ($hash->{FirmwareBlock} - 1)); delete($hash->{FirmwareFile}); delete($hash->{FirmwareBlock}); $hash->{DevState} = HMUARTLGW_STATE_QUERY_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_GET_APP, HMUARTLGW_DST_OS); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } my $block = HMUARTLGW_firmwareGetBlock($hash, $hash->{FirmwareFile}, $hash->{FirmwareBlock}); if (!defined($block)) { Log3($hash, 1, "HMUARTLGW ${name} firmware update aborted"); delete($hash->{FirmwareFile}); delete($hash->{FirmwareBlock}); $hash->{DevState} = HMUARTLGW_STATE_QUERY_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_GET_APP, HMUARTLGW_DST_OS); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } elsif ($block eq "") { Log3($hash, 1, "HMUARTLGW ${name} firmware update successfull"); delete($hash->{FirmwareFile}); delete($hash->{FirmwareBlock}); $hash->{DevState} = HMUARTLGW_STATE_QUERY_APP; HMUARTLGW_send($hash, HMUARTLGW_OS_GET_APP, HMUARTLGW_DST_OS); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } #strip CRC from block $block = substr($block, 0, -4); HMUARTLGW_send($hash, HMUARTLGW_OS_UPDATE_FIRMWARE . ${block}, HMUARTLGW_DST_OS); $hash->{FirmwareBlock}++; InternalTimer(gettimeofday()+HMUARTLGW_FIRMWARE_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); } sub HMUARTLGW_getVerbLvl($$$$) { my ($hash, $src, $dst, $def) = @_; $hash = $hash->{'.lgwHash'} if (defined($hash->{'.lgwHash'})); #Lookup IDs on change if (defined($hash->{Helper}{Log}{Resolve}) && $init_done) { foreach my $id (@{$hash->{Helper}{Log}{IDs}}) { next if ($id =~ /^([\da-f]{6}|sys|all)$/i); my $newId = substr(CUL_HM_name2Id($id),0,6); next if ($newId !~ /^[\da-f]{6}$/i); $id = $newId; } delete($hash->{Helper}{Log}{Resolve}); } return (grep /^sys$/i, @{$hash->{Helper}{Log}{IDs}}) ? 0 : $def if ((!defined($src)) || (!defined($dst))); return (grep /^($src|$dst|all)$/i, @{$hash->{Helper}{Log}{IDs}}) ? 0 : $def; } 1; =pod =item summary support for the HomeMatic UART module (RPi) and Wireless LAN Gateway =item summary_DE Anbindung von HomeMatic UART Modul (RPi) und Wireless LAN Gateway =begin html

HMUARTLGW

=end html =begin html_DE

HMUARTLGW

=end html_DE =cut