diff --git a/FHEM/00_KNXIO.pm b/FHEM/00_KNXIO.pm new file mode 100644 index 000000000..07c65797d --- /dev/null +++ b/FHEM/00_KNXIO.pm @@ -0,0 +1,1263 @@ +############################################## +# $Id$ +# new base module for KNX-communication +# idea: merge some functions of TUL- & KNXTUL-module into one and add more connectivity +# function: M - multicast support (like in KNXTUL) - connect to knxd or KNX-router +# T - tunnel (like TCP-mode of TUL) - connect to knxd +# S - Socket mode - connect to unix-socket of knxd on localhost +# H - unicast udp - connect to knxd or KNX-router +# X - dummy mode as placeholder for FHEM2FHEM-IO +# will never be supported: direct USB-connection to a TUL-USB-stick ( use TUL-Modul or use KNXD->USBstick) +# features: use DevIo where possible +# use TcpServerUtils for multicast +# FIFO - queing of incoming messages (less latency for fhem-system) read= ~4ms vs ~34ms with KNXTUL/TUL +# discard duplicate incoming messages +# more robust parser of incoming messages +# +############################################## +### changelog: +# 19/10/2021 01.60 initial beta version +# enable hostnames for mode H & T +# 05/11/2021 fix 'x' outside of string in unpack at ./FHEM/00_KNXIO.pm line 420 (Connection response) +# 30/11/2021 add long text to errorlist +# add Log msg on invalid/discarded frames +# 15/12/2021 01.70 changed acpi decoding in cemi +# add keepalive for mode-H +# add support for FHEM2FHEM (mode X) +# fix reopen on knxd crash/restart +# 28/02/2022 change MC support to TcpServerUtils - no need for IO::Socket::Multicast Module! +# modified phyaddr internal- removed phyaddrnum +# moved all hidden internals to $hash->{KNXIOhelpers}->{...} +# 31/03/2022 fixed typo on line 1106 (exits -> exists) +# 25/04/2022 changed 'state connected' logic (issue after handshake complete...) + + +package FHEM::KNXIO; ## no critic 'package' + +use strict; +use warnings; +use IO::Socket; +use TcpServerUtils; +use English qw(-no_match_vars); +use Time::HiRes qw(gettimeofday); +use DevIo qw(DevIo_OpenDev DevIo_SimpleWrite DevIo_SimpleRead DevIo_CloseDev DevIo_Disconnected DevIo_IsOpen); +use English qw(-no_match_vars); +use GPUtils qw(GP_Import GP_Export); # Package Helper Fn + +### perlcritic parameters +# these ones are NOT used! (constants,Policy::Modules::RequireFilenameMatchesPackage,Modules::RequireVersionVar,NamingConventions::Capitalization) +### the following percritic items will be ignored global ### +## no critic (ValuesAndExpressions::RequireNumberSeparators,ValuesAndExpressions::ProhibitMagicNumbers) +## no critic (RegularExpressions::RequireDotMatchAnything,RegularExpressions::RequireLineBoundaryMatching) +## no critic (ControlStructures::ProhibitPostfixControls) +### no critic (ControlStructures::ProhibitCascadingIfElse) +## no critic (Documentation::RequirePodSections) + +### import FHEM functions / global vars +### run before package compilation +BEGIN { + # Import from main context + GP_Import( + qw(readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsBeginUpdate readingsEndUpdate + Log3 + AttrVal ReadingsVal ReadingsNum setReadingsVal + AssignIoPort IOWrite + CommandDefine CommandDelete CommandModify CommandDefMod + DevIo_OpenDev DevIo_SimpleWrite DevIo_SimpleRead DevIo_CloseDev DevIo_Disconnected DevIo_IsOpen + TcpServer_Open TcpServer_SetLoopbackMode TcpServer_MCastAdd TcpServer_MCastRemove TcpServer_MCastSend TcpServer_MCastRecv TcpServer_Close + DoTrigger + Dispatch + defs modules attr + HttpUtils_gethostbyname ip2str + readingFnAttributes + selectlist readyfnlist + InternalTimer RemoveInternalTimer + init_done + IsDisabled IsDummy IsDevice + deviceEvents devspec2array + AnalyzePerlCommand EvalSpecials + TimeNow) + ); + +# export to main context +GP_Export(qw(Initialize + KNXIO_openDev KNXIO_init KNXIO_callback + ) + ); +} + + +##################################### +# global vars/constants +my $PAT_IP = '[\d]{1,3}(\.[\d]{1,3}){3}'; +my $PAT_PORT = '[\d]{4,5}'; +my $TULID = 'C'; +my $reconnectTO = 30; #01.70 Waittime after disconnect + +##################################### +sub Initialize { + my $hash = shift; + $hash->{DefFn} = \&KNXIO_Define; +# $hash->{SetFn} = "KNXIO_Set"; +# $hash->{GetFn} = "KNXIO_Get"; + $hash->{AttrFn} = \&KNXIO_Attr; + $hash->{ReadFn} = \&KNXIO_Read; + $hash->{ReadyFn} = \&KNXIO_Ready; +# $hash->{NotifyFn} = \&KNXIO_Notify; # no need for... + $hash->{WriteFn} = \&KNXIO_Write; + $hash->{UndefFn} = \&KNXIO_Undef; + $hash->{ShutdownFn} = \&KNXIO_Shutdown; +# $hash->{DeleteFn} = "KNXIO_Delete"; +# $hash->{DelayedShutdownFn} = "KNXIO_DelayedShutdown; +# $hash->{FingerprintFn} = \&KNXIO_FingerPrint; # temp disabled + + $hash->{AttrList} = "disable:1 verbose:1,2,3,4,5"; + $hash->{Clients} = "KNX"; + $hash->{MatchList} = { "1:KNX" => "^C.*" }; + + return; +} + +##################################### +### syntax: define KNXIO : +sub KNXIO_Define { + my $hash = shift; + my $def = shift; + my @arg = split(/[\s\t\n]+/x,$def); + my $name = $arg[0] // return "KNXIO: no name specified"; + + if (scalar(@arg >=3) && $arg[2] eq 'X') { #01.70 dummy for FHEM2FHEM configs + $hash->{NAME} = $name; + $hash->{model} = $arg[2]; + return; + } + + return q{KNXIO-define syntax: "define KNXIO : " } . "\n" . + q{ or "define KNXIO S " } if (scalar(@arg < 5)); + + my $mode = $arg[2]; + + return q{KNXIO-define: invalid mode specified, valid modes are one of: H M S T} if ($mode !~ /[MSHT]/ix); + $hash->{model} = $mode; # use it also for fheminfo statistics + + my ($host,$port) = split(/[:]/ix,$arg[3]); + + return q{KNXIO-define: invalid ip-address or port, correct syntax is: "define KNXIO : "} if ($mode =~ /[MHT]/ix && $port !~ /$PAT_PORT/ix); + + if (exists($hash->{OLDDEF})) { # modify definition.... + KNXIO_closeDev($hash); + } + + if ($mode eq q{M}) { # multicast + my $host1 = (split(/\./ix,$host))[0]; + return q{KNXIO-define: Multicast address is not in the range of 224.0.0.0 and 239.255.255.255 (default is 224.0.23.12:3671) } if ($host1 < 224 || $host1 > 239); + $hash->{DeviceName} = $host . q{:} . $port; + } + elsif ($mode eq q{S}) { + $hash->{DeviceName} = 'UNIX:STREAM:' . $host; # $host= path to socket + } + elsif ($mode =~ m/[HT]/ix) { + if ($host !~ /$PAT_IP/ix) { # not an ip-address, lookup name +=pod + # blocking variant ! + my $phost = inet_aton($host); + return "KNXIO_define: host name $host could not be resolved" if (! defined($phost)); + $host = inet_ntoa($phost); + return "KNXIO_define: host name could not be resolved" if (! defined($host)); +=cut + # do it non blocking! - use HttpUtils to resolve hostname + $hash->{PORT} = $port; # save port... + $hash->{timeout} = 5; # TO for DNS req. + $hash->{DNSWAIT} = 1; + my $KNXIO_DnsQ = HttpUtils_gethostbyname($hash,$host,1,\&KNXIO_gethostbyname_Cb); + } + + else { + $hash->{DeviceName} = $host . q{:} . $port; # for DevIo + } + } + + $hash->{NAME} = $name; + my $phyaddr = (defined($arg[4]))?$arg[4]:'0.0.0'; + $hash->{PhyAddr} = sprintf('%05x',KNXIO_hex2addr($phyaddr)); + + KNXIO_closeDev($hash) if ($init_done); + + $hash->{PARTIAL} = q{}; + # define helpers + $hash->{KNXIOhelper}->{FIFO} = q{}; # read fifo + $hash->{KNXIOhelper}->{FIFOTIMER} = 0; + $hash->{KNXIOhelper}->{FIFOMSG} = q{}; + + # Devio-parameters + $hash->{nextOpenDelay} = $reconnectTO; + + delete $hash->{NEXT_OPEN}; + RemoveInternalTimer($hash); + + Log3($name, 3, "KNXIO_define: opening device $name mode=$mode"); + + return InternalTimer(gettimeofday() + 0.2,\&KNXIO_openDev,$hash); +} + +##################################### +sub KNXIO_Attr { + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $defs{$name}; + if ($aName eq 'disable') { + if ($cmd eq 'set' && defined($aVal) && $aVal == 1) { + KNXIO_closeDev($hash); + } else { + CommandModify(undef, "-silent $name $hash->{DEF}"); # do a defmod ... + } + } + return; +} + +##################################### +sub KNXIO_Read { + my $hash = shift; +# my $local = shift; #? + + my $name = $hash->{NAME}; + my $mode = $hash->{model}; + + return if IsDisabled($name); + + my $buf = undef; + if ($mode eq 'M') { + my ($rhost,$rport) = TcpServer_MCastRecv($hash, $buf, 1024); + } else { + $buf = DevIo_SimpleRead($hash); + } + if (!defined($buf) || length($buf) == 0) { + Log3($name,1, 'KNXIO_Read: no data - disconnect'); + KNXIO_disconnect($hash); + return; + } + + Log3( $name, 5, 'KNXIO_Read: buf= ' . unpack('H*',$buf)); + + ### process in indiv. subs + my $readmodes = { + H => \&KNXIO_ReadH, + S => \&KNXIO_ReadST, + T => \&KNXIO_ReadST, + M => \&KNXIO_ReadM, + }; + + if (ref $readmodes->{$mode} eq 'CODE') { + $readmodes->{$mode}->($hash, $buf); + return; + } + + Log3 $name, 2,"KNXIO_Read failed - invalid mode $mode specified"; + return; +} + +### Socket & Tunnel read +sub KNXIO_ReadST { + my $hash = shift; + my $buf = shift; + my $name = $hash->{NAME}; + + $hash->{PARTIAL} .= $buf; + my $msglen = unpack('n',$hash->{PARTIAL}) + 2; + + return if (length($hash->{PARTIAL}) < $msglen); # not enough data + + # buf complete, continue + my @que = (); + @que = @{$hash->{KNXIOhelper}->{FIFO}} if (defined($hash->{KNXIOhelper}->{FIFO}) && ($hash->{KNXIOhelper}->{FIFO} ne q{})); #get que from hash + while (length($hash->{PARTIAL}) >= $msglen) { + $buf = substr($hash->{PARTIAL},0,$msglen); # get one msg from partial + $hash->{PARTIAL} = substr($hash->{PARTIAL}, $msglen); # put rest to partial + + Log3($name, 5,'KNXIO_Read Rawbuf: ' . unpack('H*',$buf)); + + my $outbuf = KNXIO_decodeEMI($hash,$buf); + if ( defined($outbuf) ) { + push(@que,$outbuf); # only valid packets! + } + if (length($hash->{PARTIAL}) >= 2) { + $msglen = unpack('n',$hash->{PARTIAL}) + 2; + } + } # /while + $hash->{KNXIOhelper}->{FIFO} = \@que; # push que to fifo + return KNXIO_processFIFO($hash); +} + +### multicast read +sub KNXIO_ReadM { + my $hash = shift; + my $buf = shift; + my $name = $hash->{NAME}; + + $buf = $hash->{PARTIAL} . $buf if (defined($hash->{PARTIAL})); + if (length($buf) < 6) { # min required for first unpack + $hash->{PARTIAL} = $buf; + return; + } + + # header format: 0x06 - header size / 0x10 - KNXNET-IPVersion / 0x0530 - Routing Indicator / 0xYYYY - Header size + size of cEMIFrame + my ($header, $header_routing, $total_length) = unpack("nnn",$buf); + + Log3 $name, 5, 'KNXIO_Read: -header=' . sprintf("%04x",$header) . ', -routing=' . sprintf("%04x",$header_routing) . ", TotalLength= $total_length \(dezimal\)" ; + + if ($header != 0x0610 ) { + Log3($name, 1, 'KNXIO_Read: invalid header size or version'); + $hash->{PARTIAL} = undef; # delete all we have so far +# KNXIO_disconnect($hash); #? + return; + } + if (length($buf) < $total_length) { # 6 Byte header + min 11 Byte data + Log3($name,4, 'KNXIO_Read: still waiting for complete packet (short packet length)'); + $hash->{PARTIAL} = $buf; # still not enough + return; + } + else { + $hash->{PARTIAL} = substr($buf,$total_length); + $buf = substr($buf,0,$total_length); + } + +###temp disabled $buf = KNXIO_chkDupl2($hash,$buf); + + ##### now, the buf is complete check if routing-Frame + if (($header_routing == 0x0530) && ($total_length >= 17)) { # 6 Byte header + min 11 Byte data + # this is the correct frame type, process it now + Log3($name, 5,'KNXIO_Read Rawbuf: ' . unpack('H*',$buf)); + + $buf = substr($buf,6); # strip off header + my $cemiRes = KNXIO_decodeCEMI($hash,$buf); + return if (! defined($cemiRes)); + return KNXIO_dispatch($hash,$cemiRes); + } + elsif ($header_routing == 0x0531) { # routing Lost Message + Log3 $name, 3, 'KNXIO_Read: a routing-lost packet was received !!! - Problems with bus or KNX-router ???'; + return; + } + elsif ($header_routing == 0x0201) { # search request + return; # ignore with silence + } + else { + Log3 $name, 4, 'KNXIO_Read: a packet with unsupported service type ' . sprintf("%04x",$header_routing) . ' was received. - discarded'; + return; + } + +} # /multicast + +##################################### +### host mode read +# packet 06 10 0206 0014 02 00 08 01 c0a8 0ae8 0e 57 04 04 00f7 - conn response: 0014 - total len / 02 - commchannel / 00 - statusoode / 08 - struc-len / 01 protocol=UDP IPV4 / xxxx xxxx - IP & port / +# packet 06 10 0209 0010 02 01 08 01 c0a8 0ae8 0e 57 - disconn requ! +# header format: 0x06 - header size / 0x10 - KNXNET-IPVersion / 0x0201 - type / 08 - struct length / 01 - protocol=UDP IPV4 / size of cEMIFrame +sub KNXIO_ReadH { + my $hash = shift; + my $buf = shift; + + my $name = $hash->{NAME}; + + if ( unpack('n',$buf) != 0x0610) { + Log3($name, 3, 'KNXIO_Read: invalid Frame Header received - discarded'); + return; + } + + my $msg = undef; # holds data to send + my $ccid = 0; + my $rxseqcntr = 0; + my $txseqcntr = 0; + my $errcode = 0; + my $responseID = unpack('x2n',$buf); + + # handle most frequent id's first + if ( $responseID == 0x0420) { # Tunnel request + ($ccid,$rxseqcntr) = unpack('x7CC',$buf); + + my $discardFrame = undef; + if ($rxseqcntr == ($hash->{KNXIOhelper}->{SEQUENCECNTR} - 1)) { + Log3($name, 3, 'KNXIO_Read: TunnelRequest received: duplicate message received (seqcntr=' . $rxseqcntr .') - ack it'); + $hash->{KNXIOhelper}->{SEQUENCECNTR}--; # one packet duplicate... we ack ist but do not process + $discardFrame = 1; + } + if ($rxseqcntr != $hash->{KNXIOhelper}->{SEQUENCECNTR}) { # really out of sequence + Log3($name, 3, 'KNXIO_Read: TunnelRequest received: out of sequence, (seqcntrRx=' . $rxseqcntr . ' (seqcntrTx=' . $hash->{KNXIOhelper}->{SEQUENCECNTR} . '- no ack & discard'); + return; + } + Log3($name, 4, 'KNXIO_Read: TunnelRequest received - send Ack and decode. seqcntrRx=' . $hash->{KNXIOhelper}->{SEQUENCECNTR} ) if (! defined($discardFrame)); + my $tacksend = pack('nnnCCCC',0x0610,0x0421,10,4,$ccid,$hash->{KNXIOhelper}->{SEQUENCECNTR},0); # send ack + $hash->{KNXIOhelper}->{SEQUENCECNTR}++; + $hash->{KNXIOhelper}->{SEQUENCECNTR} = 0 if ($hash->{KNXIOhelper}->{SEQUENCECNTR} > 255); + DevIo_SimpleWrite($hash,$tacksend,0); + + return if ($discardFrame); # duplicate frame + + #now decode & send to clients + Log3($name, 5,'KNXIO_Read Rawbuf: ' . unpack('H*',$buf)); + + $buf = substr($buf,10); # strip off header (10 bytes) + my $cemiRes = KNXIO_decodeCEMI($hash,$buf); + return if (! defined($cemiRes)); + + return KNXIO_dispatch($hash,$cemiRes); + } + elsif ( $responseID == 0x0421) { # Tunneling Ack + ($ccid,$txseqcntr,$errcode) = unpack('x7CCC',$buf); + if ($errcode > 0) { + Log3($name, 3, 'KNXIO_Read: Tunneling Ack received ' . 'CCID=' . $ccid . ' txseq=' . $txseqcntr . (($errcode)?' - Status= ' . KNXIO_errCodes($errcode):q{})); +#what next ? + } + + $hash->{KNXIOhelper}->{SEQUENCECNTR_W}++; + $hash->{KNXIOhelper}->{SEQUENCECNTR_W} = 0 if ($hash->{KNXIOhelper}->{SEQUENCECNTR_W} > 255); + RemoveInternalTimer($hash,\&KNXIO_TunnelRequestTO); # all ok, stop timer + } + elsif ( $responseID == 0x0202) { # Search response + Log3($name, 4, 'KNXIO_Read: SearchResponse received'); + my (@contolpointIp, $controlpointPort) = unpack('x6CCCn',$buf); +#Log3 $name, 5, "H-read cpIP= $contolpointIp[0]\.$contolpointIp[1]\.$contolpointIp[2]\.$contolpointIp[3] port= $controlpointPort"; + } + elsif ( $responseID == 0x0204) { # Decription response + Log3($name, 4, 'KNXIO_Read: DescriptionResponse received'); + } + elsif ( $responseID == 0x0206) { # Connection response + ($hash->{KNXIOhelper}->{CCID},$errcode) = unpack('x6CC',$buf); # save Comm Channel ID,errcode + + RemoveInternalTimer($hash,\&KNXIO_keepAlive); + if ($errcode > 0) { + Log3($name, 3, 'KNXIO_Read: ConnectionResponse received ' . 'CCID=' . $hash->{KNXIOhelper}->{CCID} . ' Status=' . KNXIO_errCodes($errcode)); + KNXIO_disconnect($hash); + return; + } + my $phyaddr = unpack('x18n',$buf); + $hash->{PhyAddr} = sprintf('%05x',$phyaddr); # correct Phyaddr. + + readingsSingleUpdate($hash, "state", "connected", 1); + + InternalTimer(gettimeofday() + 60, \&KNXIO_keepAlive, $hash); # start keepalive + + $hash->{KNXIOhelper}->{SEQUENCECNTR} = 0; + } + elsif ( $responseID == 0x0208) { # ConnectionState response + ($hash->{KNXIOhelper}->{CCID}, $errcode) = unpack('x6CC',$buf); + RemoveInternalTimer($hash,\&KNXIO_keepAlive); + RemoveInternalTimer($hash,\&KNXIO_keepAliveTO); # reset timeout timer + if ($errcode > 0) { + Log3($name, 3, 'KNXIO_Read: ConnectionStateResponse received ' . 'CCID=' . $hash->{KNXIOhelper}->{CCID} . ' Status=' . KNXIO_errCodes($errcode)); + KNXIO_disconnect($hash); + return; + } + InternalTimer(gettimeofday() + 60, \&KNXIO_keepAlive, $hash); + } + elsif ( $responseID == 0x0209) { # Disconnect request + Log3($name, 4, 'KNXIO_Read: DisconnectRequest received, restarting connenction'); + + $ccid = unpack('x6C',$buf); + $msg = pack('nnnCC',(0x0610,0x020A,8,$ccid,0)); + DevIo_SimpleWrite($hash,$msg,0); # send disco response + + $msg = KNXIO_prepareConnRequ($hash); + } + elsif ( $responseID == 0x020A) { # Disconnect response + Log3($name, 4, 'KNXIO_Read: DisconnectResponse received - sending connrequ'); +#$attr{$name}{verbose} = 3; # temp + $msg = KNXIO_prepareConnRequ($hash); + } + else { + Log3 $name, 3, 'KNXIO_Read: invalid response received: ' . unpack('H*',$buf); + return; + } + + DevIo_SimpleWrite($hash,$msg,0) if(defined($msg)); # send msg + return; +} + +##################################### +sub KNXIO_Ready { + my $hash = shift; + my $name = $hash->{NAME}; + + return if (! $init_done || exists($hash->{DNSWAIT}) || IsDisabled($name) == 1); + return if (exists($hash->{NEXT_OPEN}) && $hash->{NEXT_OPEN} > gettimeofday()); #01.70 avoid open loop +# return KNXIO_openDev($hash) if(ReadingsVal($hash, 'state', q{}) ne 'connected'); + return KNXIO_openDev($hash) if(ReadingsVal($hash, 'state', 'disconnected') eq 'disconnected'); + return; +} + +##################################### +sub KNXIO_Write { + my $hash = shift; + my $fn = shift; + my $msg = shift; + + my $name = $hash->{NAME}; + my $mode = $hash->{model}; + + Log3 $name, 5, 'KNXIO_write: started'; + return if(!defined($fn) && $fn ne $TULID); + if ($hash->{STATE} ne 'connected') { + Log3 $name, 3, "KNXIO_write called while not connected! Msg: $msg lost"; + return; + } +# return if( ReadingsVal($name,'state','connected') eq 'disconnected'); + + Log3 $name, 5, "KNXIO_write: sending $msg"; + + my $acpivalues = {r => 0x00, p => 0x01, w => 0x02}; + + if ($msg =~ /^([rwp])([0-9a-f]{5})(.*)$/ix) { # msg format: + + my $acpi = $acpivalues->{$1}<<6; + my $tcf = ($acpivalues->{$1}>>2 & 0x03); + my $dst = KNXIO_hex2addr($2); + my $str = $3; + my $src = KNXIO_hex2addr($hash->{PhyAddr}); + + #convert hex-string to array with dezimal values + my @data = map {hex()} $str =~ /(..)/xg; # PBP 9/2021 + $data[0] = 0 if (scalar(@data) == 0); # in case of read !! + my $datasize = scalar(@data); + + if ($datasize == 1) { + $data[0] = ($data[0] & 0x3F) | $acpi; + } + else { + $data[0] = $acpi; + } + + Log3 $name, 5, q{KNXIO_Write: str/size/acpi/src/dst= } . unpack('H*',@data) . q{/} . $datasize . q{/} . $acpi . q{/} . unpack("H*",$src) . q{/} . unpack("H*",$dst); + my $completemsg = q{}; + my $ret = 0; + + if ($mode =~ /^[ST]$/ix ) { #format: size | 0x0027 | dst | 0 | data + $completemsg = pack('nnnCC*',$datasize + 5,0x0027,$dst,0,@data); + } + elsif ($mode eq 'M') { + $completemsg = pack('nnnnnnnCCC*',0x0610,0x0530,$datasize + 16,0x2900,0xBCE0,0,$dst,$datasize,0,@data); + $ret = TcpServer_MCastSend($hash,$completemsg); # new TcpServerUtils + } + else { # $mode eq 'H' + # total length= $size+20 - include 2900BCEO,src,dst,size,0 + $completemsg = pack('nnnCCCCnnnnCCC*',0x0610,0x0420,$datasize + 20,4,$hash->{KNXIOhelper}->{CCID},$hash->{KNXIOhelper}->{SEQUENCECNTR_W},0,0x1100,0xBCE0,0,$dst,$datasize,0,@data); # send TunnelInd + + # Timeout function - expect TunnelAck within 1 sec! - but if fhem has a delay.... + $hash->{KNXIOhelper}->{LASTSENTMSG} = $completemsg; # save msg for resend in case of TO + InternalTimer(gettimeofday() + 1.5, \&KNXIO_TunnelRequestTO, $hash); + } + + $ret = DevIo_SimpleWrite($hash,$completemsg,0) if ($mode ne 'M'); + Log3 $name, 4, 'KNXIO_Write: Mode=' . $mode . ' buf=' . unpack('H*',$completemsg) . " rc=$ret"; + return; + } + Log3 $name, 2, 'KNXIO_write: Could not send message ' . $msg; + return; +} + +##################################### +sub KNXIO_Undef { + my $hash = shift; + my $name = shift; + + return KNXIO_Shutdown($hash); +} + +################################### +sub KNXIO_Shutdown { + my $hash = shift; + + my $mode = $hash->{model}; + if ($mode eq 'M') { + TcpServer_Close($hash); + } else { + KNXIO_closeDev($hash); + } + RemoveInternalTimer($hash); + return; +} + +################################### +### check for duplicate msgs +# not used ! +sub KNXIO_FingerPrint { + my $ioname = shift; + my $buf = shift; + my $mode = $defs{$ioname}->{model}; + + substr( $buf, 1, 5, "-----" ); # ignore src addr +# Log3 $ioname, 5, 'KNXIO_Fingerprint: ' . $buf; +# return ( $ioname, $buf ); # ignore src addr + return ( q{}, $buf ); # ignore ioname & src addr +} + +################################### +### functions called from DevIo ### +################################### + +### return from open (sucess/failure) +sub KNXIO_callback { + my $hash = shift; + my $err = shift; + + $hash->{nextOpenDelay} = $reconnectTO; + if (defined($err)) { + Log3 $hash, 2, "KNXIO_callback: device open $hash->{NAME} failed with: $err" if ($err); + $hash->{NEXT_OPEN} = gettimeofday() + $hash->{nextOpenDelay}; + } + return; +} + +################################### +######## private functions ######## +################################### + +### called from define-HttpUtils_gethostbynam when hostname needs to be resolved +### process callback from HttpUtils_gethostbyname +sub KNXIO_gethostbyname_Cb { + my $hash = shift; + my $error = shift; + my $dhost = shift; + + my $name = $hash->{NAME}; + delete $hash->{timeout}; + delete $hash->{DNSWAIT}; + if ($error) { + delete $hash->{DeviceName}; + delete $hash->{PORT}; + Log3($name, 1, "KNXIO_define: hostname could not be resolved: $error"); + return "KNXIO_define: hostname could not be resolved: $error"; + } + my $host = ip2str($dhost); + Log3($name, 3, "KNXIO_define: DNS query result= $host"); + $hash->{DeviceName} = $host . q{:} . $hash->{PORT}; + delete $hash->{PORT}; + return; +} + +### called from define - after init_complete +### return undef on success +sub KNXIO_openDev { + my $hash = shift; + my $name = $hash->{NAME}; + my $mode = $hash->{model}; + + return if (IsDisabled($name) == 1); + + if (exists $hash->{DNSWAIT}) { + $hash->{DNSWAIT} += 1; + if ($hash->{DNSWAIT} > 5) { + Log3 ($name, 2, "KNXIO_openDev: $name - DNS failed, check ip/hostname"); + return; # "KNXIO_openDev: $name - DNS failed, check ip/hostname"; + } + InternalTimer(gettimeofday() + 1,\&KNXIO_openDev,$hash); + Log3 ($name, 2, "KNXIO_openDev: waiting for DNS"); + return; # waiting for DNS + } + return if (! exists($hash->{DeviceName})); # DNS failed ! + + my $reopen = (exists($hash->{NEXT_OPEN}))?1:0; # + my $param = $hash->{DeviceName}; # (connection-code):ip:port or socket param + my ($ccode, $host, $port) = split(/[:]/ix,$param); + if (! defined($port)) { + $port = $host; + $host = $ccode; + $ccode = undef; + } + $host = $port if ($param =~ /UNIX:STREAM:/ix); + + Log3 ($name, 5, "KNXIO_openDev: $mode, $host, $port, reopen=$reopen"); + + my $ret = undef; # result + + ### multicast support via TcpServerUtils ... + if ($mode eq 'M') { + delete $hash->{TCPDev}; # devio ? + $ret = TcpServer_Open($hash, $port, $host, 1); + if (defined($ret)) { # error + Log3 ($name, 2, "KNXIO_openDev: $name Can't connect: $ret") if(!$reopen); + } + $ret = TcpServer_MCastAdd($hash,$host); + if (defined($ret)) { # error + Log3 ($name, 2, "KNXIO_openDev: $name MC add failed: $ret") if(!$reopen); + } + + delete $hash->{NEXT_OPEN}; + delete $readyfnlist{"$name.$param"}; + $ret = KNXIO_init($hash); + } + + ### socket mode + elsif ($mode eq 'S') { + if (!(-S -r -w $host) && $init_done) { + Log3 ($name, 2, q{KNXIO_openDev: Socket not available - (knxd running?)}); + return; + } + $ret = DevIo_OpenDev($hash,$reopen,\&KNXIO_init); # no callback + } + + ### host udp + elsif ($mode eq 'H') { + my $conn = 0; + $conn = IO::Socket::INET->new(PeerAddr => "$host:$port", Type => SOCK_DGRAM, Proto => 'udp', Reuse => 1); + if (!($conn)) { + Log3 ($name, 2, "KNXIO_openDev: device $name Can't connect: $ERRNO") if(!$reopen); # PBP + $readyfnlist{"$name.$param"} = $hash; + readingsSingleUpdate($hash, "state", "disconnected", 0); + $hash->{NEXT_OPEN} = gettimeofday() + $reconnectTO; + return; + } + delete $hash->{NEXT_OPEN}; + delete $hash->{DevIoJustClosed}; # DevIo + $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1); #01.70 + $hash->{TCPDev} = $conn; + $hash->{FD} = $conn->fileno(); + delete $readyfnlist{"$name.$param"}; + $selectlist{"$name.$param"} = $hash; + if($reopen) { + Log3 ($name, 3, "KNXIO_openDev: device $name reappeared"); + } + else { + Log3 ($name, 3, "KNXIO_openDev: device $name opened"); + } + $ret = KNXIO_init($hash); + } + + ### tunneling TCP + else { # $mode eq 'T' + $ret = DevIo_OpenDev($hash,$reopen,\&KNXIO_init,\&KNXIO_callback); + } + + if(defined($ret) && $ret) { + Log3 ($name, 1, "KNXIO_openDev: Cannot open KNXIO-Device $name, ignoring it"); + KNXIO_closeDev($hash); + } + + return $ret; +} + +### called from DevIo_open or KNXIO_openDev after sucessful open +sub KNXIO_init { + my $hash = shift; + my $name = $hash->{NAME}; + my $mode = $hash->{model}; + + if ($mode =~ m/[ST]/ix) { + my $opengrpcon = pack("nnnC",(5,0x26,0,0)); # KNX_OPEN_GROUPCON + DevIo_SimpleWrite($hash,$opengrpcon,0); + } + + elsif ($mode eq 'H') { + my $connreq = KNXIO_prepareConnRequ($hash); + DevIo_SimpleWrite($hash,$connreq,0); + } + +# elsif ($mode eq 'M') { +# } + + # state 'connected' is set in decode_EMI (model ST) or in readH (model H)? + else { + +# DoTrigger($name, 'CONNECTED'); + readingsSingleUpdate($hash, "state", "connected", 1); + } + + return; +} + +### prepare connection request +### called from init, disconn response, disconn request +### returns packed string, ready for sending with DevIo +sub KNXIO_prepareConnRequ { + my $hash = shift; + + ### host protocol address information see 3.8.2 core docu + ### hdr-size | Host Prococol code (01=udp/02=TCP) | Dest-IPAddr (4bytes) | IP port (2bytes) | hpais (8) | hpaid (8) | ctype (4) +# my $hpais = pack('nC4n',(0x0801,@srchost,$srcport)); # source - can be 0 (for NAT translation ! + my $hpais = pack('nCCCCn',(0x0801,0,0,0,0,0)); # source - can be 0 (for NAT translation ! +# my $hpaid = pack('nC4n',(0x0801,@desthost,$destport)); # dest we can use port 3671 for data endpoint too! + my $hpaid = pack('nCCCCn',(0x0801,0,0,0,0,0)); # dest can be 0,0 + my $ctype = pack('CCCC',(4,4,2,0)); # 04040200 for udp tunnel_connection/Tunnel_linklayer + + my $connreq = pack('nnn',0x0610,0x0205,0x1A) . $hpais . $hpaid . $ctype; + $hash->{KNXIOhelper}->{SEQUENCECNTR} = 0; # read requests + $hash->{KNXIOhelper}->{SEQUENCECNTR_W} = 0; # write requests + RemoveInternalTimer($hash,\&KNXIO_keepAliveTO); # reset timeout timer + RemoveInternalTimer($hash,\&KNXIO_keepAlive); + + return $connreq; +} + +### handle fifo and send to KNX-Module via dispatch +# all decoding already done in decode_CEMI / decode_EMI +sub KNXIO_dispatch { + my $hash = shift; + my $buf = shift; + + my @que = (); + push (@que,$buf); + $hash->{KNXIOhelper}->{FIFO} = \@que; + + return KNXIO_processFIFO($hash); +} + +### called from FIFO TIMER or direct if FIFO disabled +sub KNXIO_dispatch2 { +# my ($hash, $outbuf ) = ($_[0]->{h}, $_[0]->{m}); + my $hash = shift; + + my $buf = $hash->{KNXIOhelper}->{FIFOMSG}; + my $name = $hash->{NAME}; + $hash->{KNXIOhelper}->{FIFOTIMER} = 0; + + $hash->{"${name}_MSGCNT"}++; + $hash->{"${name}_TIME"} = TimeNow(); + + Dispatch($hash, $buf); + + KNXIO_processFIFO($hash) if (defined($hash->{KNXIOhelper}->{FIFO}) && ($hash->{KNXIOhelper}->{FIFO} ne q{})); + return; +} + +### fetch msgs from FIFO and call dispatch +sub KNXIO_processFIFO { + my $hash = shift; + my $name = $hash->{NAME}; + + if ($hash->{KNXIOhelper}->{FIFOTIMER} != 0) { # dispatch still running, do a wait loop + Log3 $hash->{NAME}, 4, 'KNXIO_processFIFO: dispatch not complete, waiting'; + InternalTimer(gettimeofday() + 0.1, \&KNXIO_processFIFO, $hash); + return; + } + my @que = @{$hash->{KNXIOhelper}->{FIFO}}; + my $queentries = scalar(@que); + if ($queentries > 0) { # process timer is not running & fifo not empty + $hash->{KNXIOhelper}->{FIFOMSG} = shift (@que); + $hash->{KNXIOhelper}->{FIFO} = \@que; + $hash->{KNXIOhelper}->{FIFOTIMER} = 1; + Log3($name, 4, 'KNXIO_processFIFO: ' . $hash->{KNXIOhelper}->{FIFOMSG} . ' Nr_msgs: ' . $queentries); +# InternalTimer(gettimeofday() + 1.0, \&KNXIO_dispatch2, $hash); # testing delay + InternalTimer(0, \&KNXIO_dispatch2, $hash); + + # delete duplicates from queue + while ($queentries > 1) { + my $nextbuf = shift (@que); + if ($hash->{KNXIOhelper}->{FIFOMSG} eq $nextbuf) { + $hash->{KNXIOhelper}->{FIFO} = \@que; # discard it + Log3($name, 4, "KNXIO_processFIFO: - deleted duplicate msg from queue"); + } + $queentries--; + } + } + return; +} + +### +sub KNXIO_disconnect { + my $hash = shift; + my $name = $hash->{NAME}; + my $param = $hash->{DeviceName}; + + DevIo_Disconnected($hash); + + Log3 ($name, 1, "KNXIO_disconnect: device $name disconnected, waiting to reappear"); + + $readyfnlist{"$name.$param"} = $hash; # Start polling + $hash->{NEXT_OPEN} = gettimeofday() + $reconnectTO; #01.70 + + # Without the following sleep the open of the device causes a SIGSEGV, + # and following opens block infinitely. Only a reboot helps. +# sleep(5); + + return; +} + +### +sub KNXIO_closeDev { + my $hash = shift; + my $name = $hash->{NAME}; + my $param = $hash->{DeviceName}; + + if ($hash->{model} eq 'M') { + TcpServer_Close($hash); + } + else { + DevIo_CloseDev($hash); + $hash->{TCPDev}->close() if($hash->{FD}); + } + + delete $hash->{nextOpenDelay}; + delete $hash->{"${name}_MSGCNT"}; + delete $hash->{"${name}_TIME"}; + +#NO! delete $hash->{'.CCID'}; + delete $hash->{KNXIOhelper}->{SEQUENCECNTR}; + delete $hash->{KNXIOhelper}->{SEQUENCECNTR_W}; + + RemoveInternalTimer($hash); + + Log3 ($name, 5, "KNXIO_closeDev: device $name closed"); + + readingsSingleUpdate($hash, "state", "disconnected", 1); + DoTrigger($name, "DISCONNECTED"); + + return; +} + + +################################### +######## Helper functions ######## +################################### + +### format: length(2) | id(2) | srcaddr(2) | dstaddr(2) | acpi(1) | data(n) | +### input: $hash, $buf(packed) +### ret: buf - format for dispatch / undef on error +sub KNXIO_decodeEMI { + my $hash = shift; + my $buf = shift; + + my $name = $hash->{NAME}; + + my ($len, $id, $src, $dst, $acpi, @data) = unpack("nnnnCC*",$buf); + if (($len + 2) != length($buf)) { + Log3 $name, 4, 'KNXIO_decodeEMI: buffer length mismatch ' . $len . q{ } . (length($buf) - 2); + return; + } + if ($id != 0x0027) { + if ($id == 0x0026) { + Log3($name, 4, 'KNXIO_decdeEMI: OpenGrpCon response received'); + readingsSingleUpdate($hash, 'state', 'connected', 1); + } + else { + Log3($name, 3, 'KNXIO_decodeEMI: invalid message code' . sprintf("04x",$id)); + } + return; + } + $src = KNXIO_addr2hex($src,0); # always a phy-address + $dst = KNXIO_addr2hex($dst,1); # always a Group addr + + Log3($name, 4, "KNXIO_decodeEMI: src=$src - dst=$dst - leng=" . scalar(@data) . " - data=" . sprintf('%02x' x scalar(@data),@data)); + + # acpi ref: KNX System Specs 03.03.07 + $acpi = ((($acpi & 0x03) << 2) | (($data[0] & 0xC0) >> 6)); + my @acpicodes = qw(read preply write invalid); + my $rwp = $acpicodes[$acpi]; + if (! defined($rwp) || ($rwp eq 'invalid')) { + Log3($name, 4, 'KNXIO_decodeEMI: no valid acpi-code (read/reply/write) received, discard packet'); + Log3($name, 4, "discarded packet: src=$src - dst=$dst - acpi=" . sprintf('%02x',$acpi) . " - leng=" . scalar(@data) . " - data=" . sprintf('%02x' x scalar(@data),@data)); + return; + } + + $data[0] = ($data[0] & 0x3f); # 6 bit data in byte 0 + shift @data if (scalar(@data) > 1 ); # byte 0 is ununsed if length > 1 + + my $outbuf = $TULID . $src . substr($rwp,0,1) . $dst . sprintf('%02x' x scalar(@data),@data); + Log3($name, 5, 'KNXIO_decodeEMI: ' . $outbuf); + + return $outbuf; +} + + +### CEMI decode +# format: message code(1) | AddInfoLen(1) | [Addinfo(x)] | ctrl1(1) | ctrl2[1) | srcaddr(2) | dstaddr(2) | datalen(1) | tpci(1) | acpi/data(1) | [data(n) | +# input: $hash, $buf(packed) (w.o length) +# ret: buf - format for dispatch / undef on error +sub KNXIO_decodeCEMI { + my $hash = shift; + my $buf = shift; + + my $name = $hash->{NAME}; + my ($mc, $addlen) = unpack('CC',$buf); + if ($mc != 0x29 && $mc != 0x2e) { + Log3($name, 4, 'KNXIO_decodeCEMI: wrong MessageCode ' . sprintf("%02x",$mc) . ', discard packet'); + return; + } + + $addlen += 2; + my ($ctrlbyte1, $ctrlbyte2, $src, $dst, $tcf, $acpi, @data) = unpack("x" . $addlen . "CCnnCCC*",$buf); + + if (($ctrlbyte1 & 0xF0) != 0xB0) { # standard frame/no repeat/broadcast - see 03_06_03 EMI_IMI specs + Log3 $name, 4, 'KNXIO_decodeCEMI: wrong ctrlbyte1' . sprintf("%02x",$ctrlbyte1) . ', discard packet'; + return; + } + my $prio = ($ctrlbyte1 & 0x0C) >>2; # priority + my $dest_addrType = ($ctrlbyte2 & 0x80) >> 7; # MSB 0 = indiv / 1 = group + my $hop_count = ($ctrlbyte2 & 0x70) >> 4; # bits 6-4 + + if ($tcf != scalar(@data)) { # $tcf: number of NPDU octets, TPCI octet not included! + Log3 $name, 4, 'KNXIO_decodeCEMI: Datalength not consistent'; + return; + } + + $src = KNXIO_addr2hex($src,0); # always a phy-address + $dst = KNXIO_addr2hex($dst,$dest_addrType); + + Log3($name, 4, "KNXIO_decodeCEMI: src=$src - dst=$dst - destaddrType=$dest_addrType - prio=$prio - hop_count=$hop_count - leng=" . scalar(@data) . " - data=" . sprintf('%02x' x scalar(@data),@data)); + + $acpi = ((($acpi & 0x03) << 2) | (($data[0] & 0xC0) >> 6)); + my @acpicodes = qw(read preply write invalid); + my $rwp = $acpicodes[$acpi]; + if (! defined($rwp) || ($rwp eq 'invalid')) { # not a groupvalue-read/write/reply + Log3($name, 4, 'KNXIO_decodeCEMI: no valid acpi-code (read/reply/write) received, discard packet'); + Log3($name, 4, "discarded packet: src=$src - dst=$dst - destaddrType=$dest_addrType - prio=$prio - hop_count=$hop_count - leng=" . scalar(@data) . " - data=" . sprintf('%02x' x scalar(@data),@data)); + return; + } + + $data[0] = ($data[0] & 0x3f); # 6 bit data in byte 0 + shift @data if (scalar(@data) > 1 ); # byte 0 is ununsed if length > 1 + + my $outbuf = $TULID . $src . substr($rwp,0,1) . $dst . sprintf('%02x' x scalar(@data),@data); + Log3($name, 5, "KNXIO_decodeCEMI: $outbuf"); + + return $outbuf; +} + + +### convert address from number to hex-string or display name ($type=2) +sub KNXIO_addr2hex { + my $adr = shift; + my $type = shift; # 1 if GA-address, else physical address + + return sprintf('%02x%01x%02x', ($adr >> 11) & 0x1f, ($adr >> 8) & 0x7, $adr & 0xff) if ($type == 1); + return sprintf('%d.%d.%d', $adr >> 12, ($adr >> 8) & 0xf, $adr & 0xff) if ($type == 2); # for display + return sprintf('%02x%01x%02x', $adr >> 12, ($adr >> 8) & 0xf, $adr & 0xff); +} + +### convert address from hex-string (5 digits) to number +sub KNXIO_hex2addr { + my $str = shift; + + if ($str =~ m/([0-9a-f]{2})([0-9a-f])([0-9a-f]{2})/ix) { + return (hex($1) << 11) | (hex($2) << 8) | hex($3); # GA Addr + } + elsif ($str =~ m/([\d]+)\.([\d]+)\.([\d]+)/ix) { + return ($1 << 12) + ($2 << 8) + $3; # phy Addr + } + return 0; +} + +### check for duplicate messages +# check the first msg in PARTIAL against all msgs in PARTIAL, +# if match, returns last matched msg (=the most recent) and delete all other msgs. +# if no match, return first msg from PARTIAL +# +# return: buf for further processing (last matched message) +sub KNXIO_chkDupl2 { + my $hash = shift; + my $buf = shift; + my $name = $hash->{NAME}; + my $mode = $hash->{model}; + my $lenpar = length($hash->{PARTIAL}); + my $outbuf = $buf; # copy for return + + Log3 $name, 5, 'KNXIO_chkDupl- msglen: ' . length($buf) . " LENPART= " . length($hash->{PARTIAL}); + + return $buf if (length($hash->{PARTIAL}) == 0); + $hash->{helper}{BUFFER} = unpack('H*',$hash->{PARTIAL}); # debugging partial + + if ($mode =~ /[ST]/x) { + # msgformat: 0008 0027 00c8 1c0c 00 80 + substr( $buf, 4, 2, q{..} ); # prepare reqex for duplicate test (ignore src addr) + my $bufrepl = substr(q{..................},0,length($buf) - 9); # ignore data-values in PARTIAL + substr( $buf, 9 ,(length($buf) - 9),$bufrepl); # ignore data-values in PARTIAL + } + elsif ($mode eq 'M') { + # msgformat: 0610 0530 0011 2900 bcc0 00c8 1c0c 01 00 81 + substr( $buf, 9, 3, '...' ); # prepare reqex for duplicate test (ignore hop count & src addr) + } + else { + return $outbuf; # unchanged + } + $buf =~ s/([)]|[(])/./gx; # mask )( + my $re = qr{$buf}; ## no critic 'RegularExpressions' + + Log3 $name, 1, 'KNXIO_chkDupl: regex-r= ' . unpack('H*', $buf); + Log3 $name, 1, 'KNXIO_chkDupl: PARTIAL= ' . unpack('H*', $hash->{PARTIAL}); + + my (@match) = ($hash->{PARTIAL} =~ m/${re}/gix); + my $nrmatches = scalar(@match); + if ($nrmatches > 0) { # we have duplicates + $outbuf = $match[scalar(@match)-1]; # use the last one + $hash->{PARTIAL} =~ s/${re}//gx; # delete duplicate packets + } + Log3 $name, 4, 'KNXIO_chkDupl: nrDuplicates= ' . $nrmatches . ' LengthPartial= ' . length($hash->{PARTIAL}) . ' Buf-in(masked)= ' . unpack('H*', $buf) . ' Buf-out= ' . unpack('H*', $outbuf); + return $outbuf; +} + + +### keep alive for mode H - every minute +# triggered on conn-response & +sub KNXIO_keepAlive { + my $hash = shift; + my $name = $hash->{NAME}; + + Log3($name, 4, 'KNXIO_keepalive - expect ConnectionStateResponse'); + + my $msg = pack('nnnCCnnnn',(0x0610,0x0207,16,$hash->{KNXIOhelper}->{CCID},0, 0x0801,0,0,0)); + RemoveInternalTimer($hash,\&KNXIO_keepAlive); + DevIo_SimpleWrite($hash,$msg,0); # send conn state requ + InternalTimer(gettimeofday() + 2,\&KNXIO_keepAliveTO,$hash); # set timeout timer - reset by ConnectionStateResponse + return; +} + +### keep alive timeout +sub KNXIO_keepAliveTO { + my $hash = shift; + my $name = $hash->{NAME}; + + Log3($name, 4, 'KNXIO_keepAlive timeout - retry'); + + return KNXIO_keepAlive($hash); +} + +### TO hit while sending... +sub KNXIO_TunnelRequestTO { + my $hash = shift; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash,\&KNXIO_TunnelRequestTO); +#$attr{$name}{verbose} = 4; # temp + # try resend...but only once + if (exists($hash->{KNXIOhelper}->{LASTSENTMSG})) { + Log3($name, 3, 'KNXIO_TunnelRequestTO hit - attempt resend'); + my $msg = $hash->{KNXIOhelper}->{LASTSENTMSG}; + DevIo_SimpleWrite($hash,$msg,0); + delete $hash->{KNXIOhelper}->{LASTSENTMSG}; + InternalTimer(gettimeofday() + 1.5, \&KNXIO_TunnelRequestTO, $hash); + return; + } + + Log3($name, 3, 'KNXIO_TunnelRequestTO hit - sending disconnect request'); + + # send disco request + my $hpai = pack('nCCCCn',(0x0801,0,0,0,0,0)); + my $msg = pack('nnnCC',(0x0610,0x0209,16,$hash->{KNXIOhelper}->{CCID},0)) . $hpai; + DevIo_SimpleWrite($hash,$msg,0); # send disconn requ + return; +} + +### translate Error-codes to text +### copied from 03_08_01 & 03_08_02_Core document +### all i know... +sub KNXIO_errCodes { + my $errcode = shift; + + my $errlist = {0=>'NO_ERROR',1=>'E_HOST_PROTCOL',2=>'E_VERSION_NOT_SUPPORTED',4=>'E_SEQUENCE_NUMBER',33=>'E_CONNECTION_ID', + 34=>'E_CONNECT_TYPE',35=>'E_CONNECTION_OPTION',36=>'E_NO_MORE_CONNECTIONS',38=>'E_DATA_CONNECTION',39=>'E_KNX_CONNECTION', + 41=>'E_TUNNELLING_LAYER', + }; + # full text + my $errlistfull = {0=>'OK', + 1=>'The requested host protocol is not supported by the KNXnet/IP device', + 2=>'The requested protocol version is not supported by the KNXnet/IP device', + 4=>'The received sequence number is out of order', + 33=>'The KNXnet/IP Server device cannot find an active data connection with the specified ID', + 34=>'The requested connection type is not supported by the KNXnet/IP Server device', + 35=>'One or more requested connection options are not supported by the KNXnet/IP Server device', + 36=>'The KNXnet/IP Server device cannot accept the new data connection because its maximum amount of concurrent connections is already occupied', + 38=>'The KNXnet/IP Server device detects an error concerning the data connection with the specified ID', + 39=>'The KNXnet/IP Server device detects an error concerning the KNX subnetwork connection with the specified ID', + 41=>'The KNXnet/IP Server device does not support the requested KNXnet/IP Tunnelling layer', + }; + + my $errtxt = $errlist->{$errcode}; + return 'E_UNDEFINED_ERROR ' . $errcode if (! defined($errtxt)); + $errtxt .= q{: } . $errlistfull->{$errcode}; # concatenate both textsegments + return $errtxt; +} + +1; + +=pod + +=encoding utf8 + +=item [device] +=item summary IO-module for KNX-devices supporting UDP, TCP & socket connections +=item summary_DE IO-Modul für KNX-devices. Unterstützt UDP, TCP & Socket Verbindungen + +=begin html + + +

