############################################## # $Id$ # # 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 # - Hide crypto state from list so it can be binary # - resolve names in logIDs package main; use strict; use warnings; 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_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_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_GET_PEERS => 17, 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_CMD_TIMEOUT => 10, HMUARTLGW_SEND_TIMEOUT => 10, }; my %sets = ( "hmPairForSec" => "HomeMatic", "hmPairSerial" => "HomeMatic", "reopen" => "", "open" => "", "close" => "", "restart" => "", ); my %gets = ( ); sub HMUARTLGW_Initialize($) { my ($hash) = @_; require "$attr{global}{modpath}/FHEM/DevIo.pm"; $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->{Clients} = ":CUL_HM:"; my %ml = ( "1:CUL_HM" => "^A......................" ); $hash->{MatchList} = \%ml; $hash->{AttrList}= "hmId " . "lgwPw " . "hmKey hmKey2 hmKey3 " . "dutyCycle:1,0 " . "csmaCa:1,0 " . "qLen " . "logIDs ". $readingFnAttributes; } sub HMUARTLGW_SendPendingCmd($); sub HMUARTLGW_SendCmd($$); sub HMUARTLGW_GetSetParameterReq($); sub HMUARTLGW_getAesKeys($); sub HMUARTLGW_updateMsgLoad($$); sub HMUARTLGW_Read($); sub HMUARTLGW_send($$$); sub HMUARTLGW_send_frame($$); sub HMUARTLGW_crc16($;$); sub HMUARTLGW_encrypt($$); sub HMUARTLGW_decrypt($$); sub HMUARTLGW_getVerbLvl($$$$); 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->{msgLoadCurrentRaw}); delete($hash->{msgLoadHistory}); delete($hash->{msgLoadHistoryAbs}); delete($hash->{owner}); $hash->{DevState} = HMUARTLGW_STATE_NONE; $hash->{XmitOpen} = 0; $hash->{LGW_Init} = 1 if ($hash->{DevType} =~ m/^LGW/); $hash->{Helper}{log} = [ split(/,/, AttrVal($name, "logIDs", "")) ]; 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"; $defs{$keepAlive->{NAME}} = $keepAlive; DevIo_CloseDev($keepAlive); my ($ip, $port) = split(/:/, $hash->{DeviceName}); $keepAlive->{DeviceName} = "${ip}:" . ($port + 1); DevIo_OpenDev($keepAlive, 0, "HMUARTLGW_DoInit"); $hash->{keepAlive} = $keepAlive; } InternalTimer(gettimeofday()+1, "HMUARTLGW_StartInit", $hash, 0); return; } 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 { $dev .= "\@115200" if ($dev !~ m/\@/); $hash->{DevType} = "UART"; readingsBeginUpdate($hash); delete($hash->{READINGS}{"D-LANfirmware"}); readingsBulkUpdate($hash, "D-type", "HM-MOD-UART"); readingsEndUpdate($hash, 1); } $hash->{DeviceName} = $dev; return DevIo_OpenDev($hash, 0, "HMUARTLGW_DoInit"); } sub HMUARTLGW_Undefine($$;$) { my ($hash, $name, $noclose) = @_; RemoveInternalTimer($hash); RemoveInternalTimer("HMUARTLGW_CheckCredits:$name"); if ($hash->{keepAlive}) { RemoveInternalTimer($hash->{keepAlive}); DevIo_CloseDev($hash->{keepAlive}); delete($attr{$hash->{keepAlive}->{NAME}}); delete($defs{$hash->{keepAlive}->{NAME}}); delete($hash->{keepAlive}); $devcount--; } DevIo_CloseDev($hash) if (!$noclose); $hash->{DevState} = HMUARTLGW_STATE_NONE; HMUARTLGW_updateCondition($hash); } sub HMUARTLGW_Reopen($;$) { my ($hash, $noclose) = @_; $hash = $hash->{lgwHash} if ($hash->{lgwHash}); my $name = $hash->{NAME}; Log3($hash, 1, "HMUARTLGW ${name} Reopen"); HMUARTLGW_Undefine($hash, $name, $noclose); return DevIo_OpenDev($hash, 1, "HMUARTLGW_DoInit"); } sub HMUARTLGW_Ready($) { my ($hash) = @_; my $name = $hash->{NAME}; Log3($hash, 4, "HMUARTLGW ${name} ready: ".$hash->{STATE}); if ((!$hash->{lgwHash}) && $hash->{STATE} eq "disconnected") { return HMUARTLGW_Reopen($hash, 1); } return 0; } #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") { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "D-type", $2); readingsBulkUpdate($hash, "D-LANfirmware", $3); readingsBulkUpdate($hash, "D-serialNr", $4); 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} = $2; $hash->{crypto}{decrypt}{keystream} = ''; $hash->{crypto}{decrypt}{ciphertext} = $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; my $wdTimer = 10; #now we have 15s InternalTimer(gettimeofday()+$wdTimer, "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"); 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", 1); } sub HMUARTLGW_SendPendingCmd($) { my ($hash) = @_; my $name = $hash->{NAME}; if ($hash->{XmitOpen} == 2) { if ($hash->{Helper}{PendingCMD}) { my $qLen = AttrVal($name, "qLen", 20); 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 3s, packet was not sent wirelessly yet! if (defined($cmd->{RetryCnt}) && $cmd->{RetryCnt} >= 15) { 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->{RetryCnt}) { Log3($hash, 5, "HMUARTLGW ${name} Retry: ".$cmd->{RetryCnt}); } 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) = @_; 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}; } } 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: " . sprintf("%08x", $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) { $hash->{AssignedPeerCnt} = 0; %{$hash->{Helper}{AssignedPeers}} = (); $msg = HMUARTLGW_APP_GET_PEERS; } if ($msg) { HMUARTLGW_send($hash, $msg, HMUARTLGW_DST_APP); 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->{Helper}{PeerQueue}}, $peer; } } sub HMUARTLGW_UpdateQueuedPeer($) { my ($hash) = @_; if ($hash->{DevState} == HMUARTLGW_STATE_RUNNING && $hash->{Helper}{PeerQueue} && @{$hash->{Helper}{PeerQueue}}) { return HMUARTLGW_UpdatePeer($hash, shift(@{$hash->{Helper}{PeerQueue}})); } } sub HMUARTLGW_ParsePeers($$) { my ($hash, $msg) = @_; my $peers = substr($msg, 8); while($peers) { my $id = substr($peers, 0, 6, ''); my $aesChannels = substr($peers, 0, 16, ''); my $flags = hex(substr($peers, 0, 2, '')); Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 4), "HMUARTLGW $hash->{NAME} known peer: ${id}, aesChannels: ${aesChannels}, flags: ${flags}"); $hash->{Helper}{AssignedPeers}{$id} = "$aesChannels (flags: ${flags})"; $hash->{AssignedPeerCnt}++; } } 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", 1); 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_GET_PEERS) { $hash->{AssignedPeerCnt} = 0; %{$hash->{Helper}{AssignedPeers}} = (); HMUARTLGW_send($hash, HMUARTLGW_APP_GET_PEERS, 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))); 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_GET_PEERS; } elsif ($hash->{DevState} == HMUARTLGW_STATE_GET_PEERS) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { #04070207... HMUARTLGW_ParsePeers($hash, $msg); #more parts in multipart message? if (hex(substr($msg, 4, 2)) < hex(substr($msg, 6, 2))) { #there will be more answer messages $hash->{DevState} = HMUARTLGW_STATE_GET_PEERS; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } } if (defined($hash->{Helper}{AssignedPeers}) && %{$hash->{Helper}{AssignedPeers}}) { foreach my $p (keys(%{$hash->{Helper}{AssignedPeers}})) { unshift @{$hash->{Helper}{PeerQueue}}, { id => $p, operation => "-" }; } } $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: ${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 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, }; #enqueue for later if ($p->{operation} eq "+") { $hash->{Peers}{$peer} = "pending"; push @{$hash->{Helper}{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", 1); $hash->{Helper}{Initialized} = 1; HMUARTLGW_updateCondition($hash); } if ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER) { $hash->{AssignedPeerCnt} = 0; if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { #040701010002fffffffffffffff9 $hash->{AssignedPeerCnt} = hex(substr($msg, 8, 4)); if (length($msg) > 12) { $hash->{Helper}{AssignedPeers}{$hash->{Helper}{UpdatePeer}->{id}} = substr($msg, 12); $hash->{Helper}{UpdatePeer}{aes} = substr($msg, 12); } } else { if ($hash->{Helper}{UpdatePeer}{operation} == "+") { Log3($hash, 1, "Adding peer $hash->{Helper}{UpdatePeer}{id} failed! " . "You have probably forced an unknown aesKey for this device."); } else { Log3($hash, 1, "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 { if (defined($hash->{Helper}{PeerQueue}) && @{$hash->{Helper}{PeerQueue}}) { #Still peers in queue, get current assigned peers #only when queue is empty $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } else { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_CFG; } } } 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->{Peers}{$hash->{Helper}{UpdatePeer}->{id}} = "assigned"; } else { delete($hash->{Peers}{$hash->{Helper}{UpdatePeer}->{id}}); } if (defined($hash->{Helper}{PeerQueue}) && @{$hash->{Helper}{PeerQueue}}) { #Still peers in queue, get current assigned peers #only when queue is empty $hash->{DevState} = HMUARTLGW_STATE_RUNNING; } else { $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_CFG; } } elsif ($hash->{DevState} == HMUARTLGW_STATE_UPDATE_PEER_CFG) { if ($ack eq HMUARTLGW_ACK_WITH_MULTIPART_DATA) { HMUARTLGW_ParsePeers($hash, $msg); #more parts in multipart message? if (hex(substr($msg, 4, 2)) < hex(substr($msg, 6, 2))) { #there will be more messages $hash->{DevState} = HMUARTLGW_STATE_UPDATE_PEER_CFG; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+HMUARTLGW_CMD_TIMEOUT, "HMUARTLGW_CheckCmdResp", $hash, 0); return; } } 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 eq HMUARTLGW_DST_OS || ($msg !~ m/^05/ && $msg !~ m/^040[3C]/)); 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; } Log3($hash, 1 ,"HMUARTLGW ${name} Ack with invalid 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, 3, "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 { 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"); HMUARTLGW_Reopen($hash); return; } } elsif ($msg =~ m/^04(..)/) { my $ack = $1; if ($ack eq HMUARTLGW_ACK_INFO && $hash->{DevState} == HMUARTLGW_STATE_QUERY_APP) { my $running = pack("H*", substr($msg, 4)); Log3($hash, 3, "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 { $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->{RetryCnt}++; RemoveInternalTimer($hash); unshift @{$hash->{Helper}{PendingCMD}}, $oldMsg; $hash->{DevState} = HMUARTLGW_STATE_SEND_TIMED; InternalTimer(gettimeofday()+0.2, "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->{RetryCnt}++; RemoveInternalTimer($hash); unshift @{$hash->{Helper}{PendingCMD}}, $oldMsg; $hash->{DevState} = HMUARTLGW_STATE_SEND_TIMED; InternalTimer(gettimeofday()+0.2, "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}"); my $wait = 0; if (!(hex($flags) & (1 << 5))) { #!BIDI $wait = 0.090; } else { $wait = 0; } $wait -= $hash->{Helper}{RoundTrip}{Delay} if (defined($hash->{Helper}{RoundTrip}{Delay})); $modules{CUL_HM}{defptr}{$src}{helper}{io}{nextSend} = $recvtime + $wait if ($modules{CUL_HM}{defptr}{$src} && $wait > 0); 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 ($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) < 7); #len len dst cnt cmd 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) { #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}; 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, }; $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, }; 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 ($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", 20); #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", }; 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. 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]->{RetryCnt} += 5; $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", 1); } elsif ($hash->{DevState} != HMUARTLGW_STATE_RUNNING) { Log3($hash, 1, "HMUARTLGW ${name} did not respond, reopening"); HMUARTLGW_Reopen($hash); } return; } sub HMUARTLGW_Get($@) { } 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 " . join(" ", sort keys %sets) if(!defined($sets{$cmd})); 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", 1); 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, 1); } elsif ($cmd eq "reopen") { HMUARTLGW_Reopen($hash); } elsif($cmd eq "close") { HMUARTLGW_Undefine($hash, $name); readingsSingleUpdate($hash, "state", "closed", 1); $hash->{XmitOpen} = 0; } elsif($cmd eq "open") { DevIo_OpenDev($hash, 0, "HMUARTLGW_DoInit"); } elsif($cmd eq "restart") { HMUARTLGW_send($hash, HMUARTLGW_OS_CHANGE_APP, HMUARTLGW_DST_OS); } 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:"")); 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*', 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") { my $dutyCycle = 1; if ($cmd eq "set") { return "wrong syntax: dutyCycle must be 1 or 0" if ($aVal !~ m/^[01]$/); $attr{$name}{$aName} = $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 100" if ($aVal !~ m/^\d+$/ || $aVal < 1 || $aVal > 100); $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; } } elsif ($aName eq "logIDs") { if ($cmd eq "set") { my @ids = split(/,/, $aVal); return "wrong syntax: logIDs can only contain hmIDs, \"sys\" and \"all\"" if (grep(!/^([\dabcdef]{6}|sys|all)$/i, @ids)); $hash->{Helper}{log} = \@ids; $attr{$name}{$aName} = $aVal; } else { delete $attr{$name}{$aName}; delete $hash->{Helper}{log}; } } 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, 0); $cond = "ok"; #FIXME: Dynamic ;evels 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"; } 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->{msgLoadCurrentRaw})) || $hash->{msgLoadCurrentRaw} != $load) { $hash->{msgLoadCurrentRaw} = $load; $hash->{msgLoadCurrent} = $adjustedLoad; HMUARTLGW_updateCondition($hash); } } sub HMUARTLGW_send($$$) { my ($hash, $msg, $dst) = @_; 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); } else { $log = sprintf("%02X", $dst). " ".uc($msg); $v = HMUARTLGW_getVerbLvl($hash, undef, undef, 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), 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); 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}); Log3($hash, HMUARTLGW_getVerbLvl($hash, undef, undef, 5), "HMUARTLGW ${name} send (".length($msg)."): ". $msg =~ s/\r\n//r); $msg = HMUARTLGW_encrypt($hash, $msg) if ($hash->{crypto} && !($msg =~ m/^V/)); $hash->{CNT} = ($hash->{CNT} + 1) & 0xff; DevIo_SimpleWrite($hash, $msg, 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 = ''; my $ks = pack("H*", $hash->{crypto}{encrypt}{keystream}); my $ct = pack("H*", $hash->{crypto}{encrypt}{ciphertext}); while(length($plaintext)) { if(length($ks)) { my $len = length($plaintext); $len = length($ks) if (length($ks) < $len); my $ppart = substr($plaintext, 0, $len, ''); my $kpart = substr($ks, 0, $len, ''); $ct .= $ppart ^ $kpart; $ciphertext .= $ppart ^ $kpart; } else { $ks = $hash->{crypto}{cipher}->encrypt($ct); $ct=''; } } $hash->{crypto}{encrypt}{keystream} = unpack("H*", $ks); $hash->{crypto}{encrypt}{ciphertext} = unpack("H*", $ct); $ciphertext; } sub HMUARTLGW_decrypt($$) { my ($hash, $ciphertext) = @_; my $plaintext = ''; my $ks = pack("H*", $hash->{crypto}{decrypt}{keystream}); my $ct = pack("H*", $hash->{crypto}{decrypt}{ciphertext}); while(length($ciphertext)) { if(length($ks)) { my $len = length($ciphertext); $len = length($ks) if (length($ks) < $len); my $cpart = substr($ciphertext, 0, $len, ''); my $kpart = substr($ks, 0, $len, ''); $ct .= $cpart; $plaintext .= $cpart ^ $kpart; } else { $ks = $hash->{crypto}{cipher}->encrypt($ct); $ct=''; } } $hash->{crypto}{decrypt}{keystream} = unpack("H*", $ks); $hash->{crypto}{decrypt}{ciphertext} = unpack("H*", $ct); $plaintext; } sub HMUARTLGW_getVerbLvl($$$$) { my ($hash, $src, $dst, $def) = @_; $hash = $hash->{lgwHash} if (defined($hash->{lgwHash})); return (grep /^sys$/i, @{$hash->{Helper}{log}}) ? 0 : $def if ((!defined($src)) || (!defined($dst))); return (grep /^($src|$dst|all)$/i, @{$hash->{Helper}{log}}) ? 0 : $def; } 1; =pod =begin html

HMUARTLGW

=end html =cut