############################################################################## # # 88_HMCCURPCPROC.pm # # $Id$ # # Version 5.0 # # Subprocess based RPC Server module for HMCCU. # # (c) 2022 by zap (zap01 t-online de) # ############################################################################## # # Required perl modules: # # RPC::XML::Client # RPC::XML::Server # # ND deactivated in Read and Write!! ############################################################################## package main; use strict; use warnings; # use Data::Dumper; use RPC::XML::Client; use RPC::XML::Server; use SetExtensions; # require "$attr{global}{modpath}/FHEM/88_HMCCU.pm"; ###################################################################### # Constants ###################################################################### # HMCCURPC version my $HMCCURPCPROC_VERSION = '5.0 220431743'; # Maximum number of events processed per call of Read() my $HMCCURPCPROC_MAX_EVENTS = 100; # Maximum number of errors during socket write before log message is written my $HMCCURPCPROC_MAX_IOERRORS = 100; # Maximum number of elements in queue my $HMCCURPCPROC_MAX_QUEUESIZE = 500; # Maximum number of events to be send to FHEM within one function call my $HMCCURPCPROC_MAX_QUEUESEND = 70; # Time to wait after data processing loop in microseconds my $HMCCURPCPROC_TIME_WAIT = 100000; # RPC ping interval for default interface, should be smaller than HMCCURPCPROC_TIMEOUT_EVENT my $HMCCURPCPROC_TIME_PING = 300; # Timeout for established CCU connection in seconds my $HMCCURPCPROC_TIMEOUT_CONNECTION = 1; # Timeout for TriggerIO() in seconds my $HMCCURPCPROC_TIMEOUT_WRITE = 0.001; # Timeout for reading from Socket my $HMCCURPCPROC_TIMEOUT_READ = 0.01; # Timeout for accepting incoming connections in seconds (0 = default) my $HMCCURPCPROC_TIMEOUT_ACCEPT = 1; # Timeout for incoming CCU events in seconds (0 = ignore timeout) my $HMCCURPCPROC_TIMEOUT_EVENT = 0; # Send statistic information after specified amount of events my $HMCCURPCPROC_STATISTICS = 500; # Default RPC server base port my $HMCCURPCPROC_SERVER_PORT = 5400; # Delay for RPC server start after FHEM is initialized in seconds my $HMCCURPCPROC_INIT_INTERVAL0 = 12; # Delay for RPC server cleanup after stop in seconds my $HMCCURPCPROC_INIT_INTERVAL2 = 30; # Delay for RPC server functionality check after start in seconds my $HMCCURPCPROC_INIT_INTERVAL3 = 25; my %HMCCURPCPROC_RPC_FLAGS = ( 'BidCos-Wired' => '_', 'BidCos-RF' => 'multicalls', 'HmIP-RF' => '_', 'VirtualDevices' => '_', 'Homegear' => '_', 'CUxD' => '_', 'HVL' => '_' ); # BinRPC data types my $BINRPC_INTEGER = 1; my $BINRPC_BOOL = 2; my $BINRPC_STRING = 3; my $BINRPC_DOUBLE = 4; my $BINRPC_BASE64 = 17; my $BINRPC_ARRAY = 256; my $BINRPC_STRUCT = 257; # BinRPC message types my $BINRPC_REQUEST = 0x42696E00; my $BINRPC_RESPONSE = 0x42696E01; my $BINRPC_REQUEST_HEADER = 0x42696E40; my $BINRPC_ERROR = 0x42696EFF; # BinRPC datatype mapping my %BINRPC_TYPE_MAPPING = ( 'BOOL' => $BINRPC_BOOL, 'INTEGER' => $BINRPC_INTEGER, 'STRING' => $BINRPC_STRING, 'FLOAT' => $BINRPC_DOUBLE, 'DOUBLE' => $BINRPC_DOUBLE, 'BASE64' => $BINRPC_BASE64, 'ARRAY' => $BINRPC_ARRAY, 'STRUCT' => $BINRPC_STRUCT ); # Usage of some RPC requests (STRUCT => HASH) my %RPC_METHODS = ( 'system.multicall' => [ 'ARRAY' ], 'putParamset' => [ 'STRING', 'STRING', 'HASH' ], 'getParamset' => [ 'STRING', 'STRING' ], 'getParamsetDescription' => [ 'STRING', 'STRING' ], 'setValue' => [ 'STRING', 'STRING', 'STRING' ], 'getValue' => [ 'STRING', 'STRING' ] ); # RPC event types # EV = Event # ND = New device # DD = Delete device # RD = Replace device # RA = Readded device # UD = Update device # IN = Init RPC connection # EX = Exit RPC process # SL = Server loop # ST = Statistics (not in list of event types) # TO = Timeout my @RPC_EVENT_TYPES = ('EV', 'ND', 'DD', 'RD', 'RA', 'UD', 'IN', 'EX', 'SL', 'TO'); ###################################################################### # Functions ###################################################################### # Standard functions sub HMCCURPCPROC_Initialize ($); sub HMCCURPCPROC_Define ($$); sub HMCCURPCPROC_InitDevice ($$); sub HMCCURPCPROC_Undef ($$); sub HMCCURPCPROC_Rename ($$); sub HMCCURPCPROC_DelayedShutdown ($); sub HMCCURPCPROC_Shutdown ($); sub HMCCURPCPROC_Attr ($@); sub HMCCURPCPROC_Set ($@); sub HMCCURPCPROC_Get ($@); sub HMCCURPCPROC_Read ($); sub HMCCURPCPROC_SetError ($$$); sub HMCCURPCPROC_SetState ($$); sub HMCCURPCPROC_ProcessEvent ($$); # RPC information sub HMCCURPCPROC_GetDeviceDesc ($@); sub HMCCURPCPROC_GetParamsetDesc ($;$); sub HMCCURPCPROC_BuildParamsetRequest ($$$$); sub HMCCURPCPROC_GetPeers ($;$); # RPC server control functions sub HMCCURPCPROC_CheckProcessState ($$); sub HMCCURPCPROC_CleanupIO ($); sub HMCCURPCPROC_CleanupProcess ($); sub HMCCURPCPROC_DeRegisterCallback ($$); sub HMCCURPCPROC_GetRPCServerID ($$); sub HMCCURPCPROC_Housekeeping ($); sub HMCCURPCPROC_InitRPCServer ($$$$); sub HMCCURPCPROC_IsRPCServerRunning ($); sub HMCCURPCPROC_IsRPCStateBlocking ($); sub HMCCURPCPROC_RegisterCallback ($$); sub HMCCURPCPROC_ResetRPCState ($); sub HMCCURPCPROC_RPCPing ($); sub HMCCURPCPROC_RPCServerStarted ($); sub HMCCURPCPROC_RPCServerStopped ($); sub HMCCURPCPROC_Connect ($;$); sub HMCCURPCPROC_Disconnect ($;$); sub HMCCURPCPROC_IsConnected ($); sub HMCCURPCPROC_SendRequest ($@); sub HMCCURPCPROC_SendXMLRequest ($@); sub HMCCURPCPROC_SendBINRequest ($@); sub HMCCURPCPROC_SetRPCState ($$$$); sub HMCCURPCPROC_StartRPCServer ($); sub HMCCURPCPROC_StopRPCServer ($$); sub HMCCURPCPROC_TerminateProcess ($); # Helper functions sub HMCCURPCPROC_GetAttribute ($$$$); sub HMCCURPCPROC_GetKey ($); sub HMCCURPCPROC_HexDump ($$); # RPC server functions sub HMCCURPCPROC_ProcessRequest ($$); sub HMCCURPCPROC_HandleConnection ($$$$); sub HMCCURPCPROC_SendQueue ($$$$); sub HMCCURPCPROC_SendData ($$); sub HMCCURPCPROC_ReceiveData ($$); sub HMCCURPCPROC_ReadFromSocket ($$$); sub HMCCURPCPROC_DataAvailableOnSocket ($$); sub HMCCURPCPROC_WriteToSocket ($$$); sub HMCCURPCPROC_Write ($$$$); sub HMCCURPCPROC_WriteStats ($$); sub HMCCURPCPROC_NewDevicesCB ($$$); sub HMCCURPCPROC_DeleteDevicesCB ($$$); sub HMCCURPCPROC_UpdateDeviceCB ($$$$); sub HMCCURPCPROC_ReplaceDeviceCB ($$$$); sub HMCCURPCPROC_ReaddDevicesCB ($$$); sub HMCCURPCPROC_EventCB ($$$$$); sub HMCCURPCPROC_ListDevicesCB ($$); # RPC encoding functions sub HMCCURPCPROC_XMLEncValue ($;$); sub HMCCURPCPROC_EncInteger ($); sub HMCCURPCPROC_EncBool ($); sub HMCCURPCPROC_EncString ($); sub HMCCURPCPROC_EncName ($); sub HMCCURPCPROC_EncDouble ($); sub HMCCURPCPROC_EncBase64 ($); sub HMCCURPCPROC_EncArray ($); sub HMCCURPCPROC_EncStruct ($); sub HMCCURPCPROC_EncType ($;$); sub HMCCURPCPROC_EncodeRequest ($$); sub HMCCURPCPROC_EncodeResponse ($;$); # Binary RPC decoding functions sub HMCCURPCPROC_DecInteger ($$$); sub HMCCURPCPROC_DecBool ($$); sub HMCCURPCPROC_DecString ($$); sub HMCCURPCPROC_DecDouble ($$); sub HMCCURPCPROC_DecBase64 ($$); sub HMCCURPCPROC_DecArray ($$); sub HMCCURPCPROC_DecStruct ($$); sub HMCCURPCPROC_DecType ($$); sub HMCCURPCPROC_DecodeRequest ($); sub HMCCURPCPROC_DecodeResponse ($); ###################################################################### # Initialize module ###################################################################### sub HMCCURPCPROC_Initialize ($) { my ($hash) = @_; $hash->{version} = $HMCCURPCPROC_VERSION; $hash->{DefFn} = 'HMCCURPCPROC_Define'; $hash->{UndefFn} = 'HMCCURPCPROC_Undef'; $hash->{RenameFn} = 'HMCCURPCPROC_Rename'; $hash->{SetFn} = 'HMCCURPCPROC_Set'; $hash->{GetFn} = 'HMCCURPCPROC_Get'; $hash->{ReadFn} = 'HMCCURPCPROC_Read'; $hash->{AttrFn} = 'HMCCURPCPROC_Attr'; $hash->{ShutdownFn} = 'HMCCURPCPROC_Shutdown'; $hash->{DelayedShutdownFn} = 'HMCCURPCPROC_DelayedShutdown'; $hash->{parseParams} = 1; $hash->{AttrList} = 'ccuflags:multiple-strict,expert,logEvents,ccuInit,queueEvents,noEvents,noInitialUpdate,noMulticalls,statistics'. ' rpcMaxEvents rpcQueueSend rpcQueueSize rpcMaxIOErrors'. ' rpcServerAddr rpcServerPort rpcReadTimeout rpcWriteTimeout rpcAcceptTimeout'. ' rpcRetryRequest:0,1,2 rpcConnTimeout rpcStatistics rpcEventTimeout rpcPingCCU '. $readingFnAttributes; } ###################################################################### # Define device ###################################################################### sub HMCCURPCPROC_Define ($$) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; my $ioHash; my $ioname = ''; my $rpcip = ''; my $iface; my $usage = 'Usage: define $name HMCCURPCPROC { CCUHost | iodev={device} } { RPCPort | RPCInterface }'; my $errSource = "HMCCURPCPROC [$name]"; $hash->{version} = $HMCCURPCPROC_VERSION; if (exists($h->{iodev})) { $ioname = $h->{iodev}; return $usage if (scalar(@$a) < 3); return "$errSource HMCCU I/O device $ioname not found" if (!exists($defs{$ioname})); return "$errSource Device $ioname is not a HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); $ioHash = $defs{$ioname}; if (scalar(@$a) < 4) { $hash->{host} = $ioHash->{host}; $hash->{prot} = $ioHash->{prot}; $iface = $$a[2]; } else { if ($$a[2] =~ /^(https?):\/\/(.+)/) { $hash->{prot} = $1; $hash->{host} = $2; } else { $hash->{prot} = 'http'; $hash->{host} = $$a[2]; } $iface = $$a[3]; } $rpcip = HMCCU_ResolveName ($hash->{host}, 'N/A'); } else { return $usage if (scalar(@$a) < 4); if ($$a[2] =~ /^(https?):\/\/(.+)/) { $hash->{prot} = $1; $hash->{host} = $2; } else { $hash->{prot} = 'http'; $hash->{host} = $$a[2]; } $iface = $$a[3]; $rpcip = HMCCU_ResolveName ($hash->{host}, 'N/A'); # Find IO device foreach my $d (keys %defs) { my $dh = $defs{$d}; next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME}) || $dh->{TYPE} ne 'HMCCU'); if ($dh->{ccuip} eq $rpcip) { $ioHash = $dh; last; } } return $errSource."HMCCU I/O device not found" if (!defined($ioHash)); } # Store some definitions for delayed initialization $hash->{hmccu}{devspec} = $iface; $hash->{rpcip} = $rpcip; if ($init_done) { # Interactive define command while CCU not ready or no IO device defined if (!defined($ioHash)) { my ($ccuactive, $ccuinactive) = HMCCU_IODeviceStates (); return $errSource.($ccuinactive > 0 ? ' CCU and/or IO device not ready. Please try again later' : ' Cannot detect IO device'); } } else { # CCU not ready during FHEM start if ($ioHash->{ccustate} ne 'active') { HMCCU_Log ($hash, 2, 'CCU not ready. Trying later ...'); readingsSingleUpdate ($hash, 'state', 'Pending', 1); $hash->{ccudevstate} = 'pending'; return undef; } } # Initialize FHEM device, set IO device my $rc = HMCCURPCPROC_InitDevice ($ioHash, $hash); return "$errSource Invalid port or interface $iface" if ($rc == 1); return "$errSource Can't assign I/O device $ioname" if ($rc == 2); return "$errSource Invalid local IP address ".$hash->{hmccu}{localaddr} if ($rc == 3); return "$errSource RPC device for CCU/port already exists" if ($rc == 4); return "$errSource Cannot connect to CCU ".$hash->{host}." interface $iface" if ($rc == 5); return "$errSource Can't fetch RPC methods supported by CCU ".$hash->{host} if ($rc == 6); return undef; } ###################################################################### # Initialization of FHEM device. # Called during Define() or by HMCCU during delayed initialization # after CCU ready. # Return 0 on successful initialization or >0 on error: # 1 = Invalid port or interface # 2 = Cannot assign IO device # 3 = Invalid local IP address # 4 = RPC device for CCU/port already exists # 5 = Cannot connect to CCU ###################################################################### sub HMCCURPCPROC_InitDevice ($$) { my ($ioHash, $devHash) = @_; my $name = $devHash->{NAME}; my $iface = $devHash->{hmccu}{devspec}; # Check if interface is valid my ($ifname, $ifport) = HMCCU_GetRPCServerInfo ($ioHash, $iface, 'name,port'); return 1 if (!defined($ifname) || !defined($ifport)); # Check if RPC device with same interface already exists foreach my $d (keys %defs) { my $dh = $defs{$d}; next if (!exists($dh->{TYPE}) || !exists($dh->{NAME})); if ($dh->{TYPE} eq 'HMCCURPCPROC' && $dh->{NAME} ne $name && IsDisabled ($dh->{NAME}) != 1) { return 4 if ($devHash->{host} eq $dh->{host} && exists ($dh->{rpcport}) && $dh->{rpcport} == $ifport); } } # Detect local IP address and check if CCU is reachable my $localaddr = HMCCU_TCPConnect ($devHash->{host}, $ifport); return 5 if ($localaddr eq ''); $devHash->{hmccu}{localaddr} = $localaddr; $devHash->{hmccu}{defaultaddr} = $devHash->{hmccu}{localaddr}; # Get unique ID for RPC server: last 2 segments of local IP address # Do not append random digits because of https://forum.fhem.de/index.php/topic,83544.msg797146.html#msg797146 my $id1 = HMCCU_GetIdFromIP ($devHash->{hmccu}{localaddr}, ''); my $id2 = HMCCU_GetIdFromIP ($ioHash->{ccuip}, ''); return 3 if ($id1 eq '' || $id2 eq ''); $devHash->{rpcid} = $id1.$id2; # Set I/O device and store reference for RPC device in I/O device my $ioname = $ioHash->{NAME}; return 2 if (!HMCCU_AssignIODevice ($devHash, $ioname, $ifname)); # Store internals $devHash->{rpcport} = $ifport; $devHash->{rpcinterface} = $ifname; $devHash->{ccuip} = $ioHash->{ccuip}; $devHash->{ccutype} = $ioHash->{ccutype}; $devHash->{CCUNum} = $ioHash->{CCUNum}; $devHash->{ccustate} = $ioHash->{ccustate}; # Fetch supported RPC methods my ($resp, $err) = HMCCURPCPROC_SendRequest ($devHash, 'system.listMethods'); if (!defined($resp)) { return HMCCU_Log ($devHash, 1, "Can't fetch RPC methods supported by CCU", 6); } elsif (ref($resp) eq 'ARRAY') { $devHash->{hmccu}{rpc}{methods} = join(',',@$resp); if (exists($HMCCURPCPROC_RPC_FLAGS{$ifname}) && $HMCCURPCPROC_RPC_FLAGS{$ifname} =~ /multicalls/ && $devHash->{hmccu}{rpc}{methods} =~ /(system\.multicall)/i) { $devHash->{hmccu}{rpc}{multicall} = $1; HMCCU_Log ($devHash, 2, "CCU interface $ifname supports RPC multicalls"); } else { HMCCU_Log ($devHash, 2, "CCU interface $ifname doesn't support RPC multicalls"); } } else { return HMCCU_Log ($devHash, 2, 'Unexpected response from system.listMethods', 6); } HMCCU_Log ($devHash, 1, "Initialized version $HMCCURPCPROC_VERSION for interface $ifname with I/O device $ioname"); # Set some attributes if ($init_done) { $attr{$name}{stateFormat} = 'rpcstate/state'; $attr{$name}{verbose} = 2; } # RPC device ready HMCCURPCPROC_ResetRPCState ($devHash); HMCCURPCPROC_SetState ($devHash, 'Initialized'); return 0; } ###################################################################### # Delete device ###################################################################### sub HMCCURPCPROC_Undef ($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; # Shutdown RPC server HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2); # Delete RPC device name in I/O device if (exists($ioHash->{hmccu}{interfaces}{$ifname}) && exists($ioHash->{hmccu}{interfaces}{$ifname}{device}) && $ioHash->{hmccu}{interfaces}{$ifname}{device} eq $name) { delete $ioHash->{hmccu}{interfaces}{$ifname}{device}; } return undef; } ###################################################################### # Rename device ###################################################################### sub HMCCURPCPROC_Rename ($$) { my ($newName, $oldName) = @_; my $hash = $defs{$newName}; my $ioHash = $hash->{IODev}; my $ifName = $hash->{rpcinterface}; $ioHash->{hmccu}{interfaces}{$ifName}{device} = $newName; } ###################################################################### # Delayed shutdown FHEM ###################################################################### sub HMCCURPCPROC_DelayedShutdown ($) { my ($hash) = @_; my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; my $delay = HMCCU_Max (AttrVal ('global', 'maxShutdownDelay', 10)-2, 0); # Shutdown RPC server if (defined($ioHash) && exists($ioHash->{hmccu}{interfaces}{$ifname}{manager}) && $ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { if (!exists($hash->{hmccu}{delayedShutdown})) { $hash->{hmccu}{delayedShutdown} = $delay; HMCCU_Log ($hash, 1, "Graceful shutdown within $delay seconds"); HMCCURPCPROC_StopRPCServer ($hash, $delay); } else { HMCCU_Log ($hash, 1, 'Graceful shutdown already in progress'); } } return 1; } ###################################################################### # Shutdown FHEM ###################################################################### sub HMCCURPCPROC_Shutdown ($) { my ($hash) = @_; my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; # Shutdown RPC server if (defined($ioHash) && exists($ioHash->{hmccu}{interfaces}{$ifname}{manager}) && $ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { if (!exists ($hash->{hmccu}{delayedShutdown})) { HMCCU_Log ($hash, 1, 'Immediate shutdown'); HMCCURPCPROC_StopRPCServer ($hash, 0); } else { HMCCU_Log ($hash, 1, 'Graceful shutdown'); } } # Remove all internal timers RemoveInternalTimer ($hash); return undef; } ###################################################################### # Set attribute ###################################################################### sub HMCCURPCPROC_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; my $hash = $defs{$name}; my $ioHash = $hash->{IODev}; if ($cmd eq 'set') { if ($attrname =~ /^(rpcAcceptTimeout|rpcReadTimeout|rpcWriteTimeout)$/ && $attrval == 0) { return "HMCCURPCPROC: [$name] Value for attribute $attrname must be greater than 0"; } elsif ($attrname eq 'rpcServerAddr') { $hash->{hmccu}{localaddr} = $attrval; } elsif ($attrname eq 'rpcPingCCU') { HMCCU_Log ($hash, 1, "Attribute rpcPingCCU ignored. Please set it in I/O device"); } elsif ($attrname eq 'ccuflags' && $attrval =~ /(reconnect|logPong)/) { HMCCU_Log ($hash, 1, "Flag $1 ignored. Please set it in I/O device"); } } elsif ($cmd eq 'del') { if ($attrname eq 'rpcServerAddr') { $hash->{hmccu}{localaddr} = $hash->{hmccu}{defaultaddr}; } } HMCCU_LogDisplay ($hash, 2, 'Please restart RPC server to apply attribute changes') if ($init_done && (!defined($ioHash) || $ioHash->{hmccu}{postInit} == 0) && HMCCURPCPROC_CheckProcessState ($hash, 'running')); return undef; } ###################################################################### # Set commands ###################################################################### sub HMCCURPCPROC_Set ($@) { my ($hash, $a, $h) = @_; my $ioHash = $hash->{IODev}; my $name = shift @$a; my $opt = shift @$a // return 'No set command specified'; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $options = $ccuflags =~ /expert/ ? 'cleanup:noArg deregister:noArg register:noArg rpcrequest rpcserver:on,off' : ''; my $busyoptions = $ccuflags =~ /expert/ ? 'rpcserver:off' : ''; return "HMCCURPCPROC: CCU busy, choose one of $busyoptions" if ($opt ne 'rpcserver' && HMCCURPCPROC_IsRPCStateBlocking ($hash)); if ($opt eq 'cleanup') { HMCCURPCPROC_Housekeeping ($hash); return undef; } elsif ($opt eq 'register') { if ($hash->{RPCState} eq 'running') { my ($rc, $rcmsg) = HMCCURPCPROC_RegisterCallback ($hash, 2); if ($rc) { $hash->{ccustate} = 'active'; return HMCCURPCPROC_SetState ($hash, 'OK'); } else { return HMCCURPCPROC_SetError ($hash, $rcmsg, 2); } } else { return HMCCURPCPROC_SetError ($hash, 'RPC server not running', 2); } } elsif ($opt eq 'deregister') { my ($rc, $err) = HMCCURPCPROC_DeRegisterCallback ($hash, 1); return HMCCURPCPROC_SetError ($hash, $err, 2) if (!$rc); return HMCCURPCPROC_SetState ($hash, "OK"); } elsif ($opt eq 'rpcrequest') { my $request = shift @$a // return HMCCURPCPROC_SetError ( $hash, "Usage: set $name rpcrequest {request} [{ value[:type] | parameter=value[:type] | !STRUCT } ...]", 2); return "RPC method $request not supported" if (defined($hash->{hmccu}{rpc}{methods}) && $hash->{hmccu}{rpc}{methods} !~ /$request/); my $structSize = scalar(keys %$h); my $s = 0; my @param = (); foreach my $p (@$a) { if ($p eq '!STRUCT' && $structSize > 0) { push @param, $h; $s = 1; } else { push @param, $p; } } push @param, $h if ($structSize > 0 && !$s); my ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, $request, @param); return HMCCURPCPROC_SetError ($hash, "RPC request failed: $err", 2) if (!defined($resp)); return HMCCU_RefToString ($resp); } elsif ($opt eq 'rpcserver') { my $action = shift @$a; return HMCCURPCPROC_SetError ($hash, "Usage: set $name rpcserver {on|off}", 2) if (!defined($action) || $action !~ /^(on|off)$/); if ($action eq 'on') { return HMCCURPCPROC_SetError ($hash, 'RPC server already running', 2) if ($hash->{RPCState} ne 'inactive' && $hash->{RPCState} ne 'error'); $ioHash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; my ($rc, $info) = HMCCURPCPROC_StartRPCServer ($hash); if (!$rc) { HMCCURPCPROC_SetRPCState ($hash, 'error', undef, undef); return HMCCURPCPROC_SetError ($hash, $info, 1); } } elsif ($action eq 'off') { $ioHash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2); } return undef; } return "HMCCURPCPROC: Unknown argument $opt, choose one of $options"; } ###################################################################### # Get commands ###################################################################### sub HMCCURPCPROC_Get ($@) { my ($hash, $a, $h) = @_; my $ioHash = $hash->{IODev}; my $name = shift @$a; my $opt = shift @$a // return 'No get command specified'; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $options = 'deviceDesc rpcevents:noArg rpcstate:noArg peers:noArg'; return 'HMCCURPCPROC: CCU busy, choose one of rpcstate:noArg' if ($opt ne 'rpcstate' && HMCCURPCPROC_IsRPCStateBlocking ($hash)); my $result = 'Command not implemented'; my $rc; if ($opt eq 'deviceDesc') { my $address; my $object = shift @$a; if (defined($object)) { if (exists($defs{$object})) { my $clHash = $defs{$object}; my $clType = $clHash->{TYPE}; return HMCCURPCPROC_SetError ($hash, "Illegal device type $clType", 2) if ($clType ne 'HMCCUCHN' && $clType ne 'HMCCUDEV'); $address = $clHash->{ccuaddr}; } else { $address = $object; } ($address, undef) = HMCCU_SplitChnAddr ($address); } HMCCU_ResetDeviceTables ($ioHash, $hash->{rpcinterface}, $address); my $cd = HMCCURPCPROC_GetDeviceDesc ($hash, $address); my $cm = HMCCURPCPROC_GetParamsetDesc ($hash, $address); return "Read $cd channel and device descriptions and $cm device models from CCU"; } elsif ($opt eq 'peers') { my $cp = HMCCURPCPROC_GetPeers ($hash); return "Read $cp links from CCU"; } elsif ($opt eq 'rpcevents') { my $clkey = HMCCURPCPROC_GetKey ($hash); $result = "Event statistics for server $clkey\n"; $result .= "Average event delay = ".$hash->{hmccu}{rpc}{avgdelay}."\n" if (defined ($hash->{hmccu}{rpc}{avgdelay})); $result .= ('=' x 40)."\nET Sent by RPC server Received by FHEM\n".('-' x 40)."\n"; foreach my $et (@RPC_EVENT_TYPES) { my $snd = exists ($hash->{hmccu}{rpc}{snd}{$et}) ? sprintf ("%7d", $hash->{hmccu}{rpc}{snd}{$et}) : " n/a"; my $rec = exists ($hash->{hmccu}{rpc}{rec}{$et}) ? sprintf ("%7d", $hash->{hmccu}{rpc}{rec}{$et}) : " n/a"; $result .= "$et $snd $rec\n\n"; } if ($ccuflags =~ /statistics/ && exists ($hash->{hmccu}{stats}{rcv})) { my $eh = HMCCU_MaxHashEntries ($hash->{hmccu}{stats}{rcv}, 3); $result .= ('=' x 40)."\nTop Sender\n".('=' x 40)."\n"; for (my $i=0; $i<3; $i++) { last if (!exists ($eh->{$i})); my $dn = HMCCU_GetDeviceName ($ioHash, $eh->{$i}{k}, '?'); $result .= "$eh->{$i}{k} / $dn : $eh->{$i}{v}\n"; } } $result .= ('=' x 40)."\nRPC requests: ".$hash->{hmccu}{rpc}{requests}; return $result eq '' ? 'No event statistics found' : $result; } elsif ($opt eq 'rpcstate') { my $clkey = HMCCURPCPROC_GetKey ($hash); $result = "PID RPC-Process State \n".('-' x 32)."\n"; my $sid = defined ($hash->{hmccu}{rpc}{pid}) ? sprintf ("%5d", $hash->{hmccu}{rpc}{pid}) : "N/A "; my $sname = sprintf ("%-10s", $clkey); my $cbport = defined ($hash->{hmccu}{rpc}{cbport}) ? $hash->{hmccu}{rpc}{cbport} : "N/A"; my $addr = defined ($hash->{hmccu}{localaddr}) ? $hash->{hmccu}{localaddr} : "N/A"; $result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n\n". "Local address = $addr\n". "Callback port = $cbport\n"; return $result; } return "HMCCURPCPROC: Unknown argument $opt, choose one of $options"; } ###################################################################### # Read data from processes ###################################################################### sub HMCCURPCPROC_Read ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $eventcount = 0; # Total number of events my $devcount = 0; # Number of DD, ND or RD events my $evcount = 0; # Number of EV events my %events = (); my %devices = (); # Check if child socket exists if (!defined ($hash->{hmccu}{sockchild})) { HMCCU_Log ($hash, 2, 'Child socket does not exist'); return; } # Get attributes my $rpcmaxevents = AttrVal ($name, 'rpcMaxEvents', $HMCCURPCPROC_MAX_EVENTS); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $hmccuflags = AttrVal ($ioHash->{NAME}, 'ccuflags', 'null'); my $socktimeout = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPCPROC_TIMEOUT_WRITE); # Read events from queue while (1) { my ($item, $err) = HMCCURPCPROC_ReceiveData ($hash->{hmccu}{sockchild}, $socktimeout); if (!defined($item)) { HMCCU_Log ($hash, 4, "Read stopped after $eventcount events $err"); last; } HMCCU_Log ($hash, 4, "read $item from queue") if ($ccuflags =~ /logEvents/); my ($et, $clkey, @par) = HMCCURPCPROC_ProcessEvent ($hash, $item); next if (!defined($et)); if ($et eq 'EV') { $events{$par[0]}{$par[1]}{VALUES}{$par[2]} = $par[3]; $evcount++; $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); # Count events per device for statistics $hash->{hmccu}{stats}{rcv}{$par[0]}++ if ($ccuflags =~ /statistics/); } elsif ($et eq 'EX') { # I/O already cleaned up. Leave Read() last; } elsif ($et eq 'ND') { # HMCCU_Log ($hash, 2, "ND: ".join(';', @par)); # $devices{$par[0]}{flag} = 'N'; # $devices{$par[0]}{version} = $par[3]; # $devices{$par[0]}{paramsets} = $par[6]; # if ($par[1] eq 'D') { # $devices{$par[0]}{addtype} = 'dev'; # $devices{$par[0]}{type} = $par[2]; # $devices{$par[0]}{firmware} = $par[4]; # $devices{$par[0]}{rxmode} = $par[5]; # $devices{$par[0]}{children} = $par[10]; # } # else { # $devices{$par[0]}{addtype} = 'chn'; # $devices{$par[0]}{usetype} = $par[2]; # $devices{$par[0]}{sourceroles} = $par[7]; # $devices{$par[0]}{targetroles} = $par[8]; # $devices{$par[0]}{direction} = $par[9]; # $devices{$par[0]}{parent} = $par[11]; # $devices{$par[0]}{aes} = $par[12]; # } # $devcount++; } elsif ($et eq 'DD') { $devices{$par[0]}{flag} = 'D'; $devcount++; } elsif ($et eq 'RD') { $devices{$par[0]}{flag} = 'R'; $devices{$par[0]}{newaddr} = $par[1]; $devcount++; } $eventcount++; if ($eventcount > $rpcmaxevents) { HMCCU_Log ($hash, 4, "Read stopped after $rpcmaxevents events"); last; } } # Update device table and client device readings HMCCU_UpdateDeviceTable ($ioHash, \%devices) if ($devcount > 0); HMCCU_UpdateMultipleDevices ($ioHash, \%events) if ($evcount > 0 && $ccuflags !~ /noEvents/ && $hmccuflags !~ /noEvents/); } ###################################################################### # Set error state and write log file message # Parameter level is optional. Default value for level is 1. ###################################################################### sub HMCCURPCPROC_SetError ($$$) { my ($hash, $text, $level) = @_; my $msg = defined ($text) ? $text : 'unknown error'; HMCCURPCPROC_SetState ($hash, 'error'); HMCCU_Log ($hash, (defined($level) ? $level : 1), $msg); return $msg; } ###################################################################### # Set state of device ###################################################################### sub HMCCURPCPROC_SetState ($$) { my ($hash, $state) = @_; if (defined($state)) { readingsSingleUpdate ($hash, 'state', $state, 1); HMCCU_Log ($hash, 4, "Set state to $state"); } return undef; } ###################################################################### # Set state of RPC server # Parameters msg and level are optional. Default for level is 1. ###################################################################### sub HMCCURPCPROC_SetRPCState ($$$$) { my ($hash, $state, $msg, $level) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; return undef if (exists($hash->{RPCState}) && $hash->{RPCState} eq $state); $hash->{hmccu}{rpc}{state} = $state; $hash->{RPCState} = $state; readingsSingleUpdate ($hash, 'rpcstate', $state, 1); HMCCURPCPROC_SetState ($hash, 'busy') if ($state ne 'running' && $state ne 'inactive' && $state ne 'error' && ReadingsVal ($name, 'state', '') ne 'busy'); HMCCU_Log ($hash, (defined($level) ? $level : 1), $msg) if (defined($msg)); HMCCU_Log ($hash, 4, "Set rpcstate to $state"); # Set state of interface in I/O device HMCCU_SetRPCState ($ioHash, $state, $hash->{rpcinterface}); return undef; } ###################################################################### # Reset RPC State ###################################################################### sub HMCCURPCPROC_ResetRPCState ($) { my ($hash) = @_; $hash->{RPCPID} = '0'; $hash->{hmccu}{rpc}{pid} = undef; $hash->{hmccu}{rpc}{clkey} = undef; $hash->{hmccu}{evtime} = 0; $hash->{hmccu}{rpcstarttime} = 0; return HMCCURPCPROC_SetRPCState ($hash, 'inactive', undef, undef); } ###################################################################### # Check if CCU is busy due to RPC start or stop ###################################################################### sub HMCCURPCPROC_IsRPCStateBlocking ($) { my ($hash) = @_; return (exists($hash->{RPCState}) && ($hash->{RPCState} eq 'running' || $hash->{RPCState} eq 'inactive')) ? 0 : 1; } ###################################################################### # Process RPC server event ###################################################################### sub HMCCURPCPROC_ProcessEvent ($$) { my ($hash, $event) = @_; my $name = $hash->{NAME}; my $rpcname = 'CB'.$hash->{rpcport}.$hash->{rpcid}; my $rh = \%{$hash->{hmccu}{rpc}}; # Just for code simplification my $ioHash = $hash->{IODev}; my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Number of arguments in RPC events (without event type and clkey) my %rpceventargs = ( 'EV', 4, 'ND', 13, 'DD', 1, 'RD', 2, 'RA', 1, 'UD', 2, 'IN', 2, 'EX', 2, 'SL', 1, 'TO', 1, 'ST', 11 ); return undef if (!defined ($event) || $event eq ''); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); my $evttimeout = ($ping > 0 && $hash->{rpcinterface} eq $defInterface) ? $ping*2 : HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); # Log event HMCCU_Log ($hash, 2, "CCUEvent = $event") if ($ccuflags =~ /logEvents/); # Detect event type and clkey my ($et, $clkey, $evdata) = split (/\|/, $event, 3); if (!defined($evdata)) { HMCCU_Log ($hash, 2, "Syntax error in RPC event data $event"); return undef; } # Check for valid server if ($clkey ne $rpcname) { HMCCU_Log ($hash, 2, "Received $et event for unknown RPC server $clkey"); return undef; } # Check event type if (!exists ($rpceventargs{$et})) { $et =~ s/([\x00-\xFF])/sprintf("0x%X ",ord($1))/eg; HMCCU_Log ($hash, 2, "Received unknown event from CCU: $et"); return undef; } # Parse event my @t = split (/\|/, $evdata, $rpceventargs{$et}); my $tc = scalar(@t); # Check event parameters if ($tc != $rpceventargs{$et}) { HMCCU_Log ($hash, 2, "Wrong number of $tc parameters in event $event. Expected ".$rpceventargs{$et}); return undef; } # Update statistic counters $rh->{rec}{$et}++; $rh->{evtime} = time (); if ($et eq 'EV') { # # Update of datapoint # Input: EV|clkey|Time|Address|Datapoint|Value # Output: EV, clkey, DevAdd, ChnNo, Datapoint, Value # my $delay = $rh->{evtime}-$t[0]; $rh->{sumdelay} += $delay; $rh->{avgdelay} = $rh->{sumdelay}/$rh->{rec}{$et}; $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); HMCCU_Log ($hash, 3, "Received CENTRAL event from $clkey. ".$t[2]."=".$t[3]) if ($t[1] eq 'CENTRAL' && $t[3] eq $rpcname && HMCCU_IsFlag ($ioHash->{NAME}, 'logPong')); my ($add, $chn) = split (/:/, $t[1]); return defined($chn) ? ($et, $clkey, $add, $chn, @t[2,3]) : undef; } elsif ($et eq 'SL') { # # RPC server enters server loop # Input: SL|clkey|Pid # Output: SL, clkey, countWorking # if ($t[0] == $rh->{pid}) { HMCCURPCPROC_SetRPCState ($hash, 'working', "RPC server $clkey enters server loop", 2); my ($rc, $rcmsg) = HMCCURPCPROC_RegisterCallback ($hash, 0); if (!$rc) { HMCCURPCPROC_SetRPCState ($hash, 'error', $rcmsg, 1); return ($et, $clkey, 1, 0, 0, 0); } else { HMCCURPCPROC_SetRPCState ($hash, $rcmsg, "RPC server $clkey $rcmsg", 1); } my $srun = HMCCURPCPROC_RPCServerStarted ($hash); return ($et, $clkey, ($srun == 0 ? 1 : 0), $srun); } else { HMCCU_Log ($hash, 0, "Received SL event. Wrong PID=".$t[0]." for RPC server $clkey"); return undef; } } elsif ($et eq 'IN') { # # RPC server initialized # Input: IN|clkey|INIT|State # Output: IN, clkey, Running, ClientsUpdated, UpdateErrors # return ($et, $clkey, 0, 0, 0) if ($rh->{state} eq 'running'); HMCCURPCPROC_SetRPCState ($hash, 'running', "RPC server $clkey running.", 1); my $run = HMCCURPCPROC_RPCServerStarted ($hash); return ($et, $clkey, $run); } elsif ($et eq 'EX') { # # Process stopped # Input: EX|clkey|SHUTDOWN|Pid # Output: EX, clkey, Pid, Stopped, All # HMCCURPCPROC_SetRPCState ($hash, 'inactive', "RPC server process $clkey terminated.", 1); HMCCURPCPROC_RPCServerStopped ($hash); return ($et, $clkey, $t[1], 1, 1); } elsif ($et eq 'ND') { # # CCU device added # Input: ND|clkey|C/D|Address|Type|Version|Firmware|RxMode|Paramsets| # LinkSourceRoles|LinkTargetRoles|Direction|Children|Parent|AESActive # Output: ND, clkey, DevAdd, C/D, Type, Version, Firmware, RxMode, Paramsets, # LinkSourceRoles, LinkTargetRoles, Direction, Children, Parent, AESActive # return ($et, $clkey, @t[1,0,2..12]); } elsif ($et eq 'DD' || $et eq 'RA') { # # CCU device deleted or readded # Input: {DD,RA}|clkey|Address # Output: {DD,RA}, clkey, DevAdd # return ($et, $clkey, $t[0]); } elsif ($et eq 'UD') { # # CCU device updated # Input: UD|clkey|Address|Hint # Output: UD, clkey, DevAdd, Hint # return ($et, $clkey, @t[0,1]); } elsif ($et eq 'RD') { # # CCU device replaced # Input: RD|clkey|Address1|Address2 # Output: RD, clkey, Address1, Address2 # return ($et, $clkey, @t[0,1]); } elsif ($et eq 'ST') { # # Statistic data. Store snapshots of sent events. # Input: ST|clkey|nTotal|nEV|nND|nDD|nRD|nRA|nUD|nIN|nEX|nSL # Output: ST, clkey, ... # my @res = ($et, $clkey); push (@res, @t); my $total = shift @t; for (my $i=0; $i{hmccu}{rpc}{snd}{$RPC_EVENT_TYPES[$i]} += $t[$i]; } return @res; } elsif ($et eq 'TO') { # # Event timeout # Input: TO|clkey|DiffTime # Output: TO, clkey, Port, DiffTime # if ($evttimeout > 0) { HMCCU_Log ($hash, 2, "Received no events from interface $clkey for ".$t[0]." seconds"); $hash->{ccustate} = 'timeout'; if ($hash->{RPCState} eq 'running' && $hash->{rpcport} == $defPort) { # If interface is default interface inform IO device about timeout HMCCU_EventsTimedOut ($ioHash) } DoTrigger ($name, "No events from interface $clkey for ".$t[0]." seconds"); } return ($et, $clkey, $hash->{rpcport}, $t[0]); } return undef; } ###################################################################### # Get attribute with fallback to I/O device attribute ###################################################################### sub HMCCURPCPROC_GetAttribute ($$$$) { my ($hash, $attr, $ioattr, $default) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $value = 'null'; if (defined($attr)) { $value = AttrVal ($name, $attr, 'null'); return $value if ($value ne 'null'); } if (defined($ioattr)) { $value = AttrVal ($ioHash->{NAME}, $ioattr, 'null'); return $value if ($value ne 'null'); } return $default; } ###################################################################### # Get links (sender and receiver) from CCU. ###################################################################### sub HMCCURPCPROC_GetPeers ($;$) { my ($hash, $address) = @_; my $ioHash = $hash->{IODev}; my $c = 0; my ($resp, $err) = defined($address) ? HMCCURPCPROC_SendRequest ($hash, 'getLinks', $address) : HMCCURPCPROC_SendRequest ($hash, 'getLinks'); if (!defined($resp)) { my $msg = defined($address) ? "Can't get peers of device $address" : "Can't get full list of peers"; return HMCCU_Log ($hash, 2, "$msg: $err", 0); } if (ref($resp) eq 'ARRAY') { $c = HMCCU_AddPeers ($ioHash, $resp, $hash->{rpcinterface}); } else { return HMCCU_Log ($hash, 2, "Unexpected response from getLinks: $err", 0); } return $c; } ###################################################################### # Get RPC device descriptions from CCU recursively. Add devices to # IO device. # If address is not specified, fetch description of all devices. # Return number of devices and channels read from CCU. ###################################################################### sub HMCCURPCPROC_GetDeviceDesc ($@) { my ($hash, @addressList) = @_; my $ioHash = $hash->{IODev}; my $c = 0; my $resp; my $err; if (@addressList) { if (scalar(@addressList) == 1) { # Read a single device or channel description ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, 'getDeviceDescription', $addressList[0]); } else { # Read multiple device or channel descriptions my @multiCall = map { { methodName => 'getDeviceDescription', params => [ $_ ] } } @addressList; ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, \@multiCall); } } else { # Read all device descriptions, including channels ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, 'listDevices'); } return HMCCU_Log ($hash, 2, "Can't read device description(s)", 0) if (!defined($resp)); if (ref($resp) eq 'HASH') { if (HMCCU_AddDeviceDesc ($ioHash, $resp, 'ADDRESS', $hash->{rpcinterface})) { $c = 1; if (defined($resp->{CHILDREN}) && ref($resp->{CHILDREN}) eq 'ARRAY') { foreach my $child (@{$resp->{CHILDREN}}) { $c += HMCCURPCPROC_GetDeviceDesc ($hash, $child); } } } } elsif (ref($resp) eq 'ARRAY') { foreach my $dev (@$resp) { $c++ if (HMCCU_AddDeviceDesc ($ioHash, $dev, 'ADDRESS', $hash->{rpcinterface})); } } else { return HMCCU_Log ($hash, 2, 'Illegal device description format', 0); } return $c; } ###################################################################### # Get RPC device paramset descriptions from CCU # Function is called recursively # Parameters: # $address - Device or channel address. If not specified, all # addresses known by IO device are used. # Return number of devices read from CCU. ###################################################################### sub HMCCURPCPROC_GetParamsetDesc ($;$) { my ($hash, $address) = @_; my $ioHash = $hash->{IODev}; if (defined($address)) { # Build multicall request for requesting all parameter set definitions of address my @multiCall = (); my @cbParam = (); my $cnt = HMCCURPCPROC_BuildParamsetRequest ($hash, $address, \@multiCall, \@cbParam); return 0 if ($cnt == 0); # Multicall request my ($c, $err) = HMCCURPCPROC_SendMulticallRequest ($hash, \@multiCall, \&HMCCU_AddDeviceModel, \@cbParam); $c //= 0; if ($c == 0) { HMCCU_Log ($hash, 2, "Error(s) while fetching parameter set descriptions $address. $err"); } return $c; } else { my $c = 0; foreach my $a (HMCCU_GetDeviceAddresses ($ioHash, $hash->{rpcinterface}, '_addtype=dev')) { $c += HMCCURPCPROC_GetParamsetDesc ($hash, $a); } return $c; } } ###################################################################### # Build RPC multicall request for device or channel # Return number of single requests ###################################################################### sub HMCCURPCPROC_BuildParamsetRequest ($$$$) { my ($hash, $address, $multiCall, $cbParam) = @_; my $ioHash = $hash->{IODev}; my $c = 0; my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $address, $hash->{rpcinterface}); return HMCCU_Log ($hash, 2, "Can't get device description for address $address", 0) if (!defined($devDesc) || !defined($devDesc->{PARAMSETS}) || $devDesc->{PARAMSETS} eq '' || !exists($devDesc->{_fw_ver})); my $chnNo = ($devDesc->{_addtype} eq 'chn') ? $devDesc->{INDEX} : 'd'; # Check if model already exists if (!HMCCU_ExistsDeviceModel ($ioHash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo)) { # Build multicall request for requesting all parameter set definitions of address foreach my $ps (split (',', $devDesc->{PARAMSETS})) { push @$multiCall, { methodName => 'getParamsetDescription', params => [ $address, $ps ] }; push @$cbParam, [ $devDesc->{_model}, $devDesc->{_fw_ver}, $ps, $chnNo ]; $c++; } } # Read paramset definitions of childs (= channels) if (defined($devDesc->{CHILDREN}) && $devDesc->{CHILDREN} ne '') { foreach my $child (split (',', $devDesc->{CHILDREN})) { $c += HMCCURPCPROC_BuildParamsetRequest ($hash, $child, $multiCall, $cbParam); } } return $c; } ###################################################################### # Register callback for specified CCU interface port. # Parameter force: # 1: callback will be registered even if state is "running". State # will not be modified. # 2: CCU connectivity is checked before registering RPC server. # Return (1, new state) on success. New state is 'running' if flag # ccuInit is not set. Otherwise 'registered'. # Return (0, errormessage) on error. ###################################################################### sub HMCCURPCPROC_RegisterCallback ($$) { my ($hash, $force) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $port = $hash->{rpcport}; my $serveraddr = $hash->{host}; my $localaddr = $hash->{hmccu}{localaddr}; my $clkey = HMCCURPCPROC_GetKey ($hash); return (0, "RPC server $clkey not in state working") if ($hash->{hmccu}{rpc}{state} ne 'working' && $force == 0); return (0, "CCU port $port not reachable") if ($force == 2 && !HMCCU_TCPConnect ($hash->{host}, $port)); my $cburl = HMCCU_GetRPCCallbackURL ($ioHash, $localaddr, $hash->{hmccu}{rpc}{cbport}, $clkey, $port); my $clurl = HMCCU_BuildURL ($ioHash, $port); my ($rpctype) = HMCCU_GetRPCServerInfo ($ioHash, $port, 'type'); return (0, "Can't get RPC parameters for ID $clkey") if (!defined($cburl) || !defined($clurl) || !defined($rpctype)); $hash->{hmccu}{rpc}{port} = $port; $hash->{hmccu}{rpc}{clurl} = $clurl; $hash->{hmccu}{rpc}{cburl} = $cburl; HMCCU_Log ($hash, 2, "Registering callback $cburl of type $rpctype with ID $clkey at $clurl"); my ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, "init", "$cburl:STRING", "$clkey:STRING"); if (defined($resp)) { return (1, $ccuflags !~ /ccuInit/ ? 'running' : 'registered'); } else { return (0, "Failed to register callback for ID $clkey: $err"); } } ###################################################################### # Deregister RPC callbacks at CCU ###################################################################### sub HMCCURPCPROC_DeRegisterCallback ($$) { my ($hash, $force) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $port = $hash->{rpcport}; my $clkey = HMCCURPCPROC_GetKey ($hash); my $localaddr = $hash->{hmccu}{localaddr}; my $cburl = ''; my $clurl = ''; my $rpchash = \%{$hash->{hmccu}{rpc}}; return (0, "RPC server $clkey not in state registered or running") if ($rpchash->{state} ne 'registered' && $rpchash->{state} ne 'running' && $force == 0); $cburl = $rpchash->{cburl} if (exists($rpchash->{cburl})); $clurl = $rpchash->{clurl} if (exists($rpchash->{clurl})); $cburl = HMCCU_GetRPCCallbackURL ($ioHash, $localaddr, $rpchash->{cbport}, $clkey, $port) if ($cburl eq ''); $clurl = HMCCU_BuildURL ($ioHash, $port) if ($clurl eq ''); return (0, "Can't get RPC parameters for ID $clkey") if ($cburl eq '' || $clurl eq ''); HMCCU_Log ($hash, 1, "Deregistering RPC server $cburl with ID $clkey at $clurl"); # Deregister up to 2 times my $resp; my $err; for (my $i=0; $i<2; $i++) { ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, "init", "$cburl:STRING", ''); if (defined ($resp)) { HMCCURPCPROC_SetRPCState ($hash, $force == 0 ? 'deregistered' : $rpchash->{state}, "Callback for RPC server $clkey deregistered", 1); $rpchash->{cburl} = ''; $rpchash->{clurl} = ''; $rpchash->{cbport} = 0; return (1, 'working'); } } return (0, "Failed to deregister RPC server $clkey: $err"); } ###################################################################### # Initialize RPC server for specified CCU port # Return server object or undef on error ###################################################################### sub HMCCURPCPROC_InitRPCServer ($$$$) { my ($name, $clkey, $cbPort, $prot) = @_; my $server; # Create binary RPC server if ($prot eq 'B') { $server->{__daemon} = IO::Socket::INET->new (LocalPort => $cbPort, Type => SOCK_STREAM, Reuse => 1, Listen => SOMAXCONN); if (!($server->{__daemon})) { HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey. Port $cbPort in use?"); return undef; } return $server; } # Create XML RPC server $server = RPC::XML::Server->new (port => $cbPort); if (!ref($server)) { HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey. Port $cbPort in use?"); return undef; } HMCCU_Log ($name, 2, "Callback server $clkey created. Listening on port $cbPort"); # Callback for events HMCCU_Log ($name, 4, "Adding callback for events for server $clkey"); $server->add_method ({ name => "event", signature => ["string string string string string","string string string string int", "string string string string double","string string string string boolean", "string string string string i4"], code => \&HMCCURPCPROC_EventCB }); # Callback for new devices HMCCU_Log ($name, 4, "Adding callback for new devices for server $clkey"); $server->add_method ({ name => "newDevices", signature => ["string string array"], code => \&HMCCURPCPROC_NewDevicesCB }); # Callback for deleted devices HMCCU_Log ($name, 4, "Adding callback for deleted devices for server $clkey"); $server->add_method ({ name => "deleteDevices", signature => ["string string array"], code => \&HMCCURPCPROC_DeleteDevicesCB }); # Callback for modified devices HMCCU_Log ($name, 4, "Adding callback for modified devices for server $clkey"); $server->add_method ({ name => "updateDevice", signature => ["string string string int", "string string string i4"], code => \&HMCCURPCPROC_UpdateDeviceCB }); # Callback for replaced devices HMCCU_Log ($name, 4, "Adding callback for replaced devices for server $clkey"); $server->add_method ({ name => "replaceDevice", signature => ["string string string string"], code => \&HMCCURPCPROC_ReplaceDeviceCB }); # Callback for readded devices HMCCU_Log ($name, 4, "Adding callback for readded devices for server $clkey"); $server->add_method ({ name => "readdedDevice", signature => ["string string array"], code => \&HMCCURPCPROC_ReaddDeviceCB }); # Dummy implementation, always return an empty array HMCCU_Log ($name, 4, "Adding callback for list devices for server $clkey"); $server->add_method ({ name => "listDevices", signature => ["array string"], code => \&HMCCURPCPROC_ListDevicesCB }); return $server; } ###################################################################### # Start RPC server process # Return (State, Msg) # State: 0=Error, 1=Started, 2=Already running ###################################################################### sub HMCCURPCPROC_StartRPCServer ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Local IP address and callback ID should be set during device definition return (0, 'Local address and/or callback ID not defined') if (!exists($hash->{hmccu}{localaddr}) || !exists($hash->{rpcid})); # Check if RPC server is already running return (2, 'RPC server already running') if (HMCCURPCPROC_CheckProcessState ($hash, 'running')); # Get parameters and attributes my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); my $localaddr = HMCCURPCPROC_GetAttribute ($hash, undef, 'rpcserveraddr', $hash->{hmccu}{localaddr}); my $rpcserverport = HMCCURPCPROC_GetAttribute ($hash, 'rpcServerPort', 'rpcserverport', $HMCCURPCPROC_SERVER_PORT); my $evttimeout = ($ping > 0 && $hash->{rpcinterface} eq $defInterface) ? $ping*2 : HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); my $ccunum = $hash->{CCUNum}; my $rpcport = $hash->{rpcport}; my ($serveraddr, $interface) = HMCCU_GetRPCServerInfo ($ioHash, $rpcport, 'host,name'); my $clkey = 'CB'.$rpcport.$hash->{rpcid}; $hash->{hmccu}{localaddr} = $localaddr; # Store parameters for child process my %procpar; $procpar{socktimeout} = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPCPROC_TIMEOUT_WRITE); $procpar{conntimeout} = AttrVal ($name, 'rpcConnTimeout', $HMCCURPCPROC_TIMEOUT_CONNECTION); $procpar{acctimeout} = AttrVal ($name, 'rpcAcceptTimeout', $HMCCURPCPROC_TIMEOUT_ACCEPT); $procpar{queuesize} = AttrVal ($name, 'rpcQueueSize', $HMCCURPCPROC_MAX_QUEUESIZE); $procpar{queuesend} = AttrVal ($name, 'rpcQueueSend', $HMCCURPCPROC_MAX_QUEUESEND); $procpar{statistics} = AttrVal ($name, 'rpcStatistics', $HMCCURPCPROC_STATISTICS); $procpar{maxioerrors} = AttrVal ($name, 'rpcMaxIOErrors', $HMCCURPCPROC_MAX_IOERRORS); $procpar{ccuflags} = AttrVal ($name, 'ccuflags', 'null'); $procpar{evttimeout} = $evttimeout; $procpar{interface} = $interface; ($procpar{flags}, $procpar{type}) = HMCCU_GetRPCServerInfo ($ioHash, $rpcport, 'flags,type'); $procpar{name} = $name; $procpar{clkey} = $clkey; # Reset state of server processes $hash->{hmccu}{rpc}{state} = 'inactive'; # Create socket pair for communication between RPC server process and FHEM process my ($sockchild, $sockparent); return (0, "Can't create I/O socket pair") if (!socketpair ($sockchild, $sockparent, AF_UNIX, SOCK_STREAM, PF_UNSPEC)); $sockchild->autoflush (1); $sockparent->autoflush (1); $hash->{hmccu}{sockparent} = $sockparent; $hash->{hmccu}{sockchild} = $sockchild; # Enable FHEM I/O, calculate RPC server port my $pid = $$; $hash->{FD} = fileno $sockchild; $selectlist{"RPC.$name.$pid"} = $hash; my $callbackport = $rpcserverport+$rpcport+($ccunum*10); # Initialize RPC server # my $err = ''; # my %srvprocpar; # Start RPC server process my $rpcpid = fhemFork (); if (!defined($rpcpid)) { close ($sockparent); close ($sockchild); return (0, "Can't create RPC server process for interface $interface"); } if (!$rpcpid) { # Child process, only needs parent socket HMCCURPCPROC_HandleConnection ($rpcport, $callbackport, $sockparent, \%procpar); # Connection loop ended. Close sockets and exit child process close ($sockparent); close ($sockchild); exit (0); } # Parent process HMCCU_Log ($hash, 2, "RPC server process started for interface $interface with PID=$rpcpid"); # Store process parameters $hash->{hmccu}{rpc}{clkey} = $clkey; $hash->{hmccu}{rpc}{cbport} = $callbackport; $hash->{hmccu}{rpc}{pid} = $rpcpid; $hash->{hmccu}{rpc}{state} = 'initialized'; # Reset statistic counter foreach my $et (@RPC_EVENT_TYPES) { $hash->{hmccu}{rpc}{rec}{$et} = 0; $hash->{hmccu}{rpc}{snd}{$et} = 0; } $hash->{hmccu}{rpc}{sumdelay} = 0; $hash->{RPCPID} = $rpcpid; # Trigger Timer function for checking successful RPC start # Timer will be removed before first execution if event 'IN' is reveived InternalTimer (gettimeofday()+$HMCCURPCPROC_INIT_INTERVAL3, "HMCCURPCPROC_IsRPCServerRunning", $hash, 0); HMCCURPCPROC_SetRPCState ($hash, 'starting', 'RPC server starting', 1); DoTrigger ($name, 'RPC server starting'); return (1, undef); } ###################################################################### # Set overall status if all RPC servers are running and update all # FHEM devices. # Return (State, updated devices, failed updates) ###################################################################### sub HMCCURPCPROC_RPCServerStarted ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ioHash = $hash->{IODev}; my $clkey = HMCCURPCPROC_GetKey ($hash); my $ifname = $hash->{rpcinterface}; my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Check if RPC servers are running. Set overall status if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { $hash->{hmccu}{rpcstarttime} = time (); HMCCURPCPROC_SetState ($hash, 'OK'); # Update client devices if interface is managed by HMCCURPCPROC device. # Normally interfaces are managed by HMCCU device. if ($ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { HMCCU_UpdateClients ($ioHash, '.*', 'Attr', 0, $ifname, 1); } RemoveInternalTimer ($hash, "HMCCURPCPROC_IsRPCServerRunning"); # Activate heartbeat if interface is default interface and rpcPingCCU > 0 if ($ping > 0 && $ifname eq $defInterface) { HMCCU_Log ($hash, 1, "Scheduled CCU ping every $ping seconds"); InternalTimer (gettimeofday()+$ping, "HMCCURPCPROC_RPCPing", $hash, 0); } DoTrigger ($name, "RPC server $clkey running"); return 1; } return 0; } ###################################################################### # Cleanup if RPC server stopped ###################################################################### sub HMCCURPCPROC_RPCServerStopped ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $clkey = HMCCURPCPROC_GetKey ($hash); HMCCURPCPROC_CleanupProcess ($hash); HMCCURPCPROC_CleanupIO ($hash); HMCCURPCPROC_ResetRPCState ($hash); HMCCURPCPROC_SetState ($hash, 'OK'); RemoveInternalTimer ($hash); DoTrigger ($name, "RPC server $clkey stopped"); # Inform FHEM that instance can be shut down HMCCU_Log ($hash, 2, 'RPC server stopped. Cancel delayed shutdown.'); CancelDelayedShutdown ($name) if (exists($hash->{hmccu}{delayedShutdown})); } ###################################################################### # Stop I/O Handling ###################################################################### sub HMCCURPCPROC_CleanupIO ($) { my ($hash) = @_; my $name = $hash->{NAME}; my $pid = $$; if (exists($selectlist{"RPC.$name.$pid"})) { HMCCU_Log ($hash, 2, 'Stop I/O handling'); delete $selectlist{"RPC.$name.$pid"}; delete $hash->{FD} if (defined ($hash->{FD})); } if (defined($hash->{hmccu}{sockchild})) { HMCCU_Log ($hash, 3, 'Close child socket'); $hash->{hmccu}{sockchild}->close (); delete $hash->{hmccu}{sockchild}; } if (defined($hash->{hmccu}{sockparent})) { HMCCU_Log ($hash, 3, 'Close parent socket'); $hash->{hmccu}{sockparent}->close (); delete $hash->{hmccu}{sockparent}; } } ###################################################################### # Terminate RPC server process by sending an INT signal. # Return 0 if RPC server not running. ###################################################################### sub HMCCURPCPROC_TerminateProcess ($) { my ($hash) = @_; my $clkey = HMCCURPCPROC_GetKey ($hash); my $pid = $hash->{hmccu}{rpc}{pid}; if (defined ($pid) && kill (0, $pid)) { HMCCURPCPROC_SetRPCState ($hash, 'stopping', "Sending signal INT to RPC server process $clkey with PID=$pid", 2); kill ('INT', $pid); return 1; } else { HMCCURPCPROC_SetRPCState ($hash, 'inactive', "RPC server process $clkey not runnning", 1); return 0; } } ###################################################################### # Cleanup inactive RPC server process. # Return 0 if process is running. ###################################################################### sub HMCCURPCPROC_CleanupProcess ($) { my ($hash) = @_; my $clkey = HMCCURPCPROC_GetKey ($hash); my $pid = $hash->{hmccu}{rpc}{pid}; if (defined($pid) && kill (0, $pid)) { HMCCU_Log ($hash, 1, "Process $clkey with PID=$pid still running. Killing it."); kill ('KILL', $pid); sleep (1); return HMCCU_Log ($hash, 1, "Can't kill process $clkey with PID=$pid", 0) if (kill (0, $pid)); } HMCCURPCPROC_SetRPCState ($hash, 'inactive', "RPC server process $clkey deleted", 2); $hash->{hmccu}{rpc}{pid} = undef; return 1; } ###################################################################### # Check if RPC server process is in specified state. # Parameter state is a regular expression. Valid states are: # inactive # starting # working # registered # running # stopping # If state is 'running' the process is checked by calling kill() with # signal 0. ###################################################################### sub HMCCURPCPROC_CheckProcessState ($$) { my ($hash, $state) = @_; my $pstate = $hash->{hmccu}{rpc}{state}; if ($state eq 'running' || $state eq '.*') { my $pid = $hash->{hmccu}{rpc}{pid}; return (defined($pid) && $pid != 0 && kill (0, $pid) && $pstate =~ /$state/) ? $pid : 0 } else { return ($pstate =~ /$state/) ? 1 : 0; } } ###################################################################### # Timer function to check if RPC server process is running. # Call Housekeeping() if process is not running. ###################################################################### sub HMCCURPCPROC_IsRPCServerRunning ($) { my ($hash) = @_; HMCCU_Log ($hash, 2, 'Checking if RPC server process is running'); if (!HMCCURPCPROC_CheckProcessState ($hash, 'running')) { HMCCU_Log ($hash, 1, 'RPC server process not running. Cleaning up'); HMCCURPCPROC_Housekeeping ($hash); return 0; } return HMCCU_Log ($hash, 2, 'RPC server process running', 1); } ###################################################################### # Cleanup RPC server environment. ###################################################################### sub HMCCURPCPROC_Housekeeping ($) { my ($hash) = @_; HMCCU_Log ($hash, 1, 'Housekeeping called. Cleaning up RPC environment'); # Deregister callback URLs in CCU HMCCURPCPROC_DeRegisterCallback ($hash, 0); # Terminate process by sending signal INT sleep (2) if (HMCCURPCPROC_TerminateProcess ($hash)); # Next call will cleanup IO, processes and reset RPC state HMCCURPCPROC_RPCServerStopped ($hash); } ###################################################################### # Stop RPC server processes. # If function is called by Shutdown. If parameter wait can be 0 or # undef. ###################################################################### sub HMCCURPCPROC_StopRPCServer ($$) { my ($hash, $wait) = @_; $wait //= $HMCCURPCPROC_INIT_INTERVAL2; my $clkey = HMCCURPCPROC_GetKey ($hash); if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { HMCCU_Log ($hash, 1, "Stopping RPC server $clkey"); HMCCURPCPROC_SetState ($hash, "busy"); # Deregister callback URLs in CCU my ($rc, $err) = HMCCURPCPROC_DeRegisterCallback ($hash, 0); HMCCU_Log ($hash, 1, $err) if (!$rc); # Stop RPC server process HMCCURPCPROC_TerminateProcess ($hash); # Trigger timer function for checking successful RPC stop # Timer will be removed wenn receiving EX event from RPC server process if ($wait > 0) { HMCCU_Log ($hash, 2, "Scheduling cleanup in $wait seconds"); InternalTimer (gettimeofday()+$wait, "HMCCURPCPROC_Housekeeping", $hash, 0); } else { HMCCU_Log ($hash, 2, 'Cleaning up immediately'); HMCCURPCPROC_Housekeeping ($hash); } # Give process the chance to terminate sleep (1); return 1; } else { HMCCU_Log ($hash, 2, 'Found no running processes. Cleaning up ...'); HMCCURPCPROC_Housekeeping ($hash); return 0; } } ###################################################################### # Establish RPC connection # Return value depends on RPC interface: # XML: Return RPC::XML::Client # BIN: Return binary TCP socket # Return 0 = error, 1 = success. ###################################################################### sub HMCCURPCPROC_Connect ($;$) { my ($hash, $ioHash) = @_; $ioHash //= $hash->{IODev}; # Connection already established return $hash->{hmccu}{rpc}{connection} if (defined($hash->{hmccu}{rpc}{connection})); if (HMCCU_IsRPCType ($ioHash, $hash->{rpcport}, 'A')) { # Build the request URL my $clurl = HMCCU_BuildURL ($ioHash, $hash->{rpcport}); return HMCCU_Log ($hash, 2, "Can't get RPC client URL for port $hash->{rpcport}", 0) if (!defined($clurl)); my $header = HTTP::Headers->new ('Connection' => 'Keep-Alive'); $hash->{hmccu}{rpc}{connection} = RPC::XML::Client->new ($clurl, useragent => [ ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 }, default_headers => $header ] ); } elsif (HMCCU_IsRPCType ($ioHash, $hash->{rpcport}, 'B')) { my ($serveraddr) = HMCCU_GetRPCServerInfo ($ioHash, $hash->{rpcport}, 'host'); return HMCCU_Log ($ioHash, 2, "Can't get server address for port $hash->{rpcport}", 0) if (!defined($serveraddr)); $hash->{hmccu}{rpc}{connection} = IO::Socket::INET->new ( PeerHost => $serveraddr, PeerPort => $hash->{rpcport}, Proto => 'tcp', Timeout => 3 ); if ($hash->{hmccu}{rpc}{connection}) { $hash->{hmccu}{rpc}{connection}->autoflush (1); $hash->{hmccu}{rpc}{connection}->timeout (1); } } return HMCCU_Log ($hash, 2, "Can't connect to RPC interface", 0) if (!defined($hash->{hmccu}{rpc}{connection})); return 1; } ###################################################################### # Close RPC connection ###################################################################### sub HMCCURPCPROC_Disconnect ($;$) { my ($hash, $ioHash) = @_; $ioHash //= $hash->{IODev}; return if (!defined($hash->{hmccu}{rpc}{connection})); if (HMCCU_IsRPCType ($ioHash, $hash->{rpcport}, 'B')) { # Close socket $hash->{hmccu}{rpc}{connection}->close(); } delete $hash->{hmccu}{rpc}{connection}; } ###################################################################### # Check if connection to CCU is established ###################################################################### sub HMCCURPCPROC_IsConnected ($) { my ($hash) = @_; return defined($hash->{hmccu}{rpc}{connection}) ? 1 : 0; } ###################################################################### # Send multicall RPC request to CCU # Function $cbFunc is executed for each successful element of result # $cbParam is a reference to an array of parameter array references # array. Syntax of callback function is: # Func ($ioHash, $respRef, @$cbPar[n]) # Return (undef, errMsg) on error # Return (reqCount, undef) on success ###################################################################### sub HMCCURPCPROC_SendMulticallRequest ($$$$) { my ($hash, $multiCall, $cbFunc, $cbPar) = @_; my ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, 'system.multicall', $multiCall); if (defined($resp)) { return HMCCURPCPROC_ProcessMulticallResponse ($hash, $multiCall, $resp, $cbFunc, $cbPar); } else { return (undef, "Error while executing RPC multicall request: $err"); } } ###################################################################### # Process RPC multicall respone recursively. If HASH reference is # found, call $cbFunc and pass ioHash, reference and @$cbPar[n] # Return (undef, errMsg) on error # Return (reqCount, undef) on success ###################################################################### sub HMCCURPCPROC_ProcessMulticallResponse ($$$$$) { my ($hash, $multiCall, $resp, $cbFunc, $cbPar) = @_; if (ref($resp) eq 'ARRAY') { my $c = 0; my $i = 0; # Single request response loop foreach my $r (@$resp) { if (ref($r) eq 'HASH') { if (exists($r->{faultString})) { # Single request failed my $req = @$multiCall[$i]; my $m = $req->{methodName}; my $p = join(',',@{$req->{params}}); HMCCU_Log ($hash, 2, "Error in RPC multicall request $m $p: $r->{faultString}"); } else { # Single request was successful. Execute callback function &$cbFunc ($hash->{IODev}, $r, @{$cbPar->[$i]}); $c++; # Count successful single request } } elsif (ref($r) eq 'ARRAY') { # Sub array of structs (normally one) # Request response element loop foreach my $e (@$r) { if (ref($e) eq 'HASH') { # Single request was successful. Execute callback function &$cbFunc ($hash->{IODev}, $e, @{$cbPar->[$i]}); $c++; # Count successful single request } } } else { HMCCU_Log ($hash, 2, 'Invalid single request response type in multicall response'); } $i++; # Count single requests } return $c == 0 ? (undef, 'All RPC multicall single requests failed') : ($c, undef); } elsif (ref($resp) eq 'HASH' && exists($resp->{faultString})) { # Multicall request failed return (undef, $resp->{faultString}); } return (undef, 'Invalid multicall request response type'); } ###################################################################### # Send RPC request to CCU. # Supports XML and BINRPC requests. # Return value: # (response, undef) - Request successful # (undef, error) - Request failed with error ###################################################################### sub HMCCURPCPROC_SendRequest ($@) { my ($hash, $request, @param) = @_; my $port = $hash->{rpcport}; my $ioHash = $hash->{IODev}; if (!defined($ioHash)) { HMCCU_Log ($hash, 2, 'I/O device not found'); return (undef, 'I/O device not found'); } my $retry = AttrVal ($hash->{NAME}, 'rpcRetryRequest', 1); $retry = 2 if ($retry > 2); $hash->{hmccu}{rpc}{requests} //= 0; # Count RPC requests # Multicall request if ($request eq 'system.multicall' && ( HMCCU_IsFlag ($hash, 'noMulticalls') || !defined($hash->{hmccu}{rpc}{multicall}) || HMCCU_IsRPCType ($ioHash, $port, 'B') )) { # If multicalls are not supported or disabled, execute multiple requests my @respList = (); my $reqList = shift @param; # Reference to request array foreach my $r (@$reqList) { my ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, $r->{methodName}, @{$r->{params}}); return ($resp, $err) if (!defined($resp)); push @respList, $resp; } return (\@respList, undef); } # Check request syntax return (undef, "Request method $request not supported by CCU interface") if (defined($hash->{hmccu}{rpc}{methods}) && $hash->{hmccu}{rpc}{methods} !~ /$request/); if (exists($RPC_METHODS{$request})) { my @rpcParam = @param; my @syntax = @{$RPC_METHODS{$request}}; while (my $t = shift @syntax) { my $p = shift @rpcParam // return (undef, "Missing parameter in RPC request $request"); return (undef, "Wrong parameter type in RPC request $request. Expected type is $t") if ($t ne 'STRING' && ref($p) ne $t); } } # Reuse existing connection my $alreadyConnected = HMCCURPCPROC_IsConnected ($hash); if (!$alreadyConnected) { if (!HMCCURPCPROC_Connect ($hash, $ioHash)) { return (undef, "Can't connect to CCU"); } } my $resp; my $err; for (my $reqNo=0; $reqNo<=$retry; $reqNo++) { if (HMCCU_IsRPCType ($ioHash, $port, 'A')) { # XML RPC request $hash->{hmccu}{rpc}{requests}++; ($resp, $err) = HMCCURPCPROC_SendXMLRequest ($hash, $ioHash, $request, @param); last if (defined($resp)); } elsif (HMCCU_IsRPCType ($ioHash, $port, 'B')) { # Binary RPC request $hash->{hmccu}{rpc}{requests}++; ($resp, $err) = HMCCURPCPROC_SendBINRequest ($hash, $ioHash, $request, @param); last if (defined($resp)); } else { HMCCU_Log ($hash, 2, 'Unknown RPC server type', undef); return (undef, 'Unknown RPC server type'); } HMCCU_Log ($hash, 2, "Retrying request $request"); } if (!$alreadyConnected) { HMCCURPCPROC_Disconnect ($hash, $ioHash); } return ($resp, $err); } ###################################################################### # Send XML RPC request to CCU # Return value: # (response, undef) - Request successful # (undef, error) - Request failed with error ###################################################################### sub HMCCURPCPROC_SendXMLRequest ($@) { my ($hash, $ioHash, $request, @param) = @_; my $name = $hash->{NAME}; my $port = $hash->{rpcport}; my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; # Build the request URL my $clurl = HMCCU_BuildURL ($ioHash, $port); if (!defined($clurl)) { HMCCU_Log ($hash, 2, "Can't get RPC client URL for port $port"); return (undef, "Can't get RPC client URL for port $port"); } HMCCU_Log ($hash, 4, "Send ASCII XML RPC request $request to $clurl"); # my $rpcclient = RPC::XML::Client->new ($clurl, useragent => [ # ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 } # ]); my @rpcParam = map { HMCCURPCPROC_XMLEncValue ($_) } @param; # Submit RPC request my $resp = $hash->{hmccu}{rpc}{connection}->simple_request ($request, @rpcParam); if (!defined($resp)) { HMCCU_Log ($hash, 2, "RPC request $request failed: ".$RPC::XML::ERROR); return (undef, "RPC request $request failed: ".$RPC::XML::ERROR); } if (ref($resp) eq 'HASH' && exists($resp->{faultString})) { HMCCU_Log ($hash, 2, "RPC request $request failed: ".$resp->{faultString}); return (undef, "RPC request $request failed: ".$resp->{faultString}); } return ($resp, undef); } ###################################################################### # Send binary RPC request to CCU # Return value: # (response, undef) - Request successful # (undef, error) - Request failed with error ###################################################################### sub HMCCURPCPROC_SendBINRequest ($@) { my ($hash, $ioHash, $request, @param) = @_; my $name = $hash->{NAME}; # my $port = $hash->{rpcport}; # my ($serveraddr) = HMCCU_GetRPCServerInfo ($ioHash, $port, 'host'); # if (!defined($serveraddr)) { # HMCCU_Log ($ioHash, 2, "Can't get server address for port $port"); # return (undef, "Can't get server address for port $port"); # } my $timeoutRead = AttrVal ($name, 'rpcReadTimeout', $HMCCURPCPROC_TIMEOUT_READ); my $timeoutWrite = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPCPROC_TIMEOUT_WRITE); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $verbose = GetVerbose ($name); my $encreq = HMCCURPCPROC_EncodeRequest ($request, \@param); return (undef, 'Error while encoding binary request') if ($encreq eq ''); if ($ccuflags =~ /logEvents/) { HMCCU_Log ($hash, 4, 'Binary RPC request'); HMCCURPCPROC_HexDump ($name, $encreq); } # Create a socket connection # my $socket = IO::Socket::INET->new (PeerHost => $serveraddr, PeerPort => $port, Proto => 'tcp', Timeout => 3); # if (!$socket) { # HMCCU_Log ($hash, 2, "Can't create socket for $serveraddr:$port"); # return (undef, "Can't create socket for $serveraddr:$port"); # } # $socket->autoflush (1); # $socket->timeout (1); my ($bytesWritten, $errmsg) = HMCCURPCPROC_WriteToSocket ($hash->{hmccu}{rpc}{connection}, $encreq, $timeoutWrite); if ($bytesWritten > 0) { my ($bytesRead, $encresp) = HMCCURPCPROC_ReadFromSocket ($hash, $hash->{hmccu}{rpc}{connection}, $timeoutRead); # $socket->close (); if ($bytesRead > 0) { if ($ccuflags =~ /logEvents/) { HMCCU_Log ($hash, 4, 'Binary RPC response'); HMCCURPCPROC_HexDump ($name, $encresp); } my ($response, $err) = HMCCURPCPROC_DecodeResponse ($encresp); return (undef, 'Error while decoding binary response') if (!defined($err) || $err == 0); return $response; } else { # Reconnect HMCCURPCPROC_Disconnect ($hash, $ioHash); HMCCURPCPROC_Connect ($hash, $ioHash); return (undef, "Error while reading response for command $request: $encresp"); } } else { # $socket->close (); return (undef, "No data sent for request $request: $errmsg"); } } ###################################################################### # Timer function for RPC Ping ###################################################################### sub HMCCURPCPROC_RPCPing ($) { my ($hash) = @_; my $ioHash = $hash->{IODev}; my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); return HMCCU_Log ($hash, 1, 'CCU ping disabled') if ($ping == 0); my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); if ($hash->{rpcinterface} eq $defInterface) { if ($init_done && HMCCURPCPROC_CheckProcessState ($hash, 'running')) { my $clkey = HMCCURPCPROC_GetKey ($hash); my ($resp, $err) = HMCCURPCPROC_SendRequest ($hash, 'ping', "$clkey:STRING"); HMCCU_Log ($hash, 3, "Failed to send RPC ping: $err") if (!defined($resp)); } InternalTimer (gettimeofday()+$ping, "HMCCURPCPROC_RPCPing", $hash, 0); } } ###################################################################### # Process binary RPC request ###################################################################### sub HMCCURPCPROC_ProcessRequest ($$) { my ($server, $connection) = @_; my $name = $server->{hmccu}{name}; my $clkey = $server->{hmccu}{clkey}; my @methodlist = ('listDevices', 'listMethods', 'system.listMethods', 'system.multicall'); my $verbose = GetVerbose ($name); # Read request my $request = ''; while (my $packet = <$connection>) { $request .= $packet; } return if ($request eq ''); if ($server->{hmccu}{ccuflags} =~ /logEvents/ && $verbose >= 4) { HMCCU_Log ($name, 4, "$clkey raw request:"); HMCCURPCPROC_HexDump ($name, $request); } # Decode request my ($method, $params) = HMCCURPCPROC_DecodeRequest ($request); return if (!defined($method)); $method = lc($method); HMCCU_Log ($name, 4, "Request method = $method"); if ($method eq 'listmethods' || $method eq 'system.listmethods') { $connection->send (HMCCURPCPROC_EncodeResponse (\@methodlist)); } elsif ($method eq 'listdevices') { HMCCURPCPROC_ListDevicesCB ($server, $clkey); $connection->send (HMCCURPCPROC_EncodeResponse (undef)); } elsif ($method eq 'system.multicall') { return if (ref($params) ne 'ARRAY'); my $a = $$params[0]; foreach my $s (@$a) { next if (!exists($s->{methodName}) || !exists($s->{params}) || $s->{methodName} ne 'event' || scalar(@{$s->{params}}) < 4); HMCCURPCPROC_EventCB ($server, $clkey, ${$s->{params}}[1], ${$s->{params}}[2], ${$s->{params}}[3]); HMCCU_Log ($name, 4, 'Event '.${$s->{params}}[1].' '.${$s->{params}}[2].' ' .${$s->{params}}[3]); } } } ###################################################################### # Subprocess function for handling incoming RPC requests ###################################################################### sub HMCCURPCPROC_HandleConnection ($$$$) { my ($port, $callbackport, $sockparent, $procpar) = @_; my $name = $procpar->{name}; my $iface = $procpar->{interface}; my $prot = $procpar->{type}; my $evttimeout = $procpar->{evttimeout}; my $conntimeout = $procpar->{conntimeout}; my $acctimeout = $procpar->{acctimeout}; my $socktimeout = $procpar->{socktimeout}; my $maxsnd = $procpar->{queuesend}; my $maxioerrors = $procpar->{maxioerrors}; my $clkey = $procpar->{clkey}; my $ioerrors = 0; my $sioerrors = 0; my $run = 1; my $pid = $$; # Initialize RPC server HMCCU_Log ($name, 2, "Initializing RPC server $clkey for interface $iface"); my $rpcsrv = HMCCURPCPROC_InitRPCServer ($name, $clkey, $callbackport, $prot); return HMCCU_Log ($name, 1, "Can't initialize RPC server $clkey for interface $iface") if (!defined($rpcsrv)); return HMCCU_Log ($name, 1, "Server socket not found for port $port") if (!($rpcsrv->{__daemon})); # Event queue my @queue = (); # Store RPC server parameters $rpcsrv->{hmccu}{name} = $name; $rpcsrv->{hmccu}{clkey} = $clkey; $rpcsrv->{hmccu}{eventqueue} = \@queue; $rpcsrv->{hmccu}{queuesize} = $procpar->{queuesize}; $rpcsrv->{hmccu}{sockparent} = $sockparent; $rpcsrv->{hmccu}{statistics} = $procpar->{statistics}; $rpcsrv->{hmccu}{ccuflags} = $procpar->{ccuflags}; $rpcsrv->{hmccu}{flags} = $procpar->{flags}; $rpcsrv->{hmccu}{evttime} = time (); # Initialize statistic counters foreach my $et (@RPC_EVENT_TYPES, 'total') { $rpcsrv->{hmccu}{rec}{$et} = 0; $rpcsrv->{hmccu}{snd}{$et} = 0; } # Signal handler $SIG{INT} = sub { $run = 0; HMCCU_Log ($name, 2, "$clkey received signal INT"); }; HMCCURPCPROC_Write ($rpcsrv, 'SL', $clkey, $pid); HMCCU_Log ($name, 2, "$clkey accepting connections. PID=$pid"); $rpcsrv->{__daemon}->timeout ($acctimeout) if ($acctimeout > 0.0); while ($run) { if ($evttimeout > 0) { my $difftime = time()-$rpcsrv->{hmccu}{evttime}; HMCCURPCPROC_Write ($rpcsrv, 'TO', $clkey, $difftime) if ($difftime >= $evttimeout); } # Send queue entries to parent process if (scalar (@queue) > 0) { HMCCU_Log ($name, 4, "RPC server $clkey sending data to FHEM"); my ($c, $m) = HMCCURPCPROC_SendQueue ($sockparent, $socktimeout, \@queue, $maxsnd); if ($c < 0) { $ioerrors++; $sioerrors++; if ($ioerrors >= $maxioerrors || $maxioerrors == 0) { HMCCU_Log ($name, 2, "Sending data to FHEM failed $ioerrors times. $m"); $ioerrors = 0; } } } # Next statement blocks for rpcAcceptTimeout seconds HMCCU_Log ($name, 4, "RPC server $clkey accepting connections"); my $connection = $rpcsrv->{__daemon}->accept (); next if (! $connection); last if (! $run); $connection->timeout ($conntimeout) if ($conntimeout > 0.0); HMCCU_Log ($name, 4, "RPC server $clkey processing request"); if ($prot eq 'A') { $rpcsrv->process_request ($connection); } else { HMCCURPCPROC_ProcessRequest ($rpcsrv, $connection); } shutdown ($connection, 2); close ($connection); undef $connection; } HMCCU_Log ($name, 1, "RPC server $clkey stopped handling connections. PID=$pid"); close ($rpcsrv->{__daemon}) if ($prot eq 'B'); # Send statistic info HMCCURPCPROC_WriteStats ($rpcsrv, $clkey); # Send exit information HMCCURPCPROC_Write ($rpcsrv, 'EX', $clkey, "SHUTDOWN|$pid"); # Send queue entries to parent process. Resend on error to ensure that EX event is sent my ($c, $m) = HMCCURPCPROC_SendQueue ($sockparent, $socktimeout, \@queue, 0); if ($c < 0) { HMCCU_Log ($name, 4, "Sending data to FHEM failed. $m"); # Wait 1 second and try again sleep (1); HMCCURPCPROC_SendQueue ($sockparent, $socktimeout, \@queue, 0); } # Log statistic counters foreach my $et (@RPC_EVENT_TYPES) { HMCCU_Log ($name, 4, "$clkey event type = $et: ".$rpcsrv->{hmccu}{rec}{$et}); } HMCCU_Log ($name, 2, "Number of I/O errors = $sioerrors"); return; } ###################################################################### # Send queue data to parent process. # Return number of queue elements sent to parent process or # (-1, errormessage) on error. ###################################################################### sub HMCCURPCPROC_SendQueue ($$$$) { my ($sockparent, $socktimeout, $queue, $maxsnd) = @_; my $fd = fileno ($sockparent); my $msg = ''; my $win = ''; vec ($win, $fd, 1) = 1; my $nf = select (undef, $win, undef, $socktimeout); if ($nf <= 0) { $msg = $nf == 0 ? 'select found no reader' : $!; return (-1, $msg); } my $sndcnt = 0; while (my $snddata = shift @{$queue}) { my ($bytes, $err) = HMCCURPCPROC_SendData ($sockparent, $snddata); if ($bytes == 0) { # Put item back in queue unshift @{$queue}, $snddata; $msg = $err; $sndcnt = -1; last; } $sndcnt++; last if ($sndcnt == $maxsnd && $maxsnd > 0); } return ($sndcnt, $msg); } ###################################################################### # Check if file descriptor is writeable and write data. # Return number of bytes written and error message. ###################################################################### sub HMCCURPCPROC_SendData ($$) { my ($sockparent, $data) = @_; my $bytes = 0; my $err = ''; my $size = pack ("N", length($data)); my $msg = $size . $data; $bytes = syswrite ($sockparent, $msg); if (!defined($bytes)) { $err = $!; $bytes = 0; } elsif ($bytes != length($msg)) { $err = 'send: incomplete data'; } return ($bytes, $err); } ###################################################################### # Check if file descriptor is readable and read data. # Return (data, '') on success # Return (undef, errmsg) on error ###################################################################### sub HMCCURPCPROC_ReceiveData ($$) { my ($fh, $timeout) = @_; my $header; my $data; my $err = ''; my ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($fh, $timeout); return (undef, $msg) if ($st <= 0); # Read datagram size my $sbytes = sysread ($fh, $header, 4); if (!defined($sbytes)) { return (undef, $!); } elsif ($sbytes != 4) { return (undef, 'receive: short header'); } # Read datagram my $size = unpack ('N', $header); my $bytes = sysread ($fh, $data, $size); if (!defined ($bytes)) { return (undef, $!); } elsif ($bytes != $size) { return (undef, 'receive: incomplete data'); } return ($data, $err); } ###################################################################### # Read data from socket # Return (-1, ErrorStr) on error. # Return (0, 'read: no data') if no data available. # Return (BytesRead, Data) on success. ###################################################################### sub HMCCURPCPROC_ReadFromSocket ($$$) { my ($hash, $socket, $timeout) = @_; my $data = ''; my $totalBytes = 0; my ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($socket, $timeout); while ($st > 0) { my $buffer; my $bytes = sysread ($socket, $buffer, 10000); return (-1, $!) if (!defined($bytes)); last if ($bytes == 0); $data .= $buffer; $totalBytes += $bytes; ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($socket, $timeout); } return $st < 0 ? ($st, $msg) : ($totalBytes, $data); } ###################################################################### # Check if data is available for reading from socket ###################################################################### sub HMCCURPCPROC_DataAvailableOnSocket ($$) { my ($socket, $timeout) = @_; my $fd = fileno ($socket); my $rin = ''; vec ($rin, $fd, 1) = 1; my $nfound = select ($rin, undef, undef, $timeout); if ($nfound < 0) { return (-1, $!); } elsif ($nfound == 0) { return (0, 'read: no data'); } return (1, ''); } ###################################################################### # Write data to socket # Return (-1, ErrorStr) on error. # Return (0, 'write: no reader') if no reading process on remote host. # Return (BytesWritten, 'OK') on success. ###################################################################### sub HMCCURPCPROC_WriteToSocket ($$$) { my ($socket, $data, $timeout) = @_; my $fd = fileno ($socket); my $win = ''; vec ($win, $fd, 1) = 1; my $nfound = select (undef, $win, undef, $timeout); if ($nfound < 0) { return (-1, $!); } elsif ($nfound == 0) { return (0, 'write: no reader'); } my $size = syswrite ($socket, $data); return defined($size) ? ($size, 'OK') : (-1, $!); } ###################################################################### # Write event into queue. ###################################################################### sub HMCCURPCPROC_Write ($$$$) { my ($server, $et, $cb, $msg) = @_; my $name = $server->{hmccu}{name}; if (defined($server->{hmccu}{eventqueue})) { my $queue = $server->{hmccu}{eventqueue}; my $ev = "$et|$cb|$msg"; $server->{hmccu}{evttime} = time (); if (defined($server->{hmccu}{queuesize}) && scalar(@{$queue}) >= $server->{hmccu}{queuesize}) { return HMCCU_Log ($name, 1, "$cb maximum queue size reached. Dropping event."); } HMCCU_Log ($name, 2, "Event = $ev") if ($server->{hmccu}{ccuflags} =~ /logEvents/); # Try to send events immediately. Put them in queue if send fails my $rc = 0; my $err = ''; if ($server->{hmccu}{ccuflags} !~ /queueEvents/) { ($rc, $err) = HMCCURPCPROC_SendData ($server->{hmccu}{sockparent}, $ev); HMCCU_Log ($name, 3, "SendData $ev $err") if ($rc == 0); } push (@{$queue}, $ev) if ($rc == 0); # Event statistics $server->{hmccu}{rec}{$et}++; $server->{hmccu}{rec}{total}++; $server->{hmccu}{snd}{$et}++; $server->{hmccu}{snd}{total}++; HMCCURPCPROC_WriteStats ($server, $cb) if ($server->{hmccu}{snd}{total} % $server->{hmccu}{statistics} == 0); } } ###################################################################### # Write statistics ###################################################################### sub HMCCURPCPROC_WriteStats ($$) { my ($server, $clkey) = @_; my $name = $server->{hmccu}{name}; if (defined ($server->{hmccu}{eventqueue})) { my $queue = $server->{hmccu}{eventqueue}; # Send statistic info my $st = $server->{hmccu}{snd}{total}; foreach my $et (@RPC_EVENT_TYPES) { $st .= '|'.$server->{hmccu}{snd}{$et}; $server->{hmccu}{snd}{$et} = 0; } HMCCU_Log ($name, 4, "Event statistics = $st"); push (@{$queue}, "ST|$clkey|$st"); } } ###################################################################### # Helper functions ###################################################################### ###################################################################### # Dump variable content as hex/ascii combination ###################################################################### sub HMCCURPCPROC_HexDump ($$) { my ($name, $data) = @_; if (!defined($data)) { HMCCU_Log ($name, 4, 'HexDump called without data'); return; } my $offset = 0; foreach my $chunk (unpack "(a16)*", $data) { my $hex = unpack "H*", $chunk; # hexadecimal $chunk =~ tr/ -~/./c; # replace unprintables $hex =~ s/(.{1,8})/$1 /gs; # insert spaces HMCCU_Log ($name, 4, sprintf "0x%08x (%05u) %-*s %s", $offset, $offset, 36, $hex, $chunk); $offset += 16; } } ###################################################################### # Build RPC server key ###################################################################### sub HMCCURPCPROC_GetKey ($) { my ($hash) = @_; return 'CB'.$hash->{rpcport}.$hash->{rpcid}; } ###################################################################### # Callback functions ###################################################################### ###################################################################### # Callback for new devices # Message format: # C|ADDRESS|TYPE|VERSION|null|null|PARAMSETS| # LINK_SOURCE_ROLES|LINK_TARGET_ROLES|DIRECTION| # null|PARENT|AES_ACTIVE # D|ADDRESS|TYPE|VERSION|FIRMWARE|RX_MODE|PARAMSETS| # null|null|null| # CHILDREN|null|null ###################################################################### sub HMCCURPCPROC_NewDevicesCB ($$$) { my ($server, $cb, $a) = @_; my $name = $server->{hmccu}{name}; my $devcount = scalar (@$a); HMCCU_Log ($name, 2, "$cb NewDevice received $devcount device and channel specifications"); # Format: # C/D|Address|Type|Version|Firmware|RxMode|Paramsets| # LinkSourceRoles|LinkTargetRoles|Direction|Children|Parent|AESActive foreach my $dev (@$a) { my $msg = ''; my $ps = ref($dev->{PARAMSETS}) eq 'ARRAY' ? join(',', @{$dev->{PARAMSETS}}) : $dev->{PARAMSETS}; if (defined($dev->{PARENT}) && $dev->{PARENT} ne '') { my $lsr = ref($dev->{LINK_SOURCE_ROLES}) eq 'ARRAY' ? join(',', @{$dev->{LINK_SOURCE_ROLES}}) : $dev->{LINK_SOURCE_ROLES}; my $ltr = ref($dev->{LINK_TARGET_ROLES}) eq 'ARRAY' ? join(',', @{$dev->{LINK_TARGET_ROLES}}) : $dev->{LINK_TARGET_ROLES}; $msg = 'C|'.$dev->{ADDRESS}.'|'.$dev->{TYPE}.'|'.$dev->{VERSION}. '|null|null|'.$ps.'|'.$lsr.'|'.$ltr.'|'.$dev->{DIRECTION}. '|null|'.$dev->{PARENT}."|".$dev->{AES_ACTIVE}; } else { # Wired devices do not have a RX_MODE attribute my $rx = exists ($dev->{RX_MODE}) ? $dev->{RX_MODE} : 'null'; $msg = 'D|'.$dev->{ADDRESS}.'|'.$dev->{TYPE}.'|'.$dev->{VERSION}."|". $dev->{FIRMWARE}.'|'.$rx.'|'.$ps.'|null|null|null|'. join(',',@{$dev->{CHILDREN}}).'|null|null'; } HMCCURPCPROC_Write ($server, 'ND', $cb, $msg); } return; } ################################################## # Callback for deleted devices ################################################## sub HMCCURPCPROC_DeleteDevicesCB ($$$) { my ($server, $cb, $a) = @_; my $name = $server->{hmccu}{name}; my $devCount = scalar (@$a); HMCCU_Log ($name, 2, "$cb DeleteDevice received $devCount device addresses"); foreach my $dev (@$a) { HMCCURPCPROC_Write ($server, 'DD', $cb, $dev); } return; } ################################################## # Callback for modified devices ################################################## sub HMCCURPCPROC_UpdateDeviceCB ($$$$) { my ($server, $cb, $devid, $hint) = @_; my $name = $server->{hmccu}{name}; HMCCU_Log ($name, 2, "$cb updated device $devid with hint $hint"); HMCCURPCPROC_Write ($server, 'UD', $cb, $devid.'|'.$hint); return; } ################################################## # Callback for replaced devices ################################################## sub HMCCURPCPROC_ReplaceDeviceCB ($$$$) { my ($server, $cb, $devid1, $devid2) = @_; my $name = $server->{hmccu}{name}; HMCCU_Log ($name, 2, "$cb device $devid1 replaced by $devid2"); HMCCURPCPROC_Write ($server, 'RD', $cb, $devid1.'|'.$devid2); return; } ################################################## # Callback for readded devices ################################################## sub HMCCURPCPROC_ReaddDevicesCB ($$$) { my ($server, $cb, $a) = @_; my $name = $server->{hmccu}{name}; my $devcount = scalar(@$a); HMCCU_Log ($name, 2, "$cb ReaddDevice received $devcount device addresses"); foreach my $dev (@$a) { HMCCURPCPROC_Write ($server, 'RA', $cb, $dev); } return; } ################################################## # Callback for handling CCU events ################################################## sub HMCCURPCPROC_EventCB ($$$$$) { my ($server, $cb, $devid, $attr, $val) = @_; my $name = $server->{hmccu}{name}; my $etime = time (); HMCCURPCPROC_Write ($server, 'EV', $cb, $etime.'|'.$devid.'|'.$attr.'|'.$val); # Never remove this statement! return; } ################################################## # Callback for list devices ################################################## sub HMCCURPCPROC_ListDevicesCB ($$) { my ($server, $cb) = @_; $cb //= 'unknown'; my $name = $server->{hmccu}{name}; if ($server->{hmccu}{ccuflags} =~ /ccuInit/) { HMCCU_Log ($name, 1, "$cb ListDevices. Sending init to HMCCU"); HMCCURPCPROC_Write ($server, 'IN', $cb, 'INIT|1'); } return RPC::XML::array->new (); } ###################################################################### # RPC encoding functions ###################################################################### ###################################################################### # Convert value to RPC data type # Supported types are bool, boolean, int, integer, float, double, # base64, string, array or hash reference. # Type of parameter $value can be appended to value, separated by # a colon, i.e. 100:INTEGER # If type is undefined, type is detected. If type cannot be detected # value is returned as it is. ###################################################################### sub HMCCURPCPROC_XMLEncValue ($;$) { my ($value, $type) = @_; # Regular expression containing all supported scalar data types my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')$'; if ($value =~ /${re}/i) { $type = $1; $value =~ s/${re}//i; } # Try to detect type if type not specified if (!defined($type)) { if (ref($value) eq 'HASH') { $type = 'struct'; } elsif (ref($value) eq 'ARRAY') { $type = 'array'; } elsif (lc($value) =~ /^(true|false)$/) { $type = 'boolean'; } elsif ($value =~ /^[-+]?\d+$/) { $type = 'integer'; } elsif ($value =~ /^[-+]?[0-9]*\.[0-9]+$/) { $type = 'float'; } elsif ($value eq '' || $value =~ /^([a-zA-Z_ ]+|'.+'|".+")$/) { $type = 'string'; } } return $value if (!defined($type)); my $lcType = lc($type); $type = 'struct' if ($type eq 'hash'); if ($type eq 'struct') { my %struct = (); foreach my $k (keys %$value) { $struct{$k} = HMCCURPCPROC_XMLEncValue ($value->{$k}); } return RPC::XML::struct->new (\%struct); } elsif ($type eq 'array') { return RPC::XML::array->new (map { HMCCURPCPROC_XMLEncValue ($_); } @$value); } elsif ($lcType =~ /^bool/ && uc($value) =~ /^(TRUE|FALSE|0|1)$/) { return RPC::XML::boolean->new ($value); } elsif ($lcType =~ /^int/ && $value =~ /^[-+]?\d+$/) { return RPC::XML::int->new ($value); } elsif ($lcType =~ /^(float|double)$/ && $value =~ /^[-+]?[0-9]*\.[0-9]+$/) { return RPC::XML::double->new ($value); } elsif ($lcType =~ /^base/) { return RPC::XML::base64->new ($value); } elsif ($lcType =~ /^str/) { return RPC::XML::string->new ($value); } else { return $value; } } ###################################################################### # Encode integer (type = 1) ###################################################################### sub HMCCURPCPROC_EncInteger ($) { my ($v) = @_; return pack ('Nl', $BINRPC_INTEGER, $v); } ###################################################################### # Encode bool (type = 2) ###################################################################### sub HMCCURPCPROC_EncBool ($) { my ($v) = @_; $v = 1 if ($v eq 'true'); $v = 0 if ($v eq 'false'); return pack ('NC', $BINRPC_BOOL, $v); } ###################################################################### # Encode string (type = 3) # Input is string. Empty string = void ###################################################################### sub HMCCURPCPROC_EncString ($) { my ($v) = @_; return pack ('NN', $BINRPC_STRING, length ($v)).$v; } ###################################################################### # Encode name # Encoded data will only contain the length and the name, no type. ###################################################################### sub HMCCURPCPROC_EncName ($) { my ($v) = @_; return pack ('N', length ($v)).$v; } ###################################################################### # Encode double (type = 4) ###################################################################### sub HMCCURPCPROC_EncDouble ($) { my ($v) = @_; my $m = 0; my $e = 0; if ($v != 0.0) { $e = int(log(abs($v))/log(2.0))+1; $m = int($v/(2**$e)*0x40000000); } return pack ('NNN', $BINRPC_DOUBLE, $m, $e); } ###################################################################### # Encode base64 (type = 17) # Input is base64 encoded string ###################################################################### sub HMCCURPCPROC_EncBase64 ($) { my ($v) = @_; return pack ('NN', $BINRPC_DOUBLE, length ($v)).$v; } ###################################################################### # Encode array (type = 256) # Input is array reference ###################################################################### sub HMCCURPCPROC_EncArray ($) { my ($a) = @_; my $r = ''; my $s = 0; # Number of elements in array if (defined($a)) { return '' if (ref($a) ne 'ARRAY'); while (my $v = shift @$a) { $r .= HMCCURPCPROC_EncType ($v); $s++; } } return pack ('NN', $BINRPC_ARRAY, $s).$r; } ###################################################################### # Encode struct (type = 257) # Input is hash reference. ###################################################################### sub HMCCURPCPROC_EncStruct ($) { my ($h) = @_; my $r = ''; my $s = 0; # Number of elements in structure if (defined($h)) { return '' if (ref($h) ne 'HASH'); foreach my $k (keys %{$h}) { my $n = HMCCURPCPROC_EncName ($k); if ($n ne '') { $r .= $n.HMCCURPCPROC_EncType ($h->{$k}); $s++; } } } return pack ('NN', $BINRPC_STRUCT, $s).$r; } ###################################################################### # Encode any type # Input is value and optionally type. # Value can be in format Value:Type # Types are: STRING, INTEGER, BOOL, FLOAT, DOUBLE, BASE64 # Return encoded data or empty string on error ###################################################################### sub HMCCURPCPROC_EncType ($;$) { my ($v, $t) = @_; return '' if (!defined($v)); my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; my $pt = ''; if (ref($v) eq 'ARRAY') { $pt = 'ARRAY'; } elsif (ref($v) eq 'HASH') { $pt = 'STRUCT'; } elsif ($v =~ /${re}/) { $pt = $1; $v =~ s/${re}//; } $t = $BINRPC_TYPE_MAPPING{uc($pt)} if ($pt ne '' && exists($BINRPC_TYPE_MAPPING{uc($pt)})); $t //= HMCCURPCPROC_DetType ($v); if ($t == $BINRPC_INTEGER) { return HMCCURPCPROC_EncInteger ($v); } elsif ($t == $BINRPC_BOOL) { return HMCCURPCPROC_EncBool ($v); } elsif ($t == $BINRPC_STRING) { return HMCCURPCPROC_EncString ($v); } elsif ($t == $BINRPC_DOUBLE) { return HMCCURPCPROC_EncDouble ($v); } elsif ($t == $BINRPC_BASE64) { return HMCCURPCPROC_EncBase64 ($v); } elsif ($t == $BINRPC_ARRAY) { return HMCCURPCPROC_EncArray ($v); } elsif ($t == $BINRPC_STRUCT) { return HMCCURPCPROC_EncStruct ($v); } return ''; } ###################################################################### # Detect type # Default type is STRING ###################################################################### sub HMCCURPCPROC_DetType ($) { my ($v) = @_; if (ref($v) eq 'ARRAY') { return $BINRPC_ARRAY; } if (ref($v) eq 'HASH') { return $BINRPC_STRUCT; } if (HMCCU_IsIntNum($v)) { return $BINRPC_INTEGER; } if (HMCCU_IsFltNum($v)) { return $BINRPC_DOUBLE; } if ($v eq 'true' || $v eq 'false') { return $BINRPC_BOOL; } return $BINRPC_STRING; } ###################################################################### # Encode RPC request with method and optional parameters. # Headers are not supported. # Input is method name and reference to parameter array. # Array must contain parameters in format value[:type]. Default for # type is STRING. # Return encoded data or empty string on error ###################################################################### # Binary RPC request format: # # Offset Size Description # 0 3 'Bin' # 3 1 Type: 0=Request, 1=Response # 4 4 Total length of data: n+4(ml)+4(pc)+p # 8 4 Length of request method name (ml) # 12 n Request method name # 12+n 4 Number of parameters (pc) # 16+n p Encoded parameters ###################################################################### sub HMCCURPCPROC_EncodeRequest ($$) { my ($method, $args) = @_; return '' if (!defined($method) || $method eq ''); # Encode method my $methodEnc = HMCCURPCPROC_EncName ($method); # Encode parameters my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; my $content = ''; my $s = 0; while (my $p = shift @$args) { my $encType = HMCCURPCPROC_EncType ($p); return '' if ($encType eq ''); $content .= $encType; $s++; } my $header = pack ('NN', $BINRPC_REQUEST, 8+length($method)+length($content)). $methodEnc.pack('N', $s); return $header.$content; } ###################################################################### # Encode RPC response # Input is type and value ###################################################################### sub HMCCURPCPROC_EncodeResponse ($;$) { my ($v, $t) = @_; if (defined ($v)) { my $r = HMCCURPCPROC_EncType ($v, $t); # BINRPC is not a standard. Some implementations require an offset of 8 to be added return pack ('NN', $BINRPC_RESPONSE, length($r)+8).$r; } else { return pack ('NN', $BINRPC_RESPONSE); } } ###################################################################### # Binary RPC decoding functions ###################################################################### ###################################################################### # Decode integer (type = 1) # Return (value, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecInteger ($$$) { my ($d, $i, $u) = @_; return ($i+4 <= length ($d)) ? (unpack ($u, substr ($d, $i, 4)), 4) : (undef, undef); } ###################################################################### # Decode bool (type = 2) # Return (value, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecBool ($$) { my ($d, $i) = @_; return ($i+1 <= length ($d)) ? (unpack ('C', substr ($d, $i, 1)), 1) : (undef, undef); } ###################################################################### # Decode string or void (type = 3) # Return (string, packet size) or (undef, undef) # Return ('', 4) for special type 'void' ###################################################################### sub HMCCURPCPROC_DecString ($$) { my ($d, $i) = @_; my ($s, $o) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); if (defined($s) && $i+$s+4 <= length ($d)) { return $s > 0 ? (substr ($d, $i+4, $s), $s+4) : ('', 4); } return (undef, undef); } ###################################################################### # Decode double (type = 4) # Return (value, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecDouble ($$) { my ($d, $i) = @_; return (undef, undef) if ($i+8 > length($d)); my $m = unpack ('l', reverse (substr ($d, $i, 4))); my $e = unpack ('l', reverse (substr ($d, $i+4, 4))); $m = $m/(1<<30); my $v = $m*(2**$e); return (sprintf ("%.6f",$v), 8); } ###################################################################### # Decode base64 encoded string (type = 17) # Return (string, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecBase64 ($$) { my ($d, $i) = @_; return HMCCURPCPROC_DecString ($d, $i); } ###################################################################### # Decode array (type = 256) # Return (arrayref, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecArray ($$) { my ($d, $i) = @_; my @r = (); my ($s, $x) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); if (defined($s)) { my $j = $x; for (my $n=0; $n<$s; $n++) { my ($v, $o) = HMCCURPCPROC_DecType ($d, $i+$j); return (undef, undef) if (!defined($o)); push (@r, $v); $j += $o; } return (\@r, $j); } return (undef, undef); } ###################################################################### # Decode struct (type = 257) # Return (hashref, packetsize) or (undef, undef) ###################################################################### sub HMCCURPCPROC_DecStruct ($$) { my ($d, $i) = @_; my %r; my ($s, $x) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); if (defined($s)) { my $j = $x; for (my $n=0; $n<$s; $n++) { my ($k, $o1) = HMCCURPCPROC_DecString ($d, $i+$j); return (undef, undef) if (!defined($o1)); my ($v, $o2) = HMCCURPCPROC_DecType ($d, $i+$j+$o1); return (undef, undef) if (!defined($o2)); $r{$k} = $v; $j += $o1+$o2; } return (\%r, $j); } return (undef, undef); } ###################################################################### # Decode any type # Return (element, packetsize) or (undef, undef) # element could be a scalar, array ref or hash ref. ###################################################################### sub HMCCURPCPROC_DecType ($$) { my ($d, $i) = @_; return (undef, undef) if ($i+4 > length ($d)); my @r = (); my $t = unpack ('N', substr ($d, $i, 4)); $i += 4; if ($t == $BINRPC_INTEGER) { # Integer @r = HMCCURPCPROC_DecInteger ($d, $i, 'N'); } elsif ($t == $BINRPC_BOOL) { # Bool @r = HMCCURPCPROC_DecBool ($d, $i); } elsif ($t == $BINRPC_STRING || $t == $BINRPC_BASE64) { # String / Base64 @r = HMCCURPCPROC_DecString ($d, $i); } elsif ($t == $BINRPC_DOUBLE) { # Double @r = HMCCURPCPROC_DecDouble ($d, $i); } elsif ($t == $BINRPC_ARRAY) { # Array @r = HMCCURPCPROC_DecArray ($d, $i); } elsif ($t == $BINRPC_STRUCT) { # Struct @r = HMCCURPCPROC_DecStruct ($d, $i); } $r[1] += 4; return @r; } ###################################################################### # Decode request. # Return method, arguments. Arguments are returned as array. ###################################################################### sub HMCCURPCPROC_DecodeRequest ($) { my ($data) = @_; my @r = (); my $i = 8; return (undef, undef) if (length ($data) < 8); # Decode method my ($method, $o) = HMCCURPCPROC_DecString ($data, $i); return (undef, undef) if (!defined ($method)); $i += $o; my $c = unpack ('N', substr ($data, $i, 4)); $i += 4; for (my $n=0; $n<$c; $n++) { my ($d, $s) = HMCCURPCPROC_DecType ($data, $i); return (undef, undef) if (!defined($d)|| !defined($s)); push (@r, $d); $i += $s; } return (lc($method), \@r); } ###################################################################### # Decode response. # Return (ref, type) or (undef, undef) # type: 1=ok, 0=error ###################################################################### sub HMCCURPCPROC_DecodeResponse ($) { my ($data) = @_; return (undef, 0) if (length($data) < 8); my $id = unpack ('N', substr ($data, 0, 4)); if ($id == $BINRPC_RESPONSE) { # Data my ($result, $offset) = HMCCURPCPROC_DecType ($data, 8); return ($result, defined($result) ? 1 : 0); } elsif ($id == $BINRPC_ERROR) { # Error my ($result, $offset) = HMCCURPCPROC_DecType ($data, 8); return ($result, 0); } # Response with header not supported # elsif ($id == 0x42696E41) { # } return (undef, 0); } 1; =pod =item device =item summary provides RPC server for connection between FHEM and Homematic CCU2 =begin html