KNXIO

+
    +

    This is a IO-module for KNX-devices. It provides an interface between FHEM and a KNX-Gateway. The Gateway can be either a KNX-Router/KNX-GW or the KNXD-daemon. + FHEM KNX-devices use this module as IO-Device. This Module does NOT support the deprecated EIB-Module! +

    + + +

    Define

    +

    define <name> KNXIO (H|M|T) <(ip-address|hostname):port> <phy-adress>
    or
    +define <name> KNXIO S <UNIX socket-path> <phy-adress>

    +
      +Connection Types (first parameter): +
        +
      • H Host Mode - connect to a KNX-router with UDP point-point protocol.
        + This is the mode also used by ETS when you specify KNXNET/IP as protocol. You do not need a KNXD installation. The protocol is complex and timing critical! + If you have delays in FHEM processing close to 1 sec, the protocol may disconnect. It should recover automatically, + however KNX-messages could have been lost!
      • +
      • M Multicast mode - connect to KNXD's or KNX-router's multicast-tree.
        + This is the mode also used by ETS when you specify KNXNET/IP Routing as protocol. + If you have a KNX-router that supports multicast, you do not need a KNXD installation. Default address:port is 224.0.23.12:3671
        + Pls. ensure that you have only one GW/KNXD in your LAN that feed the multicast tree!
        + This mode requires the IO::Socket::Multicast perl-module to be installed on yr. system. + On Debian systems this can be achieved by apt-get install libio-socket-multicast-perl.
      • +
      • T TCP Mode - uses a TCP-connection to KNXD (default port: 6720).
        + This mode is the successor of the TUL-modul, but does not support direct Serial/USB connection to a TPUart-USB Stick. + If you want to use a TPUart-USB Stick or any other serial KNX-GW, use either the TUL Module, or connect the USB-Stick to KNXD and in turn use modes M,S or T to connect to KNXD.
      • +
      • S Socket mode - communicate via KNXD's UNIX-socket on localhost. default Socket-path: /var/run/knx
        + Path might be different, depending on knxd-version or -config specification! This mode is tested ok with KNXD version 0.14.30. It does NOT work with ver. 0.10.0!
      • +
      +
      +ip-address:port or hostname:port +
        +
      • Hostname is supported for mode H and T. Port definition is mandatory.
      • +
      +
      +phy-address +
        +
      • The physical address is used as the source address of messages sent to KNX network. This address should be one of the defined client pool-addresses of KNXD or Router.
      • +
      +
    +

    All parameters are mandatory. Pls. ensure that you only have one path between your KNX-Installation and FHEM! + Do not define multiple KNXIO- or KNXTUL- or TUL-definitions at the same time.

    + +
      +Examples:
      +define myKNXGW KNXIO H 192.168.1.201:3671 0.0.51
      +define myKNXGW KNXIO M 224.0.23.12:3671 0.0.51
      +define myKNXGW KNXIO S /var/run/knx 0.0.51
      +define myKNXGW KNXIO T 192.168.1.200:6720 0.0.51
      +
    +
    +
      + Suggested parameters for KNXD (with systemd, Version >= 0.14.30): +
      + KNXD_OPTS="-e 0.0.50 -E 0.0.51:8 -D -T -S -b ip:" # knxd acts as multicast client
      + KNXD_OPTS="-e 0.0.50 -E 0.0.51:8 -D -T -R -S -b ipt:192.168.xx.yy" # connect to a knx-router with ip-addr
      + KNXD_OPTS="-e 0.0.50 -E 0.0.51:8 -D -T -R -S -single -b tpuarts:/dev/ttyxxx" # connect to a serial/USB KNX GW
      +
      +
    + + +

    Set - No Set cmd implemented

    + + +

    Get - No Get cmd impemented

    + + +

    Attributes

    +
      + +
    • disable - + Disable the device if set to 1. No send/receive from bus possible. Delete this attr to enable device again.
    • +
    • verbose - + increase verbosity of Log-Messages, system-wide default is set in "global" device. For a detailed description see: global-attr verbose
    • +
    +
    +
+ +=end html + +=cut