HMCCURPCPROC

    The module provides a subprocess based RPC server for receiving events from HomeMatic CCU2. A HMCCURPCPROC device acts as a client device for a HMCCU I/O device. Normally RPC servers of type HMCCURPCPROC are started or stopped from HMCCU I/O device via command 'set rpcserver on,off'. HMCCURPCPROC devices will be created automatically by I/O device when RPC server is started. There should be no need for creating HMCCURPCPROC devices manually.

    Define

      define <name> HMCCURPCPROC {<HostOrIP>|iodev=<DeviceName>} {<port>|<interface>}

      Examples:
      define myccurpc HMCCURPCPROC 192.168.1.10 2001
      define myccurpc HMCCURPCPROC iodev=myccudev BidCos-RF


      The parameter HostOrIP is the hostname or IP address of a Homematic CCU2. The I/O device can also be specified with parameter iodev. If more than one CCU exist it's highly recommended to specify IO device with option iodev. Supported interfaces or ports are:
      PortInterface
      2000BidCos-Wired
      2001BidCos-RF
      2010HmIP-RF
      7000HVL
      8701CUxD
      9292Virtual

    Set

    • set <name> deregister
      Deregister RPC server at CCU.

    • set <name> register
      Register RPC server at CCU. RPC server must be running. Helpful when CCU lost connection to FHEM and events timed out.

    • set <name> rpcrequest <method> [{<value[:type]>|<parameter>=<value[:type]>|'!STRUCT'} ...]
      Send RPC request to CCU. The result is displayed in FHEM browser window. See EQ-3 RPC XML documentation for mor information about valid methods and requests.
      If type is not speicifed, it's detected automatically. Valid types are:
      INTEGER, BOOL, FLOAT, DOUBLE, BASE64, STRING (defaul)
      The command also supports passing a parameter structure. All parameters in format Name=Value[:Type] are treated as members of a structure. This structure will be appended to the list of the other parameters. If you like to insert the structure at a speicifc position in the parameter list, use '!STRUCT' as a placeholder.
      Example:
      set myRPCDev rpcrequest putParamset 123456 VALUES SET_POINT_TEMPERATURE=20:FLOAT SET_POINT_MODE=1
      Parameters SET_POINT_TEMPERATURE and SET_POINT_MODE will be converted to a structure. This structure is passed as the last parameter to the request.

    • set <name> rpcserver { on | off }
      Start or stop RPC server. This command is only available if expert mode is activated.

    Get

    • get <name> devicedesc [<fhem-device>|<address>]
      Read device and paramset descriptions for current RPC interface from CCU and store the information in I/O device. The device description is always read from CCU. The paramset description is only read if it doesn't exist in IO device. If a HMCCUCHN device or a channel address is specified, the description of the corresponding device address with all channels is read.

    • get <name> rpcevent
      Show RPC server events statistics. If attribute ccuflags contains flag 'statistics' the 3 devices which sent most events are listed.

    • get <name> rpcstate
      Show RPC process state.

    Attributes

    • ccuflags { flag-list }
      Set flags for controlling device behaviour. Meaning of flags is:
      ccuInit - RPC server initialization depends on ListDevice RPC call issued by CCU. This flag is not supported by interfaces CUxD and HVL.
      expert - Activate expert mode
      logEvents - Events are written into FHEM logfile if verbose is 4
      noEvents - Ignore events from CCU, do not update client device readings.
      noInitalUpdate - Do not update devices after RPC server started.
      noMulticalls - Do not execute RPC requests as multicalls (only BidCos-RF)
      queueEvents - Always write events into queue and send them asynchronously to FHEM. Frequency of event transmission to FHEM depends on attribute rpcConnTimeout.
      statistics - Count events per device sent by CCU

    • rpcAcceptTimeout <seconds>
      Specify timeout for accepting incoming connections. Default is 1 second. Increase this value by 1 or 2 seconds on slow systems.

    • rpcConnTimeout <seconds>
      Specify timeout of incoming CCU connections. Default is 1 second. Value must be greater than 0.

    • rpcEventTimeout <seconds>
      Specify timeout for CCU events. Default is 0, timeout is ignored. If timeout occurs an event is triggered. If ccuflag reconnect is set in I/O device the RPC device tries to establish a new connection to the CCU.

    • rpcMaxEvents <count>
      Specify maximum number of events read by FHEM during one I/O loop. If FHEM performance slows down decrease this value and increase attribute rpcQueueSize. Default value is 100. Value must be greater than 0.

    • rpcMaxIOErrors <count>
      Specifiy maximum number of I/O errors allowed when sending events to FHEM before a message is written into FHEM log file. Default value is 100. Set this attribute to 0 to disable error counting.

    • rpcPingCCU <interval>
      Ignored. Should be set in I/O device.

    • rpcQueueSend <events>
      Maximum number of events sent to FHEM per accept loop. Default is 70. If set to 0 all events in queue are sent to FHEM. Transmission is stopped when an I/O error occurrs or specified number of events has been sent.

    • rpcQueueSize <count>
      Specify maximum size of event queue. When this limit is reached no more CCU events are forwarded to FHEM. In this case increase this value or increase attribute rpcMaxEvents. Default value is 500.

    • rpcReadTimeout <seconds>
      Wait the specified time for socket to become readable. Default value is 0.005 seconds. When using a CCU2 and parameter set definitions cannot be read (timeout), increase this value, i.e. to 0.01. Drawback: This could slow down the FHEM start time.

    • rpcRetryRequest <retries>
      Number of times, failed RPC requests are repeated. Default is 1. Parameter retries must be in range 0-2.

    • rpcServerAddr <ip-address>
      Set local IP address of RPC servers on FHEM system. If attribute is missing the corresponding attribute of I/O device (HMCCU device) is used or IP address is detected automatically. This attribute should be set if FHEM is running on a system with multiple network interfaces.

    • rpcServerPort <port>
      Specify TCP port number used for calculation of real RPC server ports. If attribute is missing the corresponding attribute of I/O device (HMCCU device) is used. Default value is 5400.

    • rpcStatistics <count>
      Specify amount of events after which statistic data is sent to FHEM. Default value is 500.

    • rpcWriteTimeout <seconds>
      Wait the specified time for socket to become writeable. Default value is 0.001 seconds.
=end html =cut