diff --git a/fhem/CHANGED b/fhem/CHANGED index 5c8f2d8fe..fea3edfd7 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - update: 88_HMCCU: Version 4.2 + - new: 88_HMCCURPCPROC: Sub process based RPC server for HMCCU. - feature: 73_PRESENCE: new collectord package (RSSI based room selection for BTLE devices (lepresenced), ping command for daemon monitoring, systemd service unit diff --git a/fhem/FHEM/88_HMCCU.pm b/fhem/FHEM/88_HMCCU.pm index 3b4ac5aa7..c4e84af2b 100755 --- a/fhem/FHEM/88_HMCCU.pm +++ b/fhem/FHEM/88_HMCCU.pm @@ -4,19 +4,20 @@ # # $Id$ # -# Version 4.1.004 +# Version 4.2 # # Module for communication between FHEM and Homematic CCU2. # # Supports BidCos-RF, BidCos-Wired, HmIP-RF, virtual CCU channels, -# CCU group devices, HomeGear, CUxD, Osram Lightify. +# CCU group devices, HomeGear, CUxD, Osram Lightify, Homematic Virtual Layer # -# (c) 2017 by zap (zap01 t-online de) +# (c) 2018 by zap (zap01 t-online de) # ############################################################################## # # define HMCCU [ccunumber] [waitforccu=] # +# set ackmessages # set cleardefaults # set defaults # set delete [{ OT_VARDP | OT_DEVICE }] @@ -44,19 +45,18 @@ # get updateccu [ [{ State | Value }]] # get vars # -# attr ccuackstate { 0 | 1 } # attr ccuaggregate # attr ccudef-hmstatevals # attr ccudef-readingfilter # attr ccudef-readingname # attr ccudef-substitute # attr ccudefaults -# attr ccuflags { intrpc,extrpc,dptnocheck,noagg } +# attr ccuflags { ackState,intrpc,extrpc,procrpc,dptnocheck,noagg,logEvents } # attr ccuget { State | Value } # attr ccureadings { 0 | 1 } # attr parfile # attr rpcevtimeout -# attr rpcinterfaces { BidCos-Wired, BidCos-RF, HmIP-RF, VirtualDevices, Homegear } +# attr rpcinterfaces { BidCos-Wired, BidCos-RF, HmIP-RF, VirtualDevices, Homegear, HVL } # attr rpcinterval # attr rpcport # attr rpcqueue @@ -105,31 +105,39 @@ my %HMCCU_CUST_CHN_DEFAULTS; my %HMCCU_CUST_DEV_DEFAULTS; # HMCCU version -my $HMCCU_VERSION = '4.1.004'; +my $HMCCU_VERSION = '4.2'; # Default RPC port (BidCos-RF) my $HMCCU_RPC_PORT_DEFAULT = 2001; +my $HMCCU_RPC_INTERFACE_DEFAULT = 'BidCos-RF'; + +# Constants and default values +my $HMCCU_MAX_IOERRORS = 100; +my $HMCCU_MAX_QUEUESIZE = 500; +my $HMCCU_TIME_WAIT = 100000; +my $HMCCU_TIME_TRIGGER = 10; +my $HMCCU_TIMEOUT_CONNECTION = 10; +my $HMCCU_TIMEOUT_WRITE = 0.001; +my $HMCCU_TIMEOUT_ACCEPT = 1; +my $HMCCU_TIMEOUT_EVENT = 600; +my $HMCCU_STATISTICS = 500; # RPC port name by port number my %HMCCU_RPC_NUMPORT = ( 2000 => 'BidCos-Wired', 2001 => 'BidCos-RF', 2010 => 'HmIP-RF', 9292 => 'VirtualDevices', - 2003 => 'Homegear', 8701 => 'CUxD' + 2003 => 'Homegear', 8701 => 'CUxD', 7000 => 'HVL' ); # RPC port number by port name my %HMCCU_RPC_PORT = ( 'BidCos-Wired', 2000, 'BidCos-RF', 2001, 'HmIP-RF', 2010, 'VirtualDevices', 9292, - 'Homegear', 2003, 'CUxD', 8701 + 'Homegear', 2003, 'CUxD', 8701, 'HVL', 7000 ); -# RPC URL extensions by port number -my %HMCCU_RPC_URL = ( - 9292, 'groups' -); - -# RPC protocol types by port name. A=ASCII, B=Binary -my %HMCCU_RPC_PROT = ( - 2000 => 'A', 2001 => 'A', 2010 => 'A', 9292 => 'A', 2003 => 'A', 8701 => 'B' +# RPC flags +my %HMCCU_RPC_FLAG = ( + 2000 => 'forceASCII', 2001 => 'forceASCII', 2003 => '_', 2010 => '_', + 7000 => 'forceInit', 8701 => 'forceInit', 9292 => '_' ); # Initial intervals for registration of RPC callbacks and reading RPC queue @@ -188,6 +196,9 @@ my $HMCCU_FLAGS_NCD = $HMCCU_FLAG_NAME | $HMCCU_FLAG_CHANNEL | $HMCCU_FLAG_DATAP # Default values my $HMCCU_DEF_HMSTATE = '^0\.UNREACH!(1|true):unreachable;^[0-9]\.LOW_?BAT!(1|true):warn_battery'; +# Placeholder for external addresses +my $HMCCU_EXT_ADDR = 'ZZZ0000000'; + # Binary RPC data types my $BINRPC_INTEGER = 1; my $BINRPC_BOOL = 2; @@ -221,16 +232,21 @@ sub HMCCU_Trace ($$$$); sub HMCCU_Log ($$$$); sub HMCCU_SetError ($@); sub HMCCU_SetState ($@); +sub HMCCU_SetRPCState ($@); sub HMCCU_Substitute ($$$$$); sub HMCCU_SubstRule ($$$); sub HMCCU_SubstVariables ($$$); -sub HMCCU_UpdateClients ($$$$); +sub HMCCU_UpdateClients ($$$$$); sub HMCCU_UpdateDeviceTable ($$); sub HMCCU_UpdateSingleDatapoint ($$$$); sub HMCCU_UpdateSingleDevice ($$$); sub HMCCU_UpdateMultipleDevices ($$); sub HMCCU_UpdatePeers ($$$$); +sub HMCCU_GetRPCInterfaceList ($); sub HMCCU_GetRPCPortList ($); +sub HMCCU_GetRPCCallbackURL ($$$$$); +sub HMCCU_GetRPCServerInfo ($$$); +sub HMCCU_IsRPCType ($$$); sub HMCCU_RPCRegisterCallback ($); sub HMCCU_RPCDeRegisterCallback ($); sub HMCCU_ResetCounters ($); @@ -244,7 +260,7 @@ sub HMCCU_GetDeviceInfo ($$$); sub HMCCU_FormatDeviceInfo ($); sub HMCCU_GetFirmwareVersions ($); sub HMCCU_GetDeviceList ($); -sub HMCCU_GetDatapointList ($); +sub HMCCU_GetDatapointList ($$$); sub HMCCU_FindDatapoint ($$$$$); sub HMCCU_GetAddress ($$$$); sub HMCCU_IsDevAddr ($$); @@ -252,7 +268,7 @@ sub HMCCU_IsChnAddr ($$); sub HMCCU_SplitChnAddr ($); sub HMCCU_GetCCUObjectAttribute ($$$); sub HMCCU_FindClientDevices ($$$$); -sub HMCCU_GetRPCDevice ($$); +sub HMCCU_GetRPCDevice ($$$); sub HMCCU_FindIODevice ($); sub HMCCU_GetHash ($@); sub HMCCU_GetAttribute ($$$$); @@ -264,6 +280,7 @@ sub HMCCU_IsValidDeviceOrChannel ($$); sub HMCCU_IsValidDevice ($$); sub HMCCU_IsValidChannel ($$); sub HMCCU_GetCCUDeviceParam ($$); +sub HMCCU_GetDatapointAttr ($$$$$); sub HMCCU_GetValidDatapoints ($$$$$); sub HMCCU_GetSwitchDatapoint ($$$); sub HMCCU_IsValidDatapoint ($$$$$); @@ -339,13 +356,13 @@ sub HMCCU_Initialize ($) $hash->{ShutdownFn} = "HMCCU_Shutdown"; $hash->{parseParams} = 1; - $hash->{AttrList} = "stripchar stripnumber ccuackstate:0,1 ccuaggregate:textField-long". + $hash->{AttrList} = "stripchar stripnumber ccuaggregate:textField-long". " ccudefaults rpcinterfaces:multiple-strict,".join(',',sort keys %HMCCU_RPC_PORT). " ccudef-hmstatevals:textField-long ccudef-substitute:textField-long". " ccudef-readingname:textField-long ccudef-readingfilter:textField-long". " ccudef-readingformat:name,namelc,address,addresslc,datapoint,datapointlc". - " ccuflags:multiple-strict,extrpc,intrpc,dptnocheck,noagg,nohmstate ccureadings:0,1". - " rpcdevice rpcinterval:2,3,5,7,10 rpcqueue". + " ccuflags:multiple-strict,ackState,extrpc,intrpc,procrpc,dptnocheck,noagg,nohmstate,logEvents". + " ccureadings:0,1 rpcdevice rpcinterval:2,3,5,7,10 rpcqueue". " rpcport:multiple-strict,".join(',',sort keys %HMCCU_RPC_NUMPORT). " rpcserver:on,off rpcserveraddr rpcserverport rpctimeout rpcevtimeout parfile substitute". " ccuget:Value,State ". @@ -364,7 +381,7 @@ sub HMCCU_Define ($$) return "Specify CCU hostname or IP address as a parameter" if(scalar (@$a) < 3); $hash->{host} = $$a[2]; - $hash->{Clients} = ':HMCCUDEV:HMCCUCHN:HMCCURPC:'; + $hash->{Clients} = ':HMCCUDEV:HMCCUCHN:HMCCURPC:HMCCURPCPROC:'; # Check if TCL-Rega process is running on CCU my $timeout = exists ($h->{waitforccu}) ? $h->{waitforccu} : 0; @@ -373,9 +390,17 @@ sub HMCCU_Define ($$) } else { $hash->{ccustate} = 'unreachable'; - Log3 $name, 1, "HMCCU: CCU2 is not reachable"; + Log3 $name, 1, "HMCCU: CCU2 port 8181 is not reachable"; + } + + # Get CCU IP address + my $ccuname = inet_aton ($hash->{host}); + if (defined ($ccuname)) { + my $ccuip = inet_ntoa ($ccuname); + $hash->{ccuip} = defined ($ccuip) ? $ccuip : 'N/A'; } + # Get CCU number (if more than one) if (scalar (@$a) >= 4) { return "CCU number must be in range 1-9" if ($$a[3] < 1 || $$a[3] > 9); $hash->{CCUNum} = $$a[3]; @@ -394,16 +419,22 @@ sub HMCCU_Define ($$) $hash->{version} = $HMCCU_VERSION; $hash->{ccutype} = 'CCU2'; - $hash->{RPCState} = "stopped"; + $hash->{RPCState} = "inactive"; Log3 $name, 1, "HMCCU: Device $name. Initialized version $HMCCU_VERSION"; - my ($devcnt, $chncnt) = HMCCU_GetDeviceList ($hash); + my ($devcnt, $chncnt, $ifcount) = HMCCU_GetDeviceList ($hash); if ($devcnt > 0) { Log3 $name, 1, "HMCCU: Read $devcnt devices with $chncnt channels from CCU ".$hash->{host}; } else { Log3 $name, 1, "HMCCU: No devices read from CCU ".$hash->{host}; } + if ($ifcount > 0) { + Log3 $name, 1, "HMCCU: Read $ifcount interfaces from CCU ".$hash->{host}; + } + else { + Log3 $name, 1, "HMCCU: No RPC interfaces found on CCU ".$hash->{host}; + } $hash->{hmccu}{evtime} = 0; $hash->{hmccu}{evtimeout} = 0; @@ -413,7 +444,7 @@ sub HMCCU_Define ($$) readingsBeginUpdate ($hash); readingsBulkUpdate ($hash, "state", "Initialized"); - readingsBulkUpdate ($hash, "rpcstate", "stopped"); + readingsBulkUpdate ($hash, "rpcstate", "inactive"); readingsEndUpdate ($hash, 1); $attr{$name}{stateFormat} = "rpcstate/state"; @@ -445,15 +476,17 @@ sub HMCCU_Attr ($@) $rc = HMCCU_AggregationRules ($hash, $attrval); return HMCCU_SetError ($hash, "Syntax error in attribute ccuaggregate") if ($rc == 0); } + elsif ($attrname eq 'ccuackstate') { + return "HMCCU: Attribute ccuackstate is depricated. Use ccuflags with ackState instead"; + } elsif ($attrname eq 'ccuflags') { my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - if ($attrval =~ /extrpc/ && $attrval =~ /intrpc/) { - return "Flags extrpc and inttpc cannot be combined"; - } - if ($attrval =~ /(extrpc|intrpc)/) { + my @flags = ($attrval =~ /(intrpc|extrpc|procrpc)/g); + return "Flags extrpc, procrpc and intrpc cannot be combined" if (scalar (@flags) > 1); + if ($attrval =~ /(extrpc|intrpc|procrpc)/) { my $rpcmode = $1; if ($ccuflags !~ /$rpcmode/) { - return "Stop RPC server before switching between extrpc and intrpc" + return "Stop RPC server before switching RPC server" if (HMCCU_IsRPCServerRunning ($hash, undef, undef)); } } @@ -474,8 +507,9 @@ sub HMCCU_Attr ($@) my @ports = split (',', $attrval); my @plist = (); foreach my $p (@ports) { - return "Illegal RPC interface $p" if (!exists ($HMCCU_RPC_PORT{$p})); - push (@plist, $HMCCU_RPC_PORT{$p}); + my $pn = HMCCU_GetRPCServerInfo ($hash, $p, 'port'); + return "Illegal RPC interface $p" if (!defined ($pn)); + push (@plist, $pn); } return "No RPC interface specified" if (scalar (@plist) == 0); $hash->{hmccu}{rpcports} = join (',', @plist); @@ -485,8 +519,9 @@ sub HMCCU_Attr ($@) my @ports = split (',', $attrval); my @ilist = (); foreach my $p (@ports) { - return "Illegal RPC port $p" if (!exists ($HMCCU_RPC_NUMPORT{$p})); - push (@ilist, $HMCCU_RPC_NUMPORT{$p}); + my $pn = HMCCU_GetRPCServerInfo ($hash, $p, 'name'); + return "HMCCU: Illegal RPC port $p" if (!defined ($pn)); + push (@ilist, $pn); } return "No RPC port specified" if (scalar (@ilist) == 0); $hash->{hmccu}{rpcports} = $attrval; @@ -867,7 +902,7 @@ sub HMCCU_Notify ($$) return if ($rpcserver eq 'off'); my $delay = $HMCCU_INIT_INTERVAL0; Log3 $name, 0, "HMCCU: Start of RPC server after FHEM initialization in $delay seconds"; - if ($ccuflags =~ /extrpc/) { + if ($ccuflags =~ /(extrpc|procrpc)/) { InternalTimer (gettimeofday()+$delay, "HMCCU_StartExtRPCServer", $hash, 0); } else { @@ -1079,7 +1114,7 @@ sub HMCCU_Shutdown ($) my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); # Shutdown RPC server - if ($ccuflags =~ /extrpc/) { + if ($ccuflags =~ /(extrpc|procrpc)/) { HMCCU_StopExtRPCServer ($hash); } else { @@ -1102,16 +1137,15 @@ sub HMCCU_Set ($@) my $name = shift @$a; my $opt = shift @$a; my $options = "var delete execute hmscript cleardefaults:noArg defaults:noArg ". - "importdefaults rpcserver:on,off datapoint"; + "importdefaults rpcserver:on,off datapoint ackmessages:noArg"; my $usage = "HMCCU: Unknown argument $opt, choose one of $options"; my $host = $hash->{host}; - if ($opt ne 'rpcserver' && HMCCU_IsRPCStateBlocking ($hash)) { - return HMCCU_SetState ($hash, "busy", "HMCCU: CCU busy, choose one of rpcserver:off"); - } + return "HMCCU: CCU busy, choose one of rpcserver:off" + if ($opt ne 'rpcserver' && HMCCU_IsRPCStateBlocking ($hash)); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - $options .= ",restart" if ($ccuflags =~ /intrpc/); + $options .= ",restart" if ($ccuflags !~ /(extrpc|procrpc)/); my $stripchar = AttrVal ($name, "stripchar", ''); my $ccureadings = AttrVal ($name, "ccureadings", 1); my $readingformat = HMCCU_GetAttrReadingFormat ($hash, $hash); @@ -1193,13 +1227,14 @@ sub HMCCU_Set ($@) $response .= "$scr ".$HMCCU_SCRIPTS->{$scr}{syntax}."\n". $HMCCU_SCRIPTS->{$scr}{description}."\n\n"; } + + $response .= $usage; + return $response; } - $response .= $usage; + return HMCCU_SetError ($hash, $usage) if (defined ($dump) && $dump ne 'dump'); - return HMCCU_SetError ($hash, $response) - if (!defined ($script) || (defined ($dump) && $dump ne 'dump')); - + # Execute script $response = HMCCU_HMScriptExt ($hash, $script, $h); return HMCCU_SetError ($hash, -2, $response) if ($response =~ /^ERROR:/); @@ -1240,7 +1275,7 @@ sub HMCCU_Set ($@) if (!defined ($action) || $action !~ /^(on|off|restart)$/); if ($action eq 'on') { - if ($ccuflags =~ /extrpc/) { + if ($ccuflags =~ /(extrpc|procrpc)/) { return HMCCU_SetError ($hash, "Start of RPC server failed") if (!HMCCU_StartExtRPCServer ($hash)); } @@ -1250,7 +1285,7 @@ sub HMCCU_Set ($@) } } elsif ($action eq 'off') { - if ($ccuflags =~ /extrpc/) { + if ($ccuflags =~ /(extrpc|procrpc)/) { return HMCCU_SetError ($hash, "Stop of RPC server failed") if (!HMCCU_StopExtRPCServer ($hash)); } @@ -1262,10 +1297,9 @@ sub HMCCU_Set ($@) elsif ($action eq 'restart') { return "HMCCU: No RPC server running" if (!HMCCU_IsRPCServerRunning ($hash, undef, undef)); - if ($ccuflags =~ /intrpc/) { + if ($ccuflags !~ /(extrpc|procrpc)/) { return "HMCCU: Can't stop RPC server" if (!HMCCURPC_StopRPCServer ($hash)); - $hash->{RPCState} = "restarting"; - readingsSingleUpdate ($hash, "rpcstate", "restarting", 1); + HMCCU_SetRPCState ($hash, 'restarting'); DoTrigger ($name, "RPC server restarting"); } else { @@ -1275,6 +1309,11 @@ sub HMCCU_Set ($@) return HMCCU_SetState ($hash, "OK"); } + elsif ($opt eq 'ackmessages') { + my $response = HMCCU_HMScriptExt ($hash, "!ClearUnreachable", undef); + return HMCCU_SetError ($hash, -2, $response) if ($response =~ /^ERROR:/); + return HMCCU_SetState ($hash, "OK", "Unreach errors in CCU cleared"); + } elsif ($opt eq 'defaults') { my $rc = HMCCU_SetDefaults ($hash); return HMCCU_SetError ($hash, "HMCCU: No default attributes found") if ($rc == 0); @@ -1321,9 +1360,8 @@ sub HMCCU_Get ($@) my $usage = "HMCCU: Unknown argument $opt, choose one of $options"; my $host = $hash->{host}; - if ($opt ne 'rpcstate' && HMCCU_IsRPCStateBlocking ($hash)) { - return HMCCU_SetState ($hash, "busy", "HMCCU: CCU busy, choose one of rpcstate:noArg"); - } + return "HMCCU: CCU busy, choose one of rpcstate:noArg" + if ($opt ne 'rpcstate' && HMCCU_IsRPCStateBlocking ($hash)); my $ccuflags = AttrVal ($name, "ccuflags", "null"); my $ccureadings = AttrVal ($name, "ccureadings", 1); @@ -1389,7 +1427,7 @@ sub HMCCU_Get ($@) $ccuget = 'Attr' if (!defined ($ccuget)); return HMCCU_SetError ($hash, $usage) if ($ccuget !~ /^(Attr|State|Value)$/); - my ($co, $ce) = HMCCU_UpdateClients ($hash, $devexp, $ccuget, ($opt eq 'updateccu') ? 1 : 0); + my ($co, $ce) = HMCCU_UpdateClients ($hash, $devexp, $ccuget, ($opt eq 'updateccu') ? 1 : 0, undef); return HMCCU_SetState ($hash, "OK", "$co client devices successfully updated. Update for $ce client devices failed"); @@ -1440,7 +1478,29 @@ sub HMCCU_Get ($@) return HMCCU_FormatDeviceInfo ($result); } elsif ($opt eq 'rpcevents') { - if ($ccuflags =~ /intrpc/) { + if ($ccuflags =~ /extrpc/) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, undef); + return HMCCU_SetError ($hash, "HMCCU: External RPC server not found") if ($rpcdev eq ''); + $result = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); + return HMCCU_SetState ($hash, "OK", $result) if (defined ($result)); + return HMCCU_SetError ($hash, "No event statistics available"); + } + elsif ($ccuflags =~ /procrpc/) { + $result = ''; + my @iflist = HMCCU_GetRPCInterfaceList ($hash); + foreach my $ifname (@iflist) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + if ($rpcdev eq '') { + Log3 $name, 2, "HMCCU: Can't find HMCCURPCPROC device for interface $ifname"; + next; + } + my $res = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); + $result .= $res if (defined ($result)); + } + return HMCCU_SetState ($hash, "OK", $result) if ($result ne ''); + return HMCCU_SetError ($hash, "No event statistics available"); + } + else { return HMCCU_SetError ($hash, "No event statistics available") if (!exists ($hash->{hmccu}{evs}) || !exists ($hash->{hmccu}{evr})); foreach my $stkey (sort keys %{$hash->{hmccu}{evr}}) { @@ -1449,13 +1509,6 @@ sub HMCCU_Get ($@) } return HMCCU_SetState ($hash, "OK", $result); } - else { - my $rpcdev = HMCCU_GetRPCDevice ($hash, 0); - return HMCCU_SetError ($hash, "HMCCU: External RPC server not found") if ($rpcdev eq ''); - $result = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); - return HMCCU_SetState ($hash, "OK", $result) if (defined ($result)); - return HMCCU_SetError ($hash, "No event statistics available"); - } } elsif ($opt eq 'rpcstate') { my @hm_pids = (); @@ -1472,7 +1525,7 @@ sub HMCCU_Get ($@) return HMCCU_SetState ($hash, "OK", $result); } elsif ($opt eq 'devicelist') { - my ($devcount, $chncount) = HMCCU_GetDeviceList ($hash); + my ($devcount, $chncount, $ifcount) = HMCCU_GetDeviceList ($hash); return HMCCU_SetError ($hash, -2) if ($devcount < 0); return HMCCU_SetError ($hash, "No devices received from CCU") if ($devcount == 0); $result = "Read $devcount devices with $chncount channels from CCU"; @@ -1613,13 +1666,11 @@ sub HMCCU_Get ($@) } elsif ($opt eq 'exportdefaults') { my $filename = shift @$a; - $usage = "Usage: get $name $opt filename"; - + $usage = "Usage: get $name $opt filename"; return HMCCU_SetError ($hash, $usage) if (!defined ($filename)); my $rc = HMCCU_ExportDefaults ($filename); return HMCCU_SetError ($hash, -16) if ($rc == 0); - return HMCCU_SetState ($hash, "OK", "Default attributes written to $filename"); } elsif ($opt eq 'aggregation') { @@ -1649,7 +1700,6 @@ sub HMCCU_Get ($@) my ($rc, $res) = HMCCU_RPCGetConfig ($hash, $ccuobj, "getParamsetDescription", undef); return HMCCU_SetError ($hash, $rc) if ($rc < 0); - return HMCCU_SetState ($hash, "OK", $res); } else { @@ -1663,9 +1713,13 @@ sub HMCCU_Get ($@) ###################################################################### # Parse CCU object specification. -# Supports classic Homematic and Homematic-IP addresses. -# Supports team addresses with leading * for BidCos-RF. -# Supports CCU virtual remote addresses (BidCoS:ChnNo) +# +# Supported address types: +# Classic Homematic and Homematic-IP addresses. +# Team addresses with leading * for BidCos-RF. +# CCU virtual remote addresses (BidCoS:ChnNo) +# OSRAM lightify addresses (OL-...) +# Homematic virtual layer addresses (if known by HMCCU) # # Possible syntax for datapoints: # Interface.Address:Channel.Datapoint @@ -1691,6 +1745,21 @@ sub HMCCU_ParseObject ($$$) { my ($hash, $object, $flags) = @_; my ($i, $a, $c, $d, $n, $f) = ('', '', '', '', '', '', 0); + my $extaddr; + + # Check if address is already known by HMCCU. Substitute device address by ZZZ0000000 + # to allow external addresses like HVL + if ($object =~ /^.+\.(.+):[0-9]{1,2}\..+$/ || + $object =~ /^.+\.(.+):[0-9]{1,2}$/ || + $object =~ /^(.+):[0-9]{1,2}\..+$/ || + $object =~ /^(.+):[0-9]{1,2}$/ || + $object =~ /^(.+)$/) { + $extaddr = $1; + if (!HMCCU_IsDevAddr ($extaddr, 0) && + exists ($hash->{hmccu}{dev}{$extaddr}) && $hash->{hmccu}{dev}{$extaddr}{valid}) { + $object =~ s/$extaddr/$HMCCU_EXT_ADDR/; + } + } if ($object =~ /^(.+?)\.([\*]*[A-Z]{3}[0-9]{7}):([0-9]{1,2})\.(.+)$/ || $object =~ /^(.+?)\.([0-9A-F]{12,14}):([0-9]{1,2})\.(.+)$/ || @@ -1712,7 +1781,7 @@ sub HMCCU_ParseObject ($$$) $f = $HMCCU_FLAGS_IAC | ($flags & $HMCCU_FLAG_DATAPOINT); ($i, $a, $c, $d) = ($1, $2, $3, $flags & $HMCCU_FLAG_DATAPOINT ? '.*' : ''); } - elsif ($object =~ /^([\*]*[A-Z]{3}[0-9]{7}):([0-9]){1,2}\.(.+)$/ || + elsif ($object =~ /^([\*]*[A-Z]{3}[0-9]{7}):([0-9]{1,2})\.(.+)$/ || $object =~ /^([0-9A-F]{12,14}):([0-9]{1,2})\.(.+)$/ || $object =~ /^(OL-.+):([0-9]{1,2})\.(.+)$/ || $object =~ /^(BidCoS-RF):([0-9]{1,2})\.(.+)$/) { @@ -1759,6 +1828,9 @@ sub HMCCU_ParseObject ($$$) else { $f = 0; } + + # Restore external address (i.e. HVL device address) + $a = $extaddr if ($a eq $HMCCU_EXT_ADDR); # Check if name is a valid channel name if ($f & $HMCCU_FLAG_NAME) { @@ -1813,7 +1885,7 @@ sub HMCCU_FilterReading ($$$) my $devadd = ''; # Get channel name and channel number - if (HMCCU_IsChnAddr ($chn, 0)) { + if (HMCCU_IsValidChannel ($hmccu_hash, $chn)) { $chnnam = HMCCU_GetChannelName ($hmccu_hash, $chn, ''); ($devadd, $chnnum) = HMCCU_SplitChnAddr ($chn); } @@ -1863,7 +1935,7 @@ sub HMCCU_FilterReading ($$$) ) && $dpt =~ /$f/ ) ); - # Negative filter + # Negate filter return 1 if ( !$rm && ( ($cn ne '' && "$chnnum" ne "$cn") || @@ -1981,8 +2053,7 @@ sub HMCCU_GetReadingName ($$$$$$$) } ###################################################################### -# Format reading value depending on attribute stripnumber. Integer -# values are ignored. +# Format reading value depending on attribute stripnumber. # Syntax of attribute stripnumber: # [datapoint-expr!]format[;...] # Valid formats: @@ -1990,6 +2061,7 @@ sub HMCCU_GetReadingName ($$$$$$$) # 1 = Preserve 1 digit # 2 = Remove trailing zeroes # -n = Round value to specified number of digits (-0 is allowed) +# %f = Format for numbers. String suffix is allowed. ###################################################################### sub HMCCU_FormatReadingValue ($$$) @@ -1997,7 +2069,9 @@ sub HMCCU_FormatReadingValue ($$$) my ($hash, $value, $dpt) = @_; my $stripnumber = AttrVal ($hash->{NAME}, 'stripnumber', '0'); - return $value if ($stripnumber eq '0' || $value !~ /\.[0-9]+$/); + return $value if ($stripnumber eq '0' || $value !~ /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/); + + my $isint = $value =~ /^[+-]?[0-9]+$/ ? 1 : 0; foreach my $sr (split (';', $stripnumber)) { my ($d, $s) = split ('!', $sr); @@ -2008,16 +2082,19 @@ sub HMCCU_FormatReadingValue ($$$) $s = $sr; } - if ($s eq '1') { + if ($s eq '1' && !$isint) { return sprintf ("%.1f", $value); } - elsif ($s eq '2') { + elsif ($s eq '2' && !$isint) { return sprintf ("%g", $value); } - elsif ($s =~ /^-([0-9])$/) { + elsif ($s =~ /^-([0-9])$/ && !$isint) { my $fmt = '%.'.$1.'f'; return sprintf ($fmt, $value); } + elsif ($s =~ /^%.+$/) { + return sprintf ($s, $value); + } } return $value; @@ -2045,7 +2122,7 @@ sub HMCCU_Trace ($$$$) } ###################################################################### -# Log message and return code. +# Log message and return parameter rc. ###################################################################### sub HMCCU_Log ($$$$) @@ -2054,7 +2131,7 @@ sub HMCCU_Log ($$$$) my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - Log3 $name, $level, "$type: $msg"; + Log3 $name, $level, "$type: [$name] $msg"; return $rc; } @@ -2103,7 +2180,7 @@ sub HMCCU_SetError ($@) } ################################################################## -# Set state of device if attribute ccuackstate = 1 +# Set state of device if attribute ccuflags = ackState ################################################################## sub HMCCU_SetState ($@) @@ -2111,15 +2188,83 @@ sub HMCCU_SetState ($@) my ($hash, $text, $retval) = @_; my $name = $hash->{NAME}; - my $defackstate = $hash->{TYPE} eq 'HMCCU' ? 1 : 0; - my $ackstate = AttrVal ($name, 'ccuackstate', $defackstate); - return undef if ($ackstate == 0); + my $defackstate = $hash->{TYPE} eq 'HMCCU' ? 'ackState' : 'null'; + my $ccuflags = AttrVal ($name, 'ccuflags', $defackstate); - if (defined ($hash) && defined ($text)) { + if (defined ($hash) && defined ($text) && $ccuflags =~ /ackState/) { readingsSingleUpdate ($hash, "state", $text, 1); } - return ($text eq "busy") ? "HMCCU: CCU busy" : $retval; + return $retval; +} + +###################################################################### +# Set state of RPC server. +# Parameters iface and msg are optional. +###################################################################### + +sub HMCCU_SetRPCState ($@) +{ + my ($hash, $state, $iface, $msg) = @_; + my $name = $hash->{NAME}; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $filter; + my $f = 0; # Overall process states: 0=starting/stopping, 1=running/error, 2=stopped/error + + if ($ccuflags =~ /(intrpc|extrpc)/) { + $f = 1 if ($state eq 'running'); + $f = 2 if ($state eq 'inactive'); + } + elsif (defined ($iface) && $ccuflags =~ /procrpc/) { + # Set interface state + my $ifname = HMCCU_GetRPCServerInfo ($hash, $iface, 'name'); + $hash->{hmccu}{interfaces}{$ifname}{state} = $state if (defined ($ifname)); + + # Count number of processes in state running, error or inactive + my %statecount = ("running" => 0, "error" => 0, "inactive" => 0); + my @iflist = HMCCU_GetRPCInterfaceList ($hash); + foreach my $i (@iflist) { + my $st = $hash->{hmccu}{interfaces}{$i}{state}; + $statecount{$st}++ if (exists ($statecount{$st})); + if ($hash->{hmccu}{interfaces}{$i}{manager} eq 'HMCCU') { + $filter = defined ($filter) ? "$filter|$i" : $i; + } + } + + # Determine overall process state + $f = 1 if ($statecount{"running"}+$statecount{"error"} == scalar (@iflist)); + $f = 2 if ($statecount{"inactive"}+$statecount{"error"} == scalar (@iflist)); + return undef if ($f == 0); + } + + if ($state ne $hash->{RPCState}) { + # Update RPC state + $hash->{RPCState} = $state; + readingsSingleUpdate ($hash, "rpcstate", $state, 1); + + if ($f > 0) { + # Running or inactive + HMCCU_SetState ($hash, 'OK') if (ReadingsVal ($name, 'state', '') eq 'busy'); + HMCCU_Log ($hash, 1, "All RPC servers $state", undef); + DoTrigger ($name, "RPC server $state"); + + if ($f == 1) { + # If RPC process(es) running update all devices where interface is managed by HMCCU + my ($c_ok, $c_err) = HMCCU_UpdateClients ($hash, '.*', 'Attr', 0, $filter); + Log3 $name, 2, "HMCCU: Updated devices. Success=$c_ok Failed=$c_err"; + } + } + else { + # Starting or stopping + HMCCU_SetState ($hash, 'busy') if (ReadingsVal ($name, 'state', '') ne 'busy'); + } + + HMCCU_Log ($hash, 4, "Set rpcstate to $state", undef); + HMCCU_Log ($hash, 1, $msg, undef) if (defined ($msg)); + } + + return undef; } ###################################################################### @@ -2270,22 +2415,24 @@ sub HMCCU_SubstVariables ($$$) # specified regular expression. Update will fail if device is deleted # or disabled or if attribute ccureadings of a device is set to 0. # If fromccu is 1 regular expression is compared to CCU device name. -# Otherwise it's compared to FHEM device name. +# Otherwise it's compared to FHEM device name. If ifname is specified +# only devices belonging to interface ifname are updated. ###################################################################### -sub HMCCU_UpdateClients ($$$$) +sub HMCCU_UpdateClients ($$$$$) { - my ($hash, $devexp, $ccuget, $fromccu) = @_; + my ($hash, $devexp, $ccuget, $fromccu, $ifname) = @_; my $fhname = $hash->{NAME}; my $c_ok = 0; my $c_err = 0; + my $filter = "ccudevstate=active"; + $filter .= ",ccuif=$ifname" if (defined ($ifname)); if ($fromccu) { foreach my $name (sort keys %{$hash->{hmccu}{adr}}) { next if ($name !~ /$devexp/ || !($hash->{hmccu}{adr}{$name}{valid})); - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, - "ccudevstate=active"); + my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, $filter); foreach my $d (@devlist) { my $ch = $defs{$d}; @@ -2298,7 +2445,7 @@ sub HMCCU_UpdateClients ($$$$) Log3 $fhname, 3, "HMCCU: Device $name has no readable datapoints"; } else { - Log3 $fhname, 2, "HMCCU: Update of device $name failed" if ($rc != -10); + Log3 $fhname, 2, "HMCCU: Update of device $name failed"; } $c_err++; } @@ -2309,8 +2456,7 @@ sub HMCCU_UpdateClients ($$$$) } } else { - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", $devexp, - "ccudevstate=active"); + my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", $devexp, $filter); Log3 $fhname, 2, "HMCCU: No client devices matching $devexp" if (scalar (@devlist) == 0); foreach my $d (@devlist) { @@ -2341,7 +2487,7 @@ sub HMCCU_UpdateClients ($$$$) # Update parameters in internal device tables and client devices. # Parameter devices is a hash reference with following keys: # {address} -# {address}{flag} := [N, D, R] +# {address}{flag} := [N, D, R] (N=New, D=Deleted, R=Renamed) # {address}{addtype} := [chn, dev] # {address}{channels} := Number of channels # {address}{name} := Device or channel name @@ -2369,7 +2515,18 @@ sub HMCCU_UpdateDeviceTable ($$) $nm = $devices->{$da}{name} if (defined ($devices->{$da}{name})); if ($devices->{$da}{flag} eq 'N' && defined ($nm)) { - my $at = HMCCU_IsChnAddr ($da, 0) ? 'chn' : 'dev'; + my $at = ''; + if (defined ($devices->{$da}{addtype})) { + $at = $devices->{$da}{addtype}; + } + else { + $at = 'chn' if (HMCCU_IsChnAddr ($da, 0)); + $at = 'dev' if (HMCCU_IsDevAddr ($da, 0)); + } + if ($at eq '') { + Log3 $name, 2, "HMCCU: Cannot detect type of address $da"; + next; + } Log3 $name, 2, "HMCCU: Duplicate name for device/channel $nm address=$da in CCU." if (exists ($hash->{hmccu}{adr}{$nm}) && $at ne $hash->{hmccu}{adr}{$nm}{addtype}); @@ -2471,8 +2628,8 @@ sub HMCCU_UpdateDeviceTable ($$) } ###################################################################### -# Update a single client device datapoint considering -# scaling, reading format and value substitution. +# Update a single client device datapoint considering scaling, reading +# format and value substitution. # Return stored value. ###################################################################### @@ -2497,14 +2654,10 @@ sub HMCCU_UpdateSingleDatapoint ($$$$) # Update readings of client device. # Parameter objects is a hash reference which contains updated data # for any device: -# {devaddr} -# {devaddr}{channelno} -# {devaddr}{channelno}{datapoint} # {devaddr}{channelno}{datapoint} = value # If client device is virtual device group: check if group members are # affected by updates and update readings in virtual group device. # Return a hash reference with datapoints and new values: -# {devaddr} # {devaddr}{datapoint} = value ###################################################################### @@ -2525,12 +2678,8 @@ sub HMCCU_UpdateSingleDevice ($$$) return 0 if ($clttype eq 'HMCUCCHN' && !exists ($objects->{$devaddr}{$cnum}) && !exists ($objects->{$devaddr}{0})); - # Get attributes of IO device my $ccuflags = AttrVal ($ccuname, 'ccuflags', 'null'); - # Get attributes of client device -# my $cltflags = AttrVal ($cltname, 'ccuflags', 'null'); - # Build device list including virtual devices my @grplist = ($cltname); my @virlist = HMCCU_FindClientDevices ($ccuhash, "HMCCUDEV", undef, "ccuif=VirtualDevices"); @@ -2575,6 +2724,7 @@ sub HMCCU_UpdateSingleDevice ($$$) readingsBeginUpdate ($ch); + # Update devices foreach my $dev (@devlist) { my ($da, $cnum) = HMCCU_SplitChnAddr ($dev); next if (!exists ($objects->{$da})); @@ -2756,10 +2906,39 @@ sub HMCCU_UpdatePeers ($$$$) } } +###################################################################### +# Get list of valid RPC interfaces. +# Binary interfaces are ignored if internal RPC server is used. +# Default interface is BidCos-RF. +###################################################################### + +sub HMCCU_GetRPCInterfaceList ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my @interfaces = ($HMCCU_RPC_INTERFACE_DEFAULT); + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + + if (defined ($hash->{hmccu}{rpcports})) { + foreach my $p (split (',', $hash->{hmccu}{rpcports})) { + my $ifname = HMCCU_GetRPCServerInfo ($hash, $p, 'name'); + next if (!defined ($ifname)); + my $iftype = HMCCU_GetRPCServerInfo ($hash, $ifname, 'type'); + next if ($ifname eq $HMCCU_RPC_INTERFACE_DEFAULT || + ($iftype eq 'B' && $ccuflags !~ /(extrpc|procrpc)/) || + !exists ($hash->{hmccu}{interfaces}{$ifname})); + push (@interfaces, $ifname); + } + } + + return @interfaces; +} + ###################################################################### # Get list of valid RPC ports. -# Considers binary RPC ports and interfaces used by CCU devices. -# Default is 2001. +# Binary interfaces are ignored if internal RPC server is used. +# Default port is 2001. ###################################################################### sub HMCCU_GetRPCPortList ($) @@ -2772,17 +2951,78 @@ sub HMCCU_GetRPCPortList ($) if (defined ($hash->{hmccu}{rpcports})) { foreach my $p (split (',', $hash->{hmccu}{rpcports})) { - my $ifname = $HMCCU_RPC_NUMPORT{$p}; + my $ifname = HMCCU_GetRPCServerInfo ($hash, $p, 'name'); + next if (!defined ($ifname)); + my $iftype = HMCCU_GetRPCServerInfo ($hash, $ifname, 'type'); next if ($p == $HMCCU_RPC_PORT_DEFAULT || - ($HMCCU_RPC_PROT{$p} eq 'B' && $ccuflags !~ /extrpc/) || - !exists ($hash->{hmccu}{iface}{$ifname})); + ($iftype eq 'B' && $ccuflags !~ /(extrpc|procrpc)/) || + !exists ($hash->{hmccu}{interfaces}{$ifname})); push (@ports, $p); } - } + } return @ports; } +###################################################################### +# Build RPC callback URL +# Parameter hash might be a HMCCU or a HMCCURPC hash. +###################################################################### + +sub HMCCU_GetRPCCallbackURL ($$$$$) +{ + my ($hash, $localaddr, $cbport, $clkey, $iface) = @_; + + return undef if (!defined ($hash)); + + my $hmccu_hash = $hash->{TYPE} eq 'HMCCURPC' ? $hash->{IODev} : $hash; + + return undef if (!exists ($hmccu_hash->{hmccu}{interfaces}{$iface}) && + !exists ($hmccu_hash->{hmccu}{ifports}{$iface})); + + my $ifname = $iface =~ /^[0-9]+$/ ? $hmccu_hash->{hmccu}{ifports}{$iface} : $iface; + return undef if (!exists ($hmccu_hash->{hmccu}{interfaces}{$ifname})); + + return $hmccu_hash->{hmccu}{interfaces}{$ifname}{prot}."://$localaddr:$cbport/fh". + $hmccu_hash->{hmccu}{interfaces}{$ifname}{port}; +} + +###################################################################### +# Get RPC server information. +# Parameter iface can be a port number or an interface name. +# Valid values for info are: +# url, port, prot, host, type, name, flags, device. +###################################################################### + +sub HMCCU_GetRPCServerInfo ($$$) +{ + my ($hash, $iface, $info) = @_; + + return undef if (!defined ($hash)); + return undef if (!exists ($hash->{hmccu}{interfaces}{$iface}) && + !exists ($hash->{hmccu}{ifports}{$iface})); + + my $ifname = $iface =~ /^[0-9]+$/ ? $hash->{hmccu}{ifports}{$iface} : $iface; + return undef if (!exists ($hash->{hmccu}{interfaces}{$ifname})); + return $ifname if ($info eq 'name'); + + return $hash->{hmccu}{interfaces}{$ifname}{$info}; +} + +###################################################################### +# Check if RPC interface is of specified type +###################################################################### + +sub HMCCU_IsRPCType ($$$) +{ + my ($hash, $iface, $type) = @_; + + my $rpctype = HMCCU_GetRPCServerInfo ($hash, $iface, 'type'); + return 0 if (!defined ($rpctype)); + + return $rpctype eq $type ? 1 : 0; +} + ###################################################################### # Register RPC callbacks at CCU if RPC-Server already in server loop ###################################################################### @@ -2791,26 +3031,27 @@ sub HMCCU_RPCRegisterCallback ($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $serveraddr = $hash->{host}; my $localaddr = $hash->{hmccu}{localaddr}; my $rpcinterval = AttrVal ($name, 'rpcinterval', $HMCCU_INIT_INTERVAL2); - my $rpcserveraddr = AttrVal ($name, 'rpcserveraddr', $localaddr); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my @rpcports = HMCCU_GetRPCPortList ($hash); - + foreach my $port (@rpcports) { my $clkey = 'CB'.$port; - my $cburl = "http://".$localaddr.":".$hash->{hmccu}{rpc}{$clkey}{cbport}."/fh".$port; - my $url = "http://$serveraddr:$port/"; - $url .= $HMCCU_RPC_URL{$port} if (exists ($HMCCU_RPC_URL{$port})); + my $cburl = HMCCU_GetRPCCallbackURL ($hash, $localaddr, $hash->{hmccu}{rpc}{$clkey}{cbport}, + $clkey, $port); + next if (!defined ($cburl)); + my $url = HMCCU_GetRPCServerInfo ($hash, $port, 'url'); + next if (!defined ($url)); + my $rpcflags = HMCCU_GetRPCServerInfo ($hash, $port, 'flags'); if ($hash->{hmccu}{rpc}{$clkey}{loop} == 1 || $hash->{hmccu}{rpc}{$clkey}{state} eq "register") { $hash->{hmccu}{rpc}{$clkey}{port} = $port; $hash->{hmccu}{rpc}{$clkey}{clurl} = $url; $hash->{hmccu}{rpc}{$clkey}{cburl} = $cburl; $hash->{hmccu}{rpc}{$clkey}{loop} = 2; - $hash->{hmccu}{rpc}{$clkey}{state} = "registered"; + $hash->{hmccu}{rpc}{$clkey}{state} = $rpcflags =~ /forceInit/ ? "running" : "registered"; Log3 $name, 1, "HMCCU: Registering callback $cburl with ID $clkey at $url"; my $rpcclient = RPC::XML::Client->new ($url); @@ -2867,27 +3108,66 @@ sub HMCCU_ResetCounters ($) } ###################################################################### -# Start external RPC server via HMCCURPC device. -# Return number of RPC server threads or 0 on error. +# Start external RPC server via RPC device. +# Return number of RPC servers or 0 on error. ###################################################################### sub HMCCU_StartExtRPCServer ($) { my ($hash) = @_; my $name = $hash->{NAME}; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - # Search RPC device. Create one if none exists - my $rpcdev = HMCCU_GetRPCDevice ($hash, 1); - return HMCCU_Log ($hash, 0, "Can't find or create HMCCURPC device", 0) if ($rpcdev eq ''); - - my ($rc, $msg) = HMCCURPC_StartRPCServer ($defs{$rpcdev}); - Log3 $name, 0, "HMCCURPC: $msg" if (!$rc && defined ($msg)); + if ($ccuflags =~ /extrpc/) { + # Search RPC device. Create one if none exists + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 1, undef); + return HMCCU_Log ($hash, 0, "Can't find or create HMCCURPC device", 0) if ($rpcdev eq ''); + CommandSave (undef, undef) if ($save); - return $rc; + my ($rc, $msg) = HMCCURPC_StartRPCServer ($defs{$rpcdev}); + HMCCU_SetRPCState ($hash, 'starting') if ($rc); + Log3 $name, 0, "HMCCURPC: $msg" if (!$rc && defined ($msg)); + return $rc; + } + elsif ($ccuflags =~ /procrpc/) { + my $c = 0; + my $d = 0; + my $s = 0; + my @iflist = HMCCU_GetRPCInterfaceList ($hash); + foreach my $ifname1 (@iflist) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 1, $ifname1); + next if ($rpcdev eq '' || !defined ($hash->{hmccu}{interfaces}{$ifname1}{device})); + $d++; + $s++ if ($save); + } + CommandSave (undef, undef) if ($s > 0); + + if ($d == scalar (@iflist)) { + foreach my $ifname2 (@iflist) { + my $dh = $defs{$hash->{hmccu}{interfaces}{$ifname2}{device}}; + $hash->{hmccu}{interfaces}{$ifname2}{manager} = 'HMCCU'; + my ($rc, $msg) = HMCCURPCPROC_StartRPCServer ($dh); + if (!$rc) { + HMCCU_SetRPCState ($hash, 'error', $ifname2, $msg); + } + else { + $c++; + } + } + HMCCU_SetRPCState ($hash, 'starting') if ($c > 0); + return $c; + } + else { + HMCCU_Log ($hash, 0, "Definition of some RPC devices failed", undef); + } + } + + return 0; } ###################################################################### -# Stop external RPC server via HMCCURPC device. +# Stop external RPC server via RPC device. ###################################################################### sub HMCCU_StopExtRPCServer ($) @@ -2895,13 +3175,34 @@ sub HMCCU_StopExtRPCServer ($) my ($hash) = @_; my $name = $hash->{NAME}; - return HMCCU_Log ($hash, 0, "Module HMCCURPC not loaded", 0) if (!exists ($modules{'HMCCURPC'})); + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - # Search RPC device - my $rpcdev = HMCCU_GetRPCDevice ($hash, 0); - return HMCCU_Log ($hash, 0, "Can't find RPC device", 0) if ($rpcdev eq ''); + if ($ccuflags =~ /extrpc/) { + return HMCCU_Log ($hash, 0, "Module HMCCURPC not loaded", 0) if (!exists ($modules{'HMCCURPC'})); - return HMCCURPC_StopRPCServer ($defs{$rpcdev}); + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, undef); + return HMCCU_Log ($hash, 0, "Can't find RPC device", 0) if ($rpcdev eq ''); + HMCCU_SetRPCState ($hash, 'stopping'); + return HMCCURPC_StopRPCServer ($defs{$rpcdev}); + } + elsif ($ccuflags =~ /procrpc/) { + return HMCCU_Log ($hash, 0, "Module HMCCURPCPROC not loaded", 0) if (!exists ($modules{'HMCCURPCPROC'})); + HMCCU_SetRPCState ($hash, 'stopping'); + + my $rc = 1; + my @iflist = HMCCU_GetRPCInterfaceList ($hash); + foreach my $ifname (@iflist) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + if ($rpcdev eq '') { + Log3 $name, 0, "HMCCU: Can't find RPC device"; + next; + } + $hash->{hmccu}{interfaces}{$ifname}{manager} = 'HMCCU'; + $rc &= HMCCURPCPROC_StopRPCServer ($defs{$rpcdev}); + } + + return $rc; + } } ###################################################################### @@ -2921,10 +3222,11 @@ sub HMCCU_StartIntRPCServer ($) # Address and ports my $rpcqueue = AttrVal ($name, 'rpcqueue', '/tmp/ccuqueue'); + my $localaddr = AttrVal ($name, 'rpcserveraddr', ''); my $rpcserverport = AttrVal ($name, 'rpcserverport', 5400); my $rpcinterval = AttrVal ($name, 'rpcinterval', $HMCCU_INIT_INTERVAL1); my @rpcportlist = HMCCU_GetRPCPortList ($hash); - my $serveraddr = $hash->{host}; + my $serveraddr = HMCCU_GetRPCServerInfo ($hash, $rpcportlist[0], 'host'); my $fork_cnt = 0; # Check for running RPC server processes @@ -2941,10 +3243,14 @@ sub HMCCU_StartIntRPCServer ($) } # Detect local IP address - my $socket = IO::Socket::INET->new (PeerAddr => $serveraddr, PeerPort => $rpcportlist[0]); - return HMCCU_Log ($hash, 1, "Can't connect to CCU port".$rpcportlist[0], 0) if (!$socket); - my $localaddr = $socket->sockhost (); - close ($socket); + if ($localaddr eq '') { + my $socket = IO::Socket::INET->new (PeerAddr => $serveraddr, PeerPort => $rpcportlist[0]); + return HMCCU_Log ($hash, 1, "Can't connect to RPC host $serveraddr port".$rpcportlist[0], 0) if (!$socket); + $localaddr = $socket->sockhost (); + close ($socket); + } + + $hash->{hmccu}{localaddr} = $localaddr; my $ccunum = $hash->{CCUNum}; @@ -2988,18 +3294,17 @@ sub HMCCU_StartIntRPCServer ($) } $hash->{hmccu}{rpccount} = $fork_cnt; - $hash->{hmccu}{localaddr} = $localaddr; if ($fork_cnt > 0) { # Set internals $hash->{RPCPID} = join (',', @hm_pids); $hash->{RPCPRC} = "internal"; - $hash->{RPCState} = "starting"; + + HMCCU_SetRPCState ($hash, 'starting'); # Initialize statistic counters HMCCU_ResetCounters ($hash); - readingsSingleUpdate ($hash, "rpcstate", "starting", 1); Log3 $name, 0, "RPC server(s) starting"; DoTrigger ($name, "RPC server starting"); @@ -3020,7 +3325,6 @@ sub HMCCU_StopRPCServer ($) my $pid = 0; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $serveraddr = $hash->{host}; # Deregister callback URLs in CCU HMCCU_RPCDeRegisterCallback ($hash); @@ -3034,15 +3338,12 @@ sub HMCCU_StopRPCServer ($) $rpchash->{state} = "stopping"; } else { - $rpchash->{state} = "stopped"; + $rpchash->{state} = "inactive"; } } # Update status - if ($hash->{hmccu}{rpccount} > 0) { - readingsSingleUpdate ($hash, "rpcstate", "stopping", 1); - $hash->{RPCState} = "stopping"; - } + HMCCU_SetRPCState ($hash, 'stopping') if ($hash->{hmccu}{rpccount} > 0); # Wait sleep (1); @@ -3114,12 +3415,25 @@ sub HMCCU_IsRPCServerRunning ($$$) if ($ccuflags =~ /extrpc/) { @$tids = () if (defined ($tids)); - my $rpcdev = HMCCU_GetRPCDevice ($hash, 0); + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, undef); if ($rpcdev ne '') { my ($r, $a) = HMCCURPC_CheckThreadState ($defs{$rpcdev}, 6, 'running', $tids); $c = $r; } } + elsif ($ccuflags =~ /procprc/) { + @$pids = () if (defined ($pids)); + my @iflist = HMCCU_GetRPCInterfaceList ($hash); + foreach my $ifname (@iflist) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + next if ($rpcdev eq ''); + my ($rc, $msg) = HMCCURPCPROC_CheckProcessState ($defs{$rpcdev}, 'running'); + if ($rc < 0 || $rc > 1) { + push (@$pids, $rc); + $c++; + } + } + } else { @$pids = () if (defined ($pids)); foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { @@ -3254,20 +3568,86 @@ sub HMCCU_GetFirmwareVersions ($) } ###################################################################### -# Read list of CCU devices and channels via Homematic Script. -# Update data of client devices if not current. +# Read CCU device identified by device or channel name via Homematic +# Script. # Return (device count, channel count) or (-1, -1) on error. ###################################################################### +sub HMCCU_GetDevice ($$) +{ + my ($hash, $name) = @_; + + my $devcount = 0; + my $chncount = 0; + my $devname; + my $devtype; + my %objects = (); + + my $response = HMCCU_HMScriptExt ($hash, "!GetDevice", { name => $name }); + return (-1, -1) if ($response eq '' || $response =~ /^ERROR:.*/); + + my @scrlines = split /\n/,$response; + foreach my $hmdef (@scrlines) { + my @hmdata = split /;/,$hmdef; + next if (scalar (@hmdata) == 0); + my $typeprefix = ''; + + if ($hmdata[0] eq 'D') { + next if (scalar (@hmdata) != 6); + # 1=Interface 2=Device-Address 3=Device-Name 4=Device-Type 5=Channel-Count + $objects{$hmdata[2]}{addtype} = 'dev'; + $objects{$hmdata[2]}{channels} = $hmdata[5]; + $objects{$hmdata[2]}{flag} = 'N'; + $objects{$hmdata[2]}{interface} = $hmdata[1]; + $objects{$hmdata[2]}{name} = $hmdata[3]; + $typeprefix = "CUX-" if ($hmdata[2] =~ /^CUX/); + $typeprefix = "HVL-" if ($hmdata[1] eq 'HVL'); + $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; + $objects{$hmdata[2]}{chndir} = 0; + $devname = $hmdata[3]; + $devtype = $typeprefix . $hmdata[4]; + } + elsif ($hmdata[0] eq 'C') { + next if (scalar (@hmdata) != 4); + # 1=Channel-Address 2=Channel-Name 3=Direction + $objects{$hmdata[1]}{addtype} = 'chn'; + $objects{$hmdata[1]}{channels} = 1; + $objects{$hmdata[1]}{flag} = 'N'; + $objects{$hmdata[1]}{name} = $hmdata[2]; + $objects{$hmdata[1]}{valid} = 1; + $objects{$hmdata[1]}{chndir} = $hmdata[3]; + } + } + + if (scalar (keys %objects) > 0) { + # Update HMCCU device tables + ($devcount, $chncount) = HMCCU_UpdateDeviceTable ($hash, \%objects); + + # Read available datapoints for device type + HMCCU_GetDatapointList ($hash, $devname, $devtype) if (defined ($devname) && defined ($devtype)); + } + + return ($devcount, $chncount); +} + +###################################################################### +# Read list of CCU devices, channels and interfaces via Homematic +# Script. +# Update data of client devices if not current. +# Return (device count, channel count, interface count) or (-1, -1, -1) +# on error. +###################################################################### + sub HMCCU_GetDeviceList ($) { my ($hash) = @_; my $devcount = 0; my $chncount = 0; + my $ifcount = 0; my %objects = (); my $response = HMCCU_HMScriptExt ($hash, "!GetDeviceList", undef); - return (-1, -1) if ($response eq '' || $response =~ /^ERROR:.*/); + return (-1, -1, -1) if ($response eq '' || $response =~ /^ERROR:.*/); # Delete old entries %{$hash->{hmccu}{dev}} = (); @@ -3292,6 +3672,7 @@ sub HMCCU_GetDeviceList ($) foreach my $hmdef (@scrlines) { my @hmdata = split /;/,$hmdef; next if (scalar (@hmdata) == 0); + my $typeprefix = ''; if ($hmdata[0] eq 'D') { next if (scalar (@hmdata) != 6); @@ -3301,10 +3682,10 @@ sub HMCCU_GetDeviceList ($) $objects{$hmdata[2]}{flag} = 'N'; $objects{$hmdata[2]}{interface} = $hmdata[1]; $objects{$hmdata[2]}{name} = $hmdata[3]; - $objects{$hmdata[2]}{type} = ($hmdata[2] =~ /^CUX/) ? "CUX-".$hmdata[4] : $hmdata[4]; + $typeprefix = "CUX-" if ($hmdata[2] =~ /^CUX/); + $typeprefix = "HVL-" if ($hmdata[1] eq 'HVL'); + $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; $objects{$hmdata[2]}{chndir} = 0; - # Count used interfaces - $hash->{hmccu}{iface}{$hmdata[1]}++; # CCU information (address = BidCoS-RF) if ($hmdata[2] eq 'BidCoS-RF') { $hash->{ccuname} = $hmdata[3]; @@ -3322,54 +3703,96 @@ sub HMCCU_GetDeviceList ($) $objects{$hmdata[1]}{valid} = 1; $objects{$hmdata[1]}{chndir} = $hmdata[3]; } + elsif ($hmdata[0] eq 'I') { + next if (scalar (@hmdata) != 4); + # 1=Interface-Name 2=Interface Info 3=URL + my $ifurl = $hmdata[3]; + if ($ifurl =~ /^([^:]+):\/\/([^:]+):([0-9]+)/) { + my ($prot, $ipaddr, $port) = ($1, $2, $3); + next if (!defined ($port) || $port eq ''); + if ($hash->{ccuip} ne 'N/A') { + $ifurl =~ s/127\.0\.0\.1/$hash->{ccuip}/; + $ipaddr =~ s/127\.0\.0\.1/$hash->{ccuip}/; + } + else { + $ifurl =~ s/127\.0\.0\.1/$hash->{host}/; + $ipaddr =~ s/127\.0\.0\.1/$hash->{host}/; + } + if ($HMCCU_RPC_FLAG{$port} =~ /forceASCII/) { + $ifurl =~ s/xmlrpc_bin/xmlrpc/; + $prot = "xmlrpc"; + } + # Perl RPC::XML::Client.pm does not support URLs starting with xmlrpc:// + $ifurl =~ s/xmlrpc:/http:/; + $prot =~ s/^xmlrpc$/http/; + + $hash->{hmccu}{interfaces}{$hmdata[1]}{url} = $ifurl; + $hash->{hmccu}{interfaces}{$hmdata[1]}{prot} = $prot; + $hash->{hmccu}{interfaces}{$hmdata[1]}{type} = $prot eq 'http' ? 'A' : 'B'; + $hash->{hmccu}{interfaces}{$hmdata[1]}{port} = $port; + $hash->{hmccu}{interfaces}{$hmdata[1]}{host} = $ipaddr; + $hash->{hmccu}{interfaces}{$hmdata[1]}{state} = 'inactive'; + $hash->{hmccu}{interfaces}{$hmdata[1]}{manager} = 'null'; + $hash->{hmccu}{interfaces}{$hmdata[1]}{flags} = $HMCCU_RPC_FLAG{$port}; + $hash->{hmccu}{ifports}{$port} = $hmdata[1]; + $ifcount++; + } + } } if (scalar (keys %objects) > 0) { # Update some CCU I/O device information - $hash->{ccuinterfaces} = join (',', keys %{$hash->{hmccu}{iface}}); + $hash->{ccuinterfaces} = join (',', keys %{$hash->{hmccu}{interfaces}}); # Update HMCCU device tables ($devcount, $chncount) = HMCCU_UpdateDeviceTable ($hash, \%objects); # Read available datapoints for each device type - HMCCU_GetDatapointList ($hash); + HMCCU_GetDatapointList ($hash, undef, undef); } - return ($devcount, $chncount); + return ($devcount, $chncount, $ifcount); } ###################################################################### -# Read list of datapoints for CCU device types. +# Read list of datapoints for all or one CCU device type(s). # Function must not be called before GetDeviceList. # Return number of datapoints. ###################################################################### -sub HMCCU_GetDatapointList ($) +sub HMCCU_GetDatapointList ($$$) { - my ($hash) = @_; + my ($hash, $devname, $devtype) = @_; my $name = $hash->{NAME}; - if (exists ($hash->{hmccu}{dp})) { - delete $hash->{hmccu}{dp}; - } - - # Select one device for each device type - my %alltypes; my @devunique; - foreach my $add (sort keys %{$hash->{hmccu}{dev}}) { - next if ($hash->{hmccu}{dev}{$add}{addtype} ne 'dev'); - my $dt = $hash->{hmccu}{dev}{$add}{type}; - if (defined ($dt)) { - if ($dt ne '' && !exists ($alltypes{$dt})) { - $alltypes{$dt} = 1; - push @devunique, $hash->{hmccu}{dev}{$add}{name}; + + if (defined ($devname) && defined ($devtype)) { + return 0 if (exists ($hash->{hmccu}{dp}{$devtype})); + push @devunique, $devname; + } + else { + if (exists ($hash->{hmccu}{dp})) { + delete $hash->{hmccu}{dp}; + } + + # Select one device for each device type + my %alltypes; + foreach my $add (sort keys %{$hash->{hmccu}{dev}}) { + next if ($hash->{hmccu}{dev}{$add}{addtype} ne 'dev'); + my $dt = $hash->{hmccu}{dev}{$add}{type}; + if (defined ($dt)) { + if ($dt ne '' && !exists ($alltypes{$dt})) { + $alltypes{$dt} = 1; + push @devunique, $hash->{hmccu}{dev}{$add}{name}; + } + } + else { + Log3 $name, 2, "HMCCU: Corrupt or invalid entry in device table for device $add"; } } - else { - Log3 $name, 2, "HMCCU: Corrupt or invalid entry in device table for device $add"; - } } - + if (scalar (@devunique) == 0) { Log3 $name, 2, "HMCCU: No device types found in device table. Cannot read datapoints."; return 0; @@ -3385,8 +3808,9 @@ sub HMCCU_GetDatapointList ($) my $c = 0; foreach my $dpspec (split /\n/,$response) { - my ($chna, $devt, $devc, $dptn, $dptt, $dpto) = split (";", $dpspec); - $devt = "CUX-".$devt if ($chna =~ /^CUX/); + my ($iface, $chna, $devt, $devc, $dptn, $dptt, $dpto) = split (";", $dpspec); + $devt = "CUX-".$devt if ($iface eq 'CUxD'); + $devt = "HVL-".$devt if ($iface eq 'HVL'); $hash->{hmccu}{dp}{$devt}{spc}{ontime} = $devc.".".$dptn if ($dptn eq "ON_TIME"); $hash->{hmccu}{dp}{$devt}{spc}{ramptime} = $devc.".".$dptn if ($dptn eq "RAMP_TIME"); $hash->{hmccu}{dp}{$devt}{spc}{submit} = $devc.".".$dptn if ($dptn eq "SUBMIT"); @@ -3425,8 +3849,15 @@ sub HMCCU_IsValidDeviceOrChannel ($$) return $hash->{hmccu}{dev}{$param}{valid}; } else { - return 0 if (! exists ($hash->{hmccu}{adr}{$param})); - return $hash->{hmccu}{adr}{$param}{valid}; + if (exists ($hash->{hmccu}{adr}{$param})) { + return $hash->{hmccu}{adr}{$param}{valid}; + } + elsif (exists ($hash->{hmccu}{dev}{$param})) { + return $hash->{hmccu}{dev}{$param}{valid}; + } + else { + return 0; + } } } @@ -3450,8 +3881,15 @@ sub HMCCU_IsValidDevice ($$) return $hash->{hmccu}{dev}{$param}{valid}; } else { - return 0 if (! exists ($hash->{hmccu}{adr}{$param})); - return $hash->{hmccu}{adr}{$param}{valid} && $hash->{hmccu}{adr}{$param}{addtype} eq 'dev'; + if (exists ($hash->{hmccu}{adr}{$param})) { + return $hash->{hmccu}{adr}{$param}{valid} && $hash->{hmccu}{adr}{$param}{addtype} eq 'dev' ? 1 : 0; + } + elsif (exists ($hash->{hmccu}{dev}{$param})) { + return $hash->{hmccu}{dev}{$param}{valid} && $hash->{hmccu}{dev}{$param}{addtype} eq 'dev' ? 1 : 0; + } + else { + return 0; + } } } @@ -3475,8 +3913,15 @@ sub HMCCU_IsValidChannel ($$) return $hash->{hmccu}{dev}{$param}{valid}; } else { - return 0 if (! exists ($hash->{hmccu}{adr}{$param})); - return $hash->{hmccu}{adr}{$param}{valid} && $hash->{hmccu}{adr}{$param}{addtype} eq 'chn'; + if (exists ($hash->{hmccu}{adr}{$param})) { + return $hash->{hmccu}{adr}{$param}{valid} && $hash->{hmccu}{adr}{$param}{addtype} eq 'chn' ? 1 : 0; + } + elsif (exists ($hash->{hmccu}{dev}{$param})) { + return $hash->{hmccu}{dev}{$param}{valid} && $hash->{hmccu}{dev}{$param}{addtype} eq 'chn' ? 1 : 0; + } + else { + return 0; + } } } @@ -3510,7 +3955,7 @@ sub HMCCU_GetCCUDeviceParam ($$) } return (undef, undef, undef, undef) if (!defined ($add)); - ($devadd, $chn) = HMCCU_SplitChnAddr ($add); + ($devadd, $chn) = split (':', $add); return (undef, undef, undef, undef) if (!defined ($devadd) || !exists ($hash->{hmccu}{dev}{$devadd}) || $hash->{hmccu}{dev}{$devadd}{valid} == 0); @@ -3563,6 +4008,22 @@ sub HMCCU_GetValidDatapoints ($$$$$) return scalar (@$dplistref); } +###################################################################### +# Get datapoint attribute. +# Valid attributes are 'oper' or 'type'. +###################################################################### + +sub HMCCU_GetDatapointAttr ($$$$$) +{ + my ($hash, $devtype, $chnno, $dpt, $attr) = @_; + + return undef if ($attr ne 'oper' && $attr ne 'type'); + return undef if (!exists ($hash->{hmccu}{dp}{$devtype})); + return undef if (!exists ($hash->{hmccu}{dp}{$devtype}{ch}{$chnno})); + return undef if (!exists ($hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt})); + return $hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{$attr}; +} + ###################################################################### # Find a datapoint for device type. # hash = hash of client or IO device @@ -3635,6 +4096,7 @@ sub HMCCU_GetSwitchDatapoint ($$$) sub HMCCU_IsValidDatapoint ($$$$$) { my ($hash, $devtype, $chn, $dpt, $oper) = @_; + my $fnc = "IsValidDatapoint"; my $hmccu_hash = HMCCU_GetHash ($hash); return 0 if (!defined ($hmccu_hash)); @@ -3649,17 +4111,23 @@ sub HMCCU_IsValidDatapoint ($$$$$) return 1 if (!exists ($hmccu_hash->{hmccu}{dp})); my $chnno = $chn; - if (HMCCU_IsChnAddr ($chn, 0)) { + if (HMCCU_IsValidChannel ($hmccu_hash, $chn)) { + HMCCU_Trace ($hash, 2, $fnc, "$chn is a valid channel address"); my ($a, $c) = split(":",$chn); $chnno = $c; } + else { + HMCCU_Trace ($hash, 2, $fnc, "$chn is not a valid channel address"); + } - # If datapoint name has format channel-number.datapoint ignore parameter chn if ($dpt =~ /^([0-9]{1,2})\.(.+)$/) { $chnno = $1; $dpt = $2; + HMCCU_Trace ($hash, 2, $fnc, "$dpt contains channel number"); } + HMCCU_Trace ($hash, 2, $fnc, "devtype=$devtype, chnno=$chnno, dpt=$dpt"); + return (exists ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}) && ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{oper} & $oper)) ? 1 : 0; } @@ -3695,11 +4163,9 @@ sub HMCCU_GetDeviceName ($$$) { my ($hash, $addr, $default) = @_; - if (HMCCU_IsDevAddr ($addr, 0) || HMCCU_IsChnAddr ($addr, 0)) { + if (HMCCU_IsValidDeviceOrChannel ($hash, $addr)) { $addr =~ s/:[0-9]+$//; - if (exists ($hash->{hmccu}{dev}{$addr})) { - return $hash->{hmccu}{dev}{$addr}{name}; - } + return $hash->{hmccu}{dev}{$addr}{name}; } return $default; @@ -3713,10 +4179,8 @@ sub HMCCU_GetChannelName ($$$) { my ($hash, $addr, $default) = @_; - if (HMCCU_IsChnAddr ($addr, 0)) { - if (exists ($hash->{hmccu}{dev}{$addr})) { - return $hash->{hmccu}{dev}{$addr}{name}; - } + if (HMCCU_IsValidChannel ($hash, $addr)) { + return $hash->{hmccu}{dev}{$addr}{name}; } return $default; @@ -3731,11 +4195,9 @@ sub HMCCU_GetDeviceType ($$$) { my ($hash, $addr, $default) = @_; - if (HMCCU_IsDevAddr ($addr, 0) || HMCCU_IsChnAddr ($addr, 0)) { + if (HMCCU_IsValidDeviceOrChannel ($hash, $addr)) { $addr =~ s/:[0-9]+$//; - if (exists ($hash->{hmccu}{dev}{$addr})) { - return $hash->{hmccu}{dev}{$addr}{type}; - } + return $hash->{hmccu}{dev}{$addr}{type}; } return $default; @@ -3751,11 +4213,9 @@ sub HMCCU_GetDeviceChannels ($$$) { my ($hash, $addr, $default) = @_; - if (HMCCU_IsDevAddr ($addr, 0) || HMCCU_IsChnAddr ($addr, 0)) { + if (HMCCU_IsValidDeviceOrChannel ($hash, $addr)) { $addr =~ s/:[0-9]+$//; - if (exists ($hash->{hmccu}{dev}{$addr})) { - return $hash->{hmccu}{dev}{$addr}{channels}; - } + return $hash->{hmccu}{dev}{$addr}{channels}; } return 0; @@ -3770,11 +4230,9 @@ sub HMCCU_GetDeviceInterface ($$$) { my ($hash, $addr, $default) = @_; - if (HMCCU_IsDevAddr ($addr, 0) || HMCCU_IsChnAddr ($addr, 0)) { + if (HMCCU_IsValidDeviceOrChannel ($hash, $addr)) { $addr =~ s/:[0-9]+$//; - if (exists ($hash->{hmccu}{dev}{$addr})) { - return $hash->{hmccu}{dev}{$addr}{interface}; - } + return $hash->{hmccu}{dev}{$addr}{interface}; } return $default; @@ -3795,6 +4253,8 @@ sub HMCCU_GetAddress ($$$$) my $add = $defadd; my $chn = $defchn; my $chnno = $defchn; + my $addr = ''; + my $type = ''; if ($name =~ /^hmccu:.+$/) { $name =~ s/^hmccu://; @@ -3814,29 +4274,30 @@ sub HMCCU_GetAddress ($$$$) } if (exists ($hash->{hmccu}{adr}{$name})) { + # Name known by HMCCU + $addr = $hash->{hmccu}{adr}{$name}{address}; + $type = $hash->{hmccu}{adr}{$name}{addtype}; + } + elsif (exists ($hash->{hmccu}{dev}{$name})) { # Address known by HMCCU - my $addr = $hash->{hmccu}{adr}{$name}{address}; - if (HMCCU_IsChnAddr ($addr, 0)) { - ($add, $chn) = split (":", $addr); - } - elsif (HMCCU_IsDevAddr ($addr, 0)) { - $add = $addr; - } + $addr = $name; + $type = $hash->{hmccu}{dev}{$name}{addtype}; } else { # Address not known. Query CCU - my $response = HMCCU_GetCCUObjectAttribute ($hash, $name, "Address()"); - if (defined ($response)) { - if (HMCCU_IsChnAddr ($response, 0)) { - ($add, $chn) = split (":", $response); - $hash->{hmccu}{adr}{$name}{address} = $response; - $hash->{hmccu}{adr}{$name}{addtype} = 'chn'; - } - elsif (HMCCU_IsDevAddr ($response, 0)) { - $add = $response; - $hash->{hmccu}{adr}{$name}{address} = $response; - $hash->{hmccu}{adr}{$name}{addtype} = 'dev'; - } + my ($dc, $cc) = HMCCU_GetDevice ($hash, $name); + if ($dc > 0 && $cc > 0 && exists ($hash->{hmccu}{adr}{$name})) { + $addr = $hash->{hmccu}{adr}{$name}{address}; + $type = $hash->{hmccu}{adr}{$name}{addtype}; + } + } + + if ($addr ne '') { + if ($type eq 'chn') { + ($add, $chn) = split (":", $addr); + } + else { + $add = $addr; } } @@ -3898,14 +4359,10 @@ sub HMCCU_SplitChnAddr ($) { my ($addr) = @_; - if (HMCCU_IsChnAddr ($addr, 0)) { - return split (":", $addr); - } - elsif (HMCCU_IsDevAddr ($addr, 0)) { - return ($addr, ''); - } + my ($dev, $chn) = split (':', $addr); + $chn = '' if (!defined ($chn)); - return ('', ''); + return ($dev, $chn); } ###################################################################### @@ -3932,8 +4389,8 @@ sub HMCCU_GetCCUObjectAttribute ($$$) # Get list of client devices matching the specified criteria. # If no criteria is specified all device names will be returned. # Parameters modexp and namexp are regular expressions for module -# name and device name. Parameter internal contains an expression -# like internal=valueexp. +# name and device name. Parameter internal contains a comma separated +# list of expressions like internal=valueexp. # All parameters can be undefined. In this case all devices will be # returned. ###################################################################### @@ -3945,83 +4402,125 @@ sub HMCCU_FindClientDevices ($$$$) foreach my $d (keys %defs) { my $ch = $defs{$d}; + my $m = 1; next if (!defined ($ch->{TYPE}) || !defined ($ch->{NAME})); next if (defined ($modexp) && $ch->{TYPE} !~ /$modexp/); next if (defined ($namexp) && $ch->{NAME} !~ /$namexp/); next if (defined ($hash) && exists ($ch->{IODev}) && $ch->{IODev} != $hash); if (defined ($internal)) { - my ($i, $v) = split ('=', $internal); - next if (defined ($v) && exists ($ch->{$i}) && $ch->{$i} !~ /$v/); + foreach my $intspec (split (',', $internal)) { + my ($i, $v) = split ('=', $intspec); + if (defined ($v) && exists ($ch->{$i}) && $ch->{$i} !~ /$v/) { + $m = 0; + last; + } + } } - push @devlist, $ch->{NAME}; + push @devlist, $ch->{NAME} if ($m == 1); } return @devlist; } ###################################################################### -# Get name of assigned client device of type HMCCURPC. -# Create a HMCCURPC device if none is found and parameter create -# is set to 1. -# Return empty string if HMCCURPC device cannot be identified. +# Get name of assigned client device of type HMCCURPC or HMCCURPCPROC. +# Create a RPC device of type HMCCURPC or HMCCURPCPROC if none is +# found and parameter create is set to 1. +# Return (devname, create). +# Return empty string for devname if RPC device cannot be identified +# or created. Return create = 1 if device has been created and +# configuration should be saved. ###################################################################### -sub HMCCU_GetRPCDevice ($$) +sub HMCCU_GetRPCDevice ($$$) { - my ($hash, $create) = @_; + my ($hash, $create, $ifname) = @_; my $name = $hash->{NAME}; - my $rpcdevname = ''; + my $rpcdevname; + my $rpcdevtype = 'HMCCURPC'; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - # RPC device already defined - if (defined ($hash->{RPCDEV})) { - if (exists ($defs{$hash->{RPCDEV}})) { - if ($defs{$hash->{RPCDEV}}->{IODev} == $hash) { - $rpcdevname = $hash->{RPCDEV}; + if ($ccuflags =~ /procrpc/) { + return (HMCCU_Log ($hash, 1, "Interface not defined for RPC server of type HMCCURPCPROC", ''), 0) + if (!defined ($ifname)); + $rpcdevname = HMCCU_GetRPCServerInfo ($hash, $ifname, 'device'); + return ($rpcdevname, 0) if (defined ($rpcdevname)); + $rpcdevtype = 'HMCCURPCPROC'; + } + elsif ($ccuflags =~ /extrpc/) { + if (defined ($hash->{RPCDEV})) { + if (exists ($defs{$hash->{RPCDEV}})) { + my $rpchash = $defs{$hash->{RPCDEV}}; + return (HMCCU_Log ($hash, 1, "RPC device ".$hash->{RPCDEV}." is not assigned to $name", ''), 0) + if (!defined ($rpchash->{IODev}) || $rpchash->{IODev} != $hash); } else { - Log3 $name, 1, "HMCCU: RPC device $rpcdevname is not assigned to $name"; - } - } - else { - Log3 $name, 1, "HMCCU: RPC device $rpcdevname not found"; + return (HMCCU_Log ($hash, 1, "RPC device ".$hash->{RPCDEV}." not found", ''), 0); + } + return $hash->{RPCDEV}; } } else { - # Search for HMCCURPC devices associated with I/O device - my @devlist = HMCCU_FindClientDevices ($hash, 'HMCCURPC', undef, undef); - my $devcnt = scalar (@devlist); - if ($devcnt == 0 && $create) { - # Define HMCCURPC device with same room and group as HMCCU device - $rpcdevname = $name."_rpc"; - Log3 $name, 1, "HMCCU: Creating new RPC device $rpcdevname"; - my $ret = CommandDefine (undef, $rpcdevname." HMCCURPC ".$hash->{host}); - if (!defined ($ret)) { - # HMCCURPC device created. Copy some attributes from HMCCU device - my $room = AttrVal ($name, 'room', ''); - CommandAttr (undef, "$rpcdevname room $room") if ($room ne ''); - my $group = AttrVal ($name, 'group', ''); - CommandAttr (undef, "$rpcdevname group $group") if ($group ne ''); - my $icon = AttrVal ($name, 'icon', ''); - CommandAttr (undef, "$rpcdevname icon $icon") if ($icon ne ''); - $hash->{RPCDEV} = $rpcdevname; - CommandSave (undef, undef); - } - else { - Log3 $name, 1, "HMCCU: Definition of RPC device failed. $ret"; - $rpcdevname = ''; - } - } - elsif ($devcnt == 1) { - $rpcdevname = $devlist[0]; + return (HMCCU_Log ($hash, 1, "No need for RPC device when using internal RPC server", ''), 0); + } + + # Search for RPC devices associated with I/O device + my @devlist; + foreach my $dev (keys %defs) { + my $devhash = $defs{$dev}; + next if ($devhash->{TYPE} ne $rpcdevtype || $devhash->{host} ne $hash->{host}); + next if ($rpcdevtype eq 'HMCCURPCPROC' && $devhash->{rpcinterface} ne $ifname); + push @devlist, $devhash->{NAME}; + } + my $devcnt = scalar (@devlist); + if ($devcnt == 1) { + if ($ccuflags =~ /extrpc/) { $hash->{RPCDEV} = $devlist[0]; } - elsif ($devcnt > 1) { - # Found more than 1 HMCCURPC device - Log3 $name, 2, "HMCCU: Found more than one HMCCURPC device. Specify device with attribute rpcdevice"; + else { + $hash->{hmccu}{interfaces}{$ifname}{device} = $devlist[0]; + } + return ($devlist[0], 0); + } + elsif ($devcnt > 1) { + return (HMCCU_Log ($hash, 2, "Found more than one RPC device", ''), 0); + } + + HMCCU_Log ($hash, 1, "No RPC device defined", undef); + + # Create RPC device + if ($create) { + my $alias = "CCU RPC"; + my $rpccreate = "d_rpc $rpcdevtype ".$hash->{host}; + $rpcdevname = "d_rpc"; + if (defined ($ifname)) { + $rpcdevname = makeDeviceName ("d_rpc".$ifname); + $alias .= " $ifname"; + $rpccreate = "$rpcdevname $rpcdevtype ".$hash->{host}. " $ifname"; + } + + HMCCU_Log ($hash, 1, "Creating new RPC device $rpcdevname", undef); + my $ret = CommandDefine (undef, $rpccreate); + if (!defined ($ret)) { + # HMCCURPC device created. Set/copy some attributes from HMCCU device + my %rpcdevattr = ('room' => 'copy', 'group' => 'copy', 'icon' => 'copy', + 'stateFormat' => 'rpcstate/state', 'eventMap' => '/rpcserver on:on/rpcserver off:off/', + 'verbose' => 2, 'alias' => $alias ); + foreach my $a (keys %rpcdevattr) { + my $v = $rpcdevattr{$a} eq 'copy' ? AttrVal ($name, $a, '') : $rpcdevattr{$a}; + CommandAttr (undef, "$rpcdevname $a $v") if ($v ne ''); + } + $hash->{RPCDEV} = $rpcdevname if ($ccuflags =~ /extrpc/); + return ($rpcdevname, 1); + } + else { + Log3 $name, 1, "HMCCU: [$name] Definition of RPC device failed."; + Log3 $name, 1, "HMCCU: [$name] $ret"; } } - - return $rpcdevname; + + return ('', 0); } ###################################################################### @@ -4111,6 +4610,8 @@ sub HMCCU_GetDatapointCount ($$$) # controlchannel and datapoint name. If attribute statedatapoint # contains channel number it is splitted into statechannel and # datapoint name. +# If controldatapoint is not specified it will synchronized with +# statedatapoint. ###################################################################### sub HMCCU_GetSpecialDatapoints ($$$$$) @@ -4122,7 +4623,7 @@ sub HMCCU_GetSpecialDatapoints ($$$$$) my $statedatapoint = AttrVal ($name, 'statedatapoint', ''); my $statechannel = AttrVal ($name, 'statechannel', ''); - my $controldatapoint = AttrVal ($name, 'controldatapoint', ''); + my $controldatapoint = AttrVal ($name, 'controldatapoint', $statedatapoint); if ($statedatapoint ne '') { if ($statedatapoint =~ /^([0-9]+)\.(.+)$/) { @@ -4150,7 +4651,7 @@ sub HMCCU_GetSpecialDatapoints ($$$$$) $cc = $sc; } - # Try to find state channel and state datapoint + # Try to find state channel my $c = -1; if ($sc eq '' && $sd ne '') { $c = HMCCU_FindDatapoint ($hash, $hash->{ccutype}, -1, $sd, 3); @@ -4162,6 +4663,10 @@ sub HMCCU_GetSpecialDatapoints ($$$$$) $c = HMCCU_FindDatapoint ($hash, $hash->{ccutype}, -1, $cd, 3); $cc = $c if ($c >= 0); } + + # By default set control channel and datapoint to state channel and datapoint + $cc = $sc if ($cc eq ''); + $cd = $sd if ($cd eq ''); return ($sc, $sd, $cc, $cd); } @@ -4277,7 +4782,8 @@ sub HMCCU_ProcessEvent ($$) # Input: EV|Adress|Datapoint|Value # Output: EV, DevAdd, ChnNo, Reading='', Value # - return undef if ($tc != 4 || !HMCCU_IsChnAddr ($t[1], 0)); + return HMCCU_Log ($hash, 2, "Invalid channel ".$t[1], undef) + if (!HMCCU_IsValidChannel ($hash, $t[1])); my ($add, $chn) = split (/:/, $t[1]); return ($t[0], $add, $chn, $t[2], $t[3]); } @@ -4303,8 +4809,6 @@ sub HMCCU_ProcessEvent ($$) my $clkey = $t[3]; my $norun = 0; my $run = 0; - my $c_ok = 0; - my $c_err = 0; return HMCCU_Log ($hash, 0, "Received IN event for unknown RPC server $clkey", undef) if (!exists ($rh->{$clkey})); Log3 $name, 0, "HMCCU: Received IN event. RPC server $clkey initialized."; @@ -4316,17 +4820,9 @@ sub HMCCU_ProcessEvent ($$) $norun++ if ($rh->{$ser}{state} ne "initialized" && $rh->{$ser}{pid} == 0); $run++ if ($rh->{$ser}{state} eq "running"); } - if ($norun == 0) { - $hash->{RPCState} = "running"; - readingsSingleUpdate ($hash, "rpcstate", "running", 1); - HMCCU_SetState ($hash, "OK"); - ($c_ok, $c_err) = HMCCU_UpdateClients ($hash, '.*', 'Attr', 0); - Log3 $name, 2, "HMCCU: Updated devices. Success=$c_ok Failed=$c_err"; - Log3 $name, 1, "HMCCU: All RPC servers running"; - DoTrigger ($name, "RPC server running"); - } + HMCCU_SetRPCState ($hash, 'running') if ($norun == 0); $hash->{hmccu}{rpcinit} = $run; - return ($t[0], $clkey, $run, $norun, $c_ok, $c_err); + return ($t[0], $clkey, $run, $norun); } elsif ($t[0] eq 'EX') { # @@ -4345,13 +4841,10 @@ sub HMCCU_ProcessEvent ($$) # Check if all RPC servers were terminated. Set overall status foreach my $ser (keys %{$rh}) { - $run++ if ($rh->{$ser}{state} ne "stopped"); + $run++ if ($rh->{$ser}{state} ne "inactive"); } if ($run == 0) { - if ($f == 1) { - $hash->{RPCState} = "stopped"; - readingsSingleUpdate ($hash, "rpcstate", "stopped", 1); - } + HMCCU_SetRPCState ($hash, 'inactive') if ($f == 1); $hash->{RPCPID} = '0'; } $hash->{hmccu}{rpccount} = $run; @@ -4433,7 +4926,7 @@ sub HMCCU_ReadRPCQueue ($) my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $rpcinterval = AttrVal ($name, 'rpcinterval', 5); my $rpcqueue = AttrVal ($name, 'rpcqueue', '/tmp/ccuqueue'); - my $rpctimeout = AttrVal ($name, 'rpcevtimeout', 300); + my $rpcevtimeout = AttrVal ($name, 'rpcevtimeout', $HMCCU_TIMEOUT_EVENT); my $maxevents = $rpcinterval*10; $maxevents = 50 if ($maxevents > 50); $maxevents = 10 if ($maxevents < 10); @@ -4451,6 +4944,7 @@ sub HMCCU_ReadRPCQueue ($) my $element = HMCCU_QueueDeq ($hash); while (defined ($element)) { + Log3 $name, 2, "HMCCU: Event = $element" if ($ccuflags =~ /logEvents/); my ($et, @par) = HMCCU_ProcessEvent ($hash, $element); if (defined ($et)) { if ($et eq 'EV') { @@ -4465,11 +4959,13 @@ sub HMCCU_ReadRPCQueue ($) $devices{$par[0]}{flag} = 'N'; $devices{$par[0]}{version} = $par[3]; 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]; } else { + $devices{$par[0]}{addtype} = 'chn'; $devices{$par[0]}{usetype} = $par[2]; } $devcount++; @@ -4516,12 +5012,12 @@ sub HMCCU_ReadRPCQueue ($) return if ($f == -1); # Check if events from CCU timed out - if ($hash->{hmccu}{evtime} > 0 && time()-$hash->{hmccu}{evtime} > $rpctimeout && + if ($hash->{hmccu}{evtime} > 0 && time()-$hash->{hmccu}{evtime} > $rpcevtimeout && $hash->{hmccu}{evtimeout} == 0) { $hash->{hmccu}{evtimeout} = 1; $hash->{ccustate} = HMCCU_TCPConnect ($hash->{host}, 8181) ? 'timeout' : 'unreachable'; - Log3 $name, 2, "HMCCU: Received no events from CCU since $rpctimeout seconds"; - DoTrigger ($name, "No events from CCU since $rpctimeout seconds"); + Log3 $name, 2, "HMCCU: Received no events from CCU since $rpcevtimeout seconds"; + DoTrigger ($name, "No events from CCU since $rpcevtimeout seconds"); } else { $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); @@ -4547,7 +5043,7 @@ sub HMCCU_ReadRPCQueue ($) if ($f == 2 && $nhm_pids == 0) { # All RPC servers terminated and restart flag set - if ($ccuflags =~ /intrpc/) { + if ($ccuflags !~ /(extrpc|procrpc)/) { return if (HMCCU_StartIntRPCServer ($hash)); } Log3 $name, 0, "HMCCU: Restart of RPC server failed"; @@ -4573,10 +5069,9 @@ sub HMCCU_ReadRPCQueue ($) $hash->{RPCPID} = '0'; $hash->{RPCPRC} = 'none'; - $hash->{RPCState} = "stopped"; + HMCCU_SetRPCState ($hash, 'inactive'); Log3 $name, 0, "HMCCU: All RPC servers stopped"; - readingsSingleUpdate ($hash, "rpcstate", "stopped", 1); DoTrigger ($name, "All RPC servers stopped"); } } @@ -4600,7 +5095,11 @@ sub HMCCU_HMScriptExt ($$$) my $host = $hash->{host}; my $code = $hmscript; my $scrname = ''; - + + if ($hash->{TYPE} ne 'HMCCU') { + Log3 $name, 2, "HMCCU: HMScriptExt called with hash of type ".$hash->{TYPE}.", Dev=$name, script=$hmscript"; + } + # Check for internal script if ($hmscript =~ /^!(.*)$/) { $scrname = $1; @@ -4779,6 +5278,11 @@ sub HMCCU_SetDatapoint ($$$) $value = HMCCU_ScaleValue ($hash, $dpt, $value, 1); } + my $dpttype = HMCCU_GetDatapointAttr ($hmccu_hash, $hash->{ccutype}, $chn, $dpt, 'type'); + if (defined ($dpttype) && $dpttype == $HMCCU_TYPE_STRING) { + $value = "'".$value."'"; + } + if ($flags == $HMCCU_FLAGS_IACD) { $url .= $int.'.'.$add.':'.$chn.'.'.$dpt.'").State('.$value.')'; $nam = HMCCU_GetChannelName ($hmccu_hash, $add.":".$chn, ''); @@ -4983,15 +5487,15 @@ sub HMCCU_GetUpdate ($$$) $ccuget = HMCCU_GetAttribute ($hmccu_hash, $cl_hash, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - if (HMCCU_IsChnAddr ($addr, 0)) { + if (HMCCU_IsValidChannel ($hmccu_hash, $addr)) { $nam = HMCCU_GetChannelName ($hmccu_hash, $addr, ''); return -1 if ($nam eq ''); - my ($stadd, $stchn) = HMCCU_SplitChnAddr ($addr); + my ($stadd, $stchn) = split (':', $addr); my $stnam = HMCCU_GetChannelName ($hmccu_hash, "$stadd:0", ''); $list = $stnam eq '' ? $nam : $stnam . "," . $nam; $script = "!GetDatapointsByChannel"; } - elsif (HMCCU_IsDevAddr ($addr, 0)) { + elsif (HMCCU_IsValidDevice ($hmccu_hash, $addr)) { $nam = HMCCU_GetDeviceName ($hmccu_hash, $addr, ''); return -1 if ($nam eq ''); $list = $nam; @@ -5027,7 +5531,7 @@ sub HMCCU_GetUpdate ($$$) my ($iface, $chnadd, $dpt) = split /\./, $dpspec; next if (!defined ($dpt)); my ($add, $chn) = HMCCU_SplitChnAddr ($chnadd); - next if (!defined ($chn)); + next if ($chn eq ''); $events{$add}{$chn}{$dpt} = $value; } @@ -5090,7 +5594,7 @@ sub HMCCU_GetChannel ($$) my ($iface, $chnaddr, $dpt) = split /\./, $dptaddr; next if (!defined ($dpt)); my ($add, $chn) = HMCCU_SplitChnAddr ($chnaddr); - next if (!defined ($chn)); + next if ($chn eq ''); if (defined ($chnpars{$chnname}{dpt}) && $chnpars{$chnname}{dpt} ne '') { next if ($dpt !~ $chnpars{$chnname}{dpt}); } @@ -5126,7 +5630,7 @@ sub HMCCU_RPCGetConfig ($$$$) return (-3, $result) if (!defined ($hmccu_hash)); return (-4, $result) if ($type ne 'HMCCU' && $hash->{ccudevstate} eq 'deleted'); -# my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $ccureadings = AttrVal ($name, 'ccureadings', 1); my $readingformat = HMCCU_GetAttrReadingFormat ($hash, $hmccu_hash); my $substitute = HMCCU_GetAttrSubstitute ($hash, $hmccu_hash); @@ -5137,20 +5641,27 @@ sub HMCCU_RPCGetConfig ($$$$) $addr = $add; $addr .= ':'.$chn if ($flags & $HMCCU_FLAG_CHANNEL); - return (-9, '') if (!exists ($HMCCU_RPC_PORT{$int})); - my $port = $HMCCU_RPC_PORT{$int}; + my $rpctype = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'type'); + my $port = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'port'); + return (-9, '') if (!defined ($rpctype) || !defined ($port)); - if ($HMCCU_RPC_PROT{$port} eq 'B') { + if ($rpctype eq 'B') { # Search RPC device - my $rpcdev = HMCCU_GetRPCDevice ($hmccu_hash, 0); + my $rpcdev = HMCCU_GetRPCDevice ($hmccu_hash, 0, $int); return (-17, '') if ($rpcdev eq ''); HMCCU_Trace ($hash, 2, $fnc, "Method=$method Addr=$addr Port=$port"); - $res = HMCCURPC_SendBinRequest ($defs{$rpcdev}, $port, $method, $BINRPC_STRING, $addr, - $BINRPC_STRING, "MASTER"); + if ($ccuflags =~ /extrpc/) { + $res = HMCCURPC_SendBinRequest ($defs{$rpcdev}, $port, $method, $BINRPC_STRING, $addr, + $BINRPC_STRING, "MASTER"); + } + elsif ($ccuflags =~ /procrpc/) { + $res = HMCCURPCPROC_SendRequest ($defs{$rpcdev}, $method, $BINRPC_STRING, $addr, + $BINRPC_STRING, "MASTER"); + } } else { - my $url = "http://".$hmccu_hash->{host}.":".$port."/"; - $url .= $HMCCU_RPC_URL{$port} if (exists ($HMCCU_RPC_URL{$port})); + my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url'); + return (-9, '') if (!defined ($url)); HMCCU_Trace ($hash, 2, $fnc, "Method=$method Addr=$addr Port=$port"); my $client = RPC::XML::Client->new ($url); $res = $client->simple_request ($method, $addr, "MASTER"); @@ -5243,8 +5754,10 @@ sub HMCCU_RPCSetConfig ($$$) $addr = $add; $addr .= ':'.$chn if ($flags & $HMCCU_FLAG_CHANNEL); - return -9 if (!exists ($HMCCU_RPC_PORT{$int})); - my $port = $HMCCU_RPC_PORT{$int}; + my $rpctype = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'type'); + return -9 if (!defined ($rpctype)); + my $port = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'port'); + return -9 if (!defined ($port)); if ($ccuflags =~ /trace/) { my $ps = ''; @@ -5254,9 +5767,9 @@ sub HMCCU_RPCSetConfig ($$$) Log3 $name, 2, "HMCCU: RPCSetConfig: addr=$addr".$ps; } - if ($HMCCU_RPC_PROT{$port} eq 'B') { + if ($rpctype eq 'B') { # Search RPC device - my $rpcdev = HMCCU_GetRPCDevice ($hmccu_hash, 0); + my $rpcdev = HMCCU_GetRPCDevice ($hmccu_hash, 0, $int); return -17 if ($rpcdev eq ''); # Rebuild parameter hash for binary encoding @@ -5266,12 +5779,18 @@ sub HMCCU_RPCSetConfig ($$$) $binpar{$e}{V} = $parref->{$e}; } - $res = HMCCURPC_SendBinRequest ($defs{$rpcdev}, $port, "putParamset", $BINRPC_STRING, $addr, - $BINRPC_STRING, "MASTER", $BINRPC_STRUCT, \%binpar); + if ($ccuflags =~ /extrpc/) { + $res = HMCCURPC_SendBinRequest ($defs{$rpcdev}, $port, "putParamset", $BINRPC_STRING, $addr, + $BINRPC_STRING, "MASTER", $BINRPC_STRUCT, \%binpar); + } + elsif ($ccuflags =~ /procrpc/) { + $res = HMCCURPCPROC_SendRequest ($defs{$rpcdev}, "putParamset", $BINRPC_STRING, $addr, + $BINRPC_STRING, "MASTER", $BINRPC_STRUCT, \%binpar); + } } else { - my $url = "http://".$hmccu_hash->{host}.":".$port."/"; - $url .= $HMCCU_RPC_URL{$port} if (exists ($HMCCU_RPC_URL{$port})); + my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url'); + return -9 if (!defined ($url)); my $client = RPC::XML::Client->new ($url); $res = $client->simple_request ("putParamset", $addr, "MASTER", $parref); } @@ -5723,7 +6242,7 @@ sub HMCCU_EncodeEPDisplay ($) $sig = $disp_signals{$conf{signal}} if (exists ($disp_signals{$conf{signal}})); $cmd .= ','.$sig.',0x03'; - return '"'.$cmd.'"'; + return $cmd; } ###################################################################### @@ -5795,7 +6314,6 @@ sub HMCCU_GetDutyCycle ($) my ($hash) = @_; my $name = $hash->{NAME}; - my $host = $hash->{host}; my $dc = 0; my @rpcports = HMCCU_GetRPCPortList ($hash); @@ -5803,7 +6321,8 @@ sub HMCCU_GetDutyCycle ($) foreach my $port (@rpcports) { next if ($port != 2001 && $port != 2010); - my $url = "http://$host:$port/"; + my $url = HMCCU_GetRPCServerInfo ($hash, $port, 'url'); + next if (!defined ($url)); my $rpcclient = RPC::XML::Client->new ($url); my $response = $rpcclient->simple_request ("listBidcosInterfaces"); next if (!defined ($response) || ref($response) ne 'ARRAY'); @@ -5811,7 +6330,7 @@ sub HMCCU_GetDutyCycle ($) next if (ref ($iface) ne 'HASH'); next if (!exists ($iface->{DUTY_CYCLE})); $dc++; - my $type = exists ($iface->{TYPE}) ? $iface->{TYPE} : $HMCCU_RPC_NUMPORT{$port}; + my $type = exists ($iface->{TYPE}) ? $iface->{TYPE} : HMCCU_GetRPCServerInfo ($hash, $port, 'name'); readingsBulkUpdate ($hash, "iface_addr_$dc", $iface->{ADDRESS}); readingsBulkUpdate ($hash, "iface_conn_$dc", $iface->{CONNECTED}); readingsBulkUpdate ($hash, "iface_type_$dc", $type); @@ -5962,16 +6481,21 @@ sub HMCCU_CCURPC_OnRun ($) Log3 $name, 0, "CCURPC: Callback server created listening on port $callbackport"; } + # Format of signature: + # string par1 ... parN + # Callback for events + # Parameters: Server, InterfaceId, Address, ValueKey, Value Log3 $name, 1, "CCURPC: $clkey Adding callback for events"; $ccurpc_server->add_method ( { name=>"event", - signature=> ["string string string string int","string string string string double","string string string string boolean","string string string string i4"], + 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=>\&HMCCU_CCURPC_EventCB } ); # Callback for new devices + # Parameters: Server, InterfaceId, DeviceDescriptions[] Log3 $name, 1, "CCURPC: $clkey Adding callback for new devices"; $ccurpc_server->add_method ( { name=>"newDevices", @@ -5981,6 +6505,7 @@ sub HMCCU_CCURPC_OnRun ($) ); # Callback for deleted devices + # Parameters: Server, InterfaceId, Addresses[] Log3 $name, 1, "CCURPC: $clkey Adding callback for deleted devices"; $ccurpc_server->add_method ( { name=>"deleteDevices", @@ -5990,6 +6515,7 @@ sub HMCCU_CCURPC_OnRun ($) ); # Callback for modified devices + # Parameters: Server, InterfaceId, Address, Hint Log3 $name, 1, "CCURPC: $clkey Adding callback for modified devices"; $ccurpc_server->add_method ( { name=>"updateDevice", @@ -5999,6 +6525,7 @@ sub HMCCU_CCURPC_OnRun ($) ); # Callback for replaced devices + # Parameters: Server, InterfaceId, OldAddress, NewAddress Log3 $name, 1, "CCURPC: $clkey Adding callback for replaced devices"; $ccurpc_server->add_method ( { name=>"replaceDevice", @@ -6008,6 +6535,7 @@ sub HMCCU_CCURPC_OnRun ($) ); # Callback for readded devices + # Parameters: Server, InterfaceId, Addresses[] Log3 $name, 1, "CCURPC: $clkey Adding callback for readded devices"; $ccurpc_server->add_method ( { name=>"replaceDevice", @@ -6017,10 +6545,11 @@ sub HMCCU_CCURPC_OnRun ($) ); # Dummy implementation, always return an empty array + # Parameters: Server, InterfaceId Log3 $name, 1, "CCURPC: $clkey Adding callback for list devices"; $ccurpc_server->add_method ( { name=>"listDevices", - signature=>["array string"], + signature=>["string string"], code=>\&HMCCU_CCURPC_ListDevicesCB } ); @@ -6234,6 +6763,9 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) Set

    +
  • set <name> ackmessages
    + Acknowledge device was unreachable messages in CCU. +

  • set <name> cleardefaults
    Clear default attributes imported from file.

  • @@ -6385,9 +6917,6 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) Attributes

      -
    • ccuackstate {0 | 1}
      - If set to 1 state will be set to result of command (i.e. 'OK'). -

    • ccuaggregate <rule>[;...]
      Define aggregation rules for client device readings. With an aggregation rule it's easy to detect if some or all client device readings are set to a specific @@ -6458,8 +6987,9 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) practice for creating a custom default attribute file is by exporting predefined default attributes from HMCCU with command 'get exportdefaults'.

    • -
    • ccuflags {extrpc, intrpc}
      - Control RPC server process and datapoint validation:
      +
    • ccuflags {extrpc, procprc, intrpc}
      + Control behaviour of several HMCCU functions:
      + ackState - Acknowledge command execution by setting STATE to error or success.
      intrpc - Use internal RPC server. This is the default.
      extrpc - Use external RPC server provided by module HMCCURPC. If no HMCCURPC device exists HMCCU will create one after command 'set rpcserver on'.
      @@ -6482,12 +7012,13 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)

    • rpcinterfaces <interface>[,...]
      Specify list of CCU RPC interfaces. HMCCU will register a RPC server for each interface. - Valid interfaces are:

      + Interface BidCos-RF is default and always active. Valid interfaces are:

      • BidCos-Wired (Port 2000)
      • BidCos-RF (Port 2001)
      • Homegear (Port 2003)
      • HmIP-RF (Port 2010)
      • +
      • HVL (Port 7000)
      • CUxD (Port 8701)
      • VirtualDevice (Port 9292)
      @@ -6503,6 +7034,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
    • 2001 = BidCos-RF (wireless 868 MHz components with BidCos protocol)
    • 2003 = Homegear (experimental)
    • 2010 = HM-IP (wireless 868 MHz components with IPv6 protocol)
    • +
    • 7000 = HVL (Homematic Virtual Layer devices)
    • 8701 = CUxD (only supported with external RPC server HMCCURPC)
    • 9292 = CCU group devices (especially heating groups)
    diff --git a/fhem/FHEM/88_HMCCUCHN.pm b/fhem/FHEM/88_HMCCUCHN.pm index 682d6a3d8..375f3e335 100644 --- a/fhem/FHEM/88_HMCCUCHN.pm +++ b/fhem/FHEM/88_HMCCUCHN.pm @@ -4,9 +4,9 @@ # # $Id$ # -# Version 4.1.003 +# Version 4.2 # -# (c) 2017 zap (zap01 t-online de) +# (c) 2018 zap (zap01 t-online de) # ###################################################################### # @@ -15,7 +15,7 @@ # # set config [device] = [...] # set control -# set datapoint +# set datapoint [...] # set defaults # set devstate # set @@ -33,9 +33,8 @@ # get devstate # get update # -# attr ccuackstate { 0 | 1 } # attr ccucalculate :[:][...] -# attr ccuflags { altread, nochn0, trace } +# attr ccuflags { ackState, nochn0, trace } # attr ccuget { State | Value } # attr ccureadings { 0 | 1 } # attr ccureadingfilter [;...] @@ -82,8 +81,8 @@ sub HMCCUCHN_Initialize ($) $hash->{AttrFn} = "HMCCUCHN_Attr"; $hash->{parseParams} = 1; - $hash->{AttrList} = "IODev ccuackstate:0,1 ccucalculate ". - "ccuflags:multiple-strict,altread,nochn0,trace ccureadingfilter ". + $hash->{AttrList} = "IODev ccucalculate ". + "ccuflags:multiple-strict,ackState,nochn0,trace ccureadingfilter ". "ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc ". "ccureadingname:textField-long ". "ccureadings:0,1 ccuscaleval ccuverify:0,1,2 ccuget:State,Value controldatapoint ". @@ -219,20 +218,28 @@ sub HMCCUCHN_Set ($@) my $rc; if ($opt eq 'datapoint') { - my $objname = shift @$a; - my $objvalue = shift @$a; + my $usage = "Usage: set $name datapoint {datapoint} {value} [...]"; + my %dpval; + while (my $objname = shift @$a) { + my $objvalue = shift @$a; - return HMCCU_SetError ($hash, "Usage: set $name datapoint {datapoint} {value}") - if (!defined ($objname) || !defined ($objvalue)); - return HMCCU_SetError ($hash, -8) - if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $objname, 2)); + return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue)); + return HMCCU_SetError ($hash, -8) + if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $objname, 2)); - $objvalue =~ s/\\_/%20/g; - $objvalue = HMCCU_Substitute ($objvalue, $statevals, 1, undef, ''); + $objvalue =~ s/\\_/%20/g; + $objvalue = HMCCU_Substitute ($objvalue, $statevals, 1, undef, ''); - $objname = $ccuif.'.'.$ccuaddr.'.'.$objname; - $rc = HMCCU_SetDatapoint ($hash, $objname, $objvalue); - return HMCCU_SetError ($hash, $rc) if ($rc < 0); + $objname = $ccuif.'.'.$ccuaddr.'.'.$objname; + $dpval{$objname} = $objvalue; + } + + return HMCCU_SetError ($hash, $usage) if (scalar (keys %dpval) < 1); + + foreach my $dpt (keys %dpval) { + $rc = HMCCU_SetDatapoint ($hash, $dpt, $dpval{$dpt}); + return HMCCU_SetError ($hash, $rc) if ($rc < 0); + } return HMCCU_SetState ($hash, "OK"); } @@ -623,12 +630,13 @@ sub HMCCUCHN_Get ($@) Valid parameters can be listed by using commands 'get configdesc' or 'get configlist'. With option 'device' specified parameters are set in device instead of channel.
    -
  • set <name> datapoint <datapoint> <value>
    - Set value of a datapoint of a CCU channel. If parameter value contains special +
  • set <name> datapoint <datapoint> <value> [...]
    + Set datapoint values of a CCU channel. If parameter value contains special character \_ it's substituted by blank.

    Examples:
    - set temp_control datapoint SET_TEMPERATURE 21 + set temp_control datapoint SET_TEMPERATURE 21
    + set temp_control datapoint AUTO_MODE 1 SET_TEMPERATURE 21

  • set <name> defaults
    Set default attributes for CCU device type. Default attributes are only available for @@ -745,10 +753,6 @@ sub HMCCUCHN_Get ($@) To reduce the amount of events it's recommended to set attribute 'event-on-change-reading' to '.*'.

    -
  • ccuackstate {0 | 1}
    - If set to 1 state will be set to result of command (i.e. 'OK'). Otherwise state is only - updated if value of state datapoint has changed. -

  • ccucalculate <value-type>:<reading>[:<dp-list>[;...]
    Calculate special values like dewpoint based on datapoints specified in dp-list. The result is stored in reading. The following values @@ -766,6 +770,7 @@ sub HMCCUCHN_Get ($@)

  • ccuflags {nochn0, trace}
    Control behaviour of device:
    + ackState: Acknowledge command execution by setting STATE to error or success.
    nochn0: Prevent update of status channel 0 datapoints / readings.
    trace: Write log file information for operations related to this device.

  • @@ -915,16 +920,21 @@ sub HMCCUCHN_Get ($@) set my_switch on
    -
  • stripnumber [<datapoint-expr>!]{0|1|2|-n}[;...]
    - Remove trailing digits or zeroes from floating point numbers and/or round floating - point numbers. If attribute is negative (-0 is valid) floating point values are rounded +
  • stripnumber [<datapoint-expr>!]{0|1|2|-n|%fmt}[;...]
    + Remove trailing digits or zeroes from floating point numbers, round or format + numbers. If attribute is negative (-0 is valid) floating point values are rounded to the specified number of digits before they are stored in readings. The meaning of - values 0-2 is:
    + values 0,1,2 is:
    0 = Floating point numbers are stored as read from CCU (i.e. with trailing zeros)
    1 = Trailing zeros are stripped from floating point numbers except one digit.
    2 = All trailing zeros are stripped from floating point numbers.
    + With %fmt one can specify any valid sprintf() format string.
    If datapoint-expr is specified the formatting applies only to datapoints - matching the regular expression. + matching the regular expression.
    + Example:
    + + attr myDev stripnumber TEMPERATURE!%.2f degree +

  • substexcl <reading-expr>
    Exclude values of readings matching reading-expr from substitution. This is helpful diff --git a/fhem/FHEM/88_HMCCUDEV.pm b/fhem/FHEM/88_HMCCUDEV.pm index cc8fb60cb..86f8c5946 100644 --- a/fhem/FHEM/88_HMCCUDEV.pm +++ b/fhem/FHEM/88_HMCCUDEV.pm @@ -4,9 +4,9 @@ # # $Id$ # -# Version 4.1.001 +# Version 4.2 # -# (c) 2017 zap (zap01 t-online de) +# (c) 2018 zap (zap01 t-online de) # ###################################################################### # @@ -16,7 +16,7 @@ # set clear [] # set config [] = [...] # set control -# set datapoint [.] +# set datapoint [.] [...] # set defaults # set devstate # set on-till @@ -33,9 +33,8 @@ # get devstate # get update # -# attr ccuackstate { 0 | 1 } # attr ccucalculate :[:][...] -# attr ccuflags { altread, nochn0, trace } +# attr ccuflags { ackState, nochn0, trace } # attr ccuget { State | Value } # attr ccureadings { 0 | 1 } # attr ccureadingformat { address[lc] | name[lc] | datapoint[lc] } @@ -85,8 +84,8 @@ sub HMCCUDEV_Initialize ($) $hash->{AttrFn} = "HMCCUDEV_Attr"; $hash->{parseParams} = 1; - $hash->{AttrList} = "IODev ccuackstate:0,1 ccucalculate:textField-long ". - "ccuflags:multiple-strict,altread,nochn0,trace ccureadingfilter:textField-long ". + $hash->{AttrList} = "IODev ccucalculate:textField-long ". + "ccuflags:multiple-strict,ackState,nochn0,trace ccureadingfilter:textField-long ". "ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc ". "ccureadingname:textField-long ". "ccureadings:0,1 ccuget:State,Value ccuscaleval ccuverify:0,1,2 disable:0,1 ". @@ -142,7 +141,7 @@ sub HMCCUDEV_Define ($@) $hmccu_hash = HMCCU_FindIODevice ($devspec) if (!defined ($hmccu_hash)); return "Cannot detect IO device" if (!defined ($hmccu_hash)); - return "Invalid or unknown CCU device name or address" + return "Invalid or unknown CCU device name or address: $devspec" if (! HMCCU_IsValidDevice ($hmccu_hash, $devspec)); my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($hmccu_hash, $devspec); @@ -180,14 +179,9 @@ sub HMCCUDEV_Define ($@) return "Invalid device or channel $gd" if (!HMCCU_IsValidDevice ($hmccu_hash, $gd)); - if (HMCCU_IsDevAddr ($gd, 0) || HMCCU_IsChnAddr ($gd, 1)) { - $gdo = $gd; - } - else { - ($gda, $gdc) = HMCCU_GetAddress ($hmccu_hash, $gd, '', ''); - $gdo = $gda; - $gdo .= ':'.$gdc if ($gdc ne ''); - } + ($gda, $gdc) = HMCCU_GetAddress ($hmccu_hash, $gd, '', ''); + $gdo = $gda; + $gdo .= ':'.$gdc if ($gdc ne ''); if (exists ($hash->{ccugroup}) && $hash->{ccugroup} ne '') { $hash->{ccugroup} .= ",".$gdo; @@ -287,42 +281,49 @@ sub HMCCUDEV_Set ($@) my $rc; if ($opt eq 'datapoint') { - my $objname = shift @$a; - my $objvalue = shift @$a; + my $usage = "Usage: set $name datapoint [{channel-number}.]{datapoint} {value} [...]"; + my %dpval; + while (my $objname = shift @$a) { + my $objvalue = shift @$a; - if ($ccutype eq 'HM-Dis-EP-WM55' && !defined ($objvalue)) { - $objvalue = ''; - foreach my $t (keys %{$h}) { - if ($objvalue eq '') { - $objvalue = $t.'='.$h->{$t}; - } - else { - $objvalue .= ','.$t.'='.$h->{$t}; + if ($ccutype eq 'HM-Dis-EP-WM55' && !defined ($objvalue)) { + $objvalue = ''; + foreach my $t (keys %{$h}) { + if ($objvalue eq '') { + $objvalue = $t.'='.$h->{$t}; + } + else { + $objvalue .= ','.$t.'='.$h->{$t}; + } } } - } - return HMCCU_SetError ($hash, "Usage: set $name datapoint [{channel-number}.]{datapoint} {value}") - if (!defined ($objvalue) || $objvalue eq ''); + return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue) || $objvalue eq ''); - if ($objname =~ /^([0-9]+)\..+$/) { - my $chn = $1; - return HMCCU_SetError ($hash, -7) if ($chn >= $hash->{channels}); - } - else { - return HMCCU_SetError ($hash, -11) if ($sc eq ''); - $objname = $sc.'.'.$objname; - } + if ($objname =~ /^([0-9]+)\..+$/) { + my $chn = $1; + return HMCCU_SetError ($hash, -7) if ($chn >= $hash->{channels}); + } + else { + return HMCCU_SetError ($hash, -11) if ($sc eq ''); + $objname = $sc.'.'.$objname; + } - return HMCCU_SetError ($hash, -8) - if (!HMCCU_IsValidDatapoint ($hash, $ccutype, 0, $objname, 2)); + return HMCCU_SetError ($hash, -8) + if (!HMCCU_IsValidDatapoint ($hash, $ccutype, 0, $objname, 2)); - $objvalue =~ s/\\_/%20/g; - $objvalue = HMCCU_Substitute ($objvalue, $statevals, 1, undef, ''); - $objname = $ccuif.'.'.$ccuaddr.':'.$objname; + $objvalue =~ s/\\_/%20/g; + $objvalue = HMCCU_Substitute ($objvalue, $statevals, 1, undef, ''); + $objname = $ccuif.'.'.$ccuaddr.':'.$objname; + $dpval{$objname} = $objvalue; + } - $rc = HMCCU_SetDatapoint ($hash, $objname, $objvalue); - return HMCCU_SetError ($hash, $rc) if ($rc < 0); + return HMCCU_SetError ($hash, $usage) if (scalar (keys %dpval) < 1); + + foreach my $dpt (keys %dpval) { + $rc = HMCCU_SetDatapoint ($hash, $dpt, $dpval{$dpt}); + return HMCCU_SetError ($hash, $rc) if ($rc < 0); + } return HMCCU_SetState ($hash, "OK"); } @@ -775,12 +776,13 @@ sub HMCCUDEV_Get ($@) using command 'get configdesc'.

  • set <name> datapoint [<channel-number>.]<datapoint> - <value>
    - Set value of a datapoint of a CCU device channel. If channel number is not specified + <value> [...]
    + Set datapoint values of a CCU device channel. If channel number is not specified state channel is used. String \_ is substituted by blank.

    Example:
    - set temp_control datapoint 1.SET_TEMPERATURE 21 + set temp_control datapoint 2.SET_TEMPERATURE 21
    + set temp_control datapoint 2.AUTO_MODE 1 2.SET_TEMPERATURE 21

  • set <name> defaults
    Set default attributes for CCU device type. Default attributes are only available for @@ -893,9 +895,6 @@ sub HMCCUDEV_Get ($@)
      To reduce the amount of events it's recommended to set attribute 'event-on-change-reading' to '.*'.

      -
    • ccuackstate {0 | 1}
      - see HMCCUCHN -

    • ccucalculate <value-type>:<reading>[:<dp-list>[;...]
      see HMCCUCHN

    • diff --git a/fhem/FHEM/88_HMCCURPC.pm b/fhem/FHEM/88_HMCCURPC.pm index 52d15660c..8ceee784a 100644 --- a/fhem/FHEM/88_HMCCURPC.pm +++ b/fhem/FHEM/88_HMCCURPC.pm @@ -4,7 +4,7 @@ # # $Id$ # -# Version 0.98 beta +# Version 1.0 # # Thread based RPC Server module for HMCCU. # @@ -40,7 +40,7 @@ use SetExtensions; ###################################################################### # HMCCURPC version -my $HMCCURPC_VERSION = '0.98 beta'; +my $HMCCURPC_VERSION = '1.0'; # Maximum number of events processed per call of Read() my $HMCCURPC_MAX_EVENTS = 50; @@ -74,27 +74,14 @@ my $HMCCURPC_STATISTICS = 500; # Default RPC Port = BidCos-RF my $HMCCURPC_RPC_PORT_DEFAULT = 2001; +my $HMCCURPC_RPC_INTERFACE_DEFAULT = 'BidCos-RF'; -# RPC protocol name by port number -my %HMCCURPC_RPC_NUMPORT = ( - 2000 => 'BidCos-Wired', 2001 => 'BidCos-RF', 2010 => 'HmIP-RF', 9292 => 'VirtualDevices', - 2003 => 'Homegear', 8701 => 'CUxD' -); +# Default RPC server base port +my $HMCCURPC_SERVER_PORT = 5400; # RPC ports by protocol name -my %HMCCURPC_RPC_PORT = ( - 'BidCos-Wired', 2000, 'BidCos-RF', 2001, 'HmIP-RF', 2010, 'VirtualDevices', 9292, - 'Homegear', 2003, 'CUxD', 8701 -); - -# URL extensions -my %HMCCURPC_RPC_URL = ( - 9292, 'groups' -); - -# Type of RPC interface. A=ASCII B=BINARY -my %HMCCURPC_RPC_PROT = ( - 2000 => 'A', 2001 => 'A', 2010 => 'A', 9292 => 'A', 2003 => 'A', 8701 => 'B' +my @HMCCURPC_RPC_INTERFACES = ( + 'BidCos-Wired', 'BidCos-RF', 'HmIP-RF', 'VirtualDevices', 'Homegear', 'CUxD', 'HVL' ); # Initial intervals for registration of RPC callbacks and reading RPC queue @@ -155,13 +142,16 @@ sub HMCCURPC_ProcessEvent ($$); # RPC server management functions sub HMCCURPC_GetAttribute ($$$$); +sub HMCCURPC_GetRPCInterfaceList ($); sub HMCCURPC_GetRPCPortList ($); -sub HMCCURPC_ListDevices ($); +sub HMCCURPC_GetEventTimeout ($$); sub HMCCURPC_RegisterCallback ($); sub HMCCURPC_RegisterSingleCallback ($$$); sub HMCCURPC_DeRegisterCallback ($); -sub HMCCURPC_InitRPCServer ($$$); +sub HMCCURPC_DeRegisterSingleCallback ($$$); +sub HMCCURPC_InitRPCServer ($$$$); sub HMCCURPC_StartRPCServer ($); +sub HMCCURPC_RPCServerStarted ($$); sub HMCCURPC_CleanupThreads ($$$); sub HMCCURPC_CleanupThreadIO ($); sub HMCCURPC_TerminateThreads ($$); @@ -169,8 +159,6 @@ sub HMCCURPC_CheckThreadState ($$$$); sub HMCCURPC_IsRPCServerRunning ($); sub HMCCURPC_Housekeeping ($); sub HMCCURPC_StopRPCServer ($); -sub HMCCURPC_IsAscRPCPort ($); -sub HMCCURPC_IsBinRPCPort ($); sub HMCCURPC_SendRequest ($@); sub HMCCURPC_SendBinRequest ($@); @@ -178,7 +166,7 @@ sub HMCCURPC_SendBinRequest ($@); sub HMCCURPC_HexDump ($$); # RPC server functions -sub HMCCURPC_ProcessRequest ($$); +sub HMCCURPC_ProcessRequest ($$$); sub HMCCURPC_HandleConnection ($$$$); sub HMCCURPC_TriggerIO ($$$); sub HMCCURPC_ProcessData ($$$$); @@ -236,7 +224,7 @@ sub HMCCURPC_Initialize ($) $hash->{ShutdownFn} = "HMCCURPC_Shutdown"; $hash->{parseParams} = 1; - $hash->{AttrList} = "rpcInterfaces:multiple-strict,".join(',',sort keys %HMCCURPC_RPC_PORT). + $hash->{AttrList} = "rpcInterfaces:multiple-strict,".join(',',@HMCCURPC_RPC_INTERFACES). " ccuflags:multiple-strict,expert,keepThreads,logEvents,reconnect". " rpcMaxEvents rpcQueueSize rpcTriggerTime". " rpcServer:on,off rpcServerAddr rpcServerPort rpcWriteTimeout rpcAcceptTimeout". @@ -253,37 +241,33 @@ sub HMCCURPC_Define ($$) my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; my $hmccu_hash; - my $usage = "Usage: define $name HMCCURPC { CCUHost [noiodev] | iodev=Device_Name }"; + my $usage = "Usage: define $name HMCCURPC { CCUHost | iodev=Device_Name }"; $hash->{version} = $HMCCURPC_VERSION; - $hash->{noiodev} = 0; if (exists ($h->{iodev})) { my $ioname = $h->{iodev}; - return "HMCCU I/O device $ioname not found" if (!exists ($defs{$ioname})); - return "Device $ioname is no HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); + return "HMCCURPC: HMCCU I/O device $ioname not found" if (!exists ($defs{$ioname})); + return "HMCCURPC: Device $ioname is not a HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); $hmccu_hash = $defs{$ioname}; $hash->{host} = $hmccu_hash->{host}; } else { return $usage if (scalar (@$a) < 3); $hash->{host} = $$a[2]; - if (scalar (@$a) > 3) { - return $usage if ($$a[3] ne 'noiodev'); - $hash->{noiodev} = 1; - } } - # Try to find I/O device if not defined by parameter iodev - if (!defined ($hmccu_hash) && $hash->{noiodev} == 0) { + # Try to find I/O device + if (!defined ($hmccu_hash)) { $hmccu_hash = HMCCURPC_FindHMCCUDevice ($hash); - return "Can't find HMCCU I/O device" if (!defined ($hmccu_hash)); + return "HMCCURPC: Can't find HMCCU I/O device" if (!defined ($hmccu_hash)); } if (defined ($hmccu_hash)) { # Set I/O device and store reference for RPC device in I/O device AssignIoPort ($hash, $hmccu_hash->{NAME}); $hmccu_hash->{RPCDEV} = $name; + $hash->{ccuip} = $hmccu_hash->{ccuip}; $hash->{ccutype} = $hmccu_hash->{ccutype}; $hash->{CCUNum} = $hmccu_hash->{CCUNum}; $hash->{ccustate} = $hmccu_hash->{ccustate}; @@ -295,7 +279,7 @@ sub HMCCURPC_Define ($$) my $ch = $defs{$d}; next if (!exists ($ch->{TYPE})); $ccucount++ if ($ch->{TYPE} eq 'HMCCU'); - $ccucount++ if ($ch->{TYPE} eq 'HMCCURPC' && $ch != $hash && $ch->{noiodev} == 1); + $ccucount++ if ($ch->{TYPE} eq 'HMCCURPC' && $ch != $hash); } $hash->{CCUNum} = $ccucount+1; $hash->{ccutype} = "CCU2"; @@ -357,6 +341,9 @@ sub HMCCURPC_Attr ($@) my $hash = $defs{$name}; my $rc = 0; + my $hmccu_hash = (exists ($hash->{IODev})) ? $hash->{IODev} : undef; + return "HMCCURPC: Can't find HMCCU I/O device" if (!defined ($hmccu_hash)); + if ($attrname eq 'rpcInterfaces') { my ($run, $all) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_ALL, 'running', undef); return 'Stop RPC server before modifying rpcInterfaces' if ($run > 0); @@ -367,10 +354,11 @@ sub HMCCURPC_Attr ($@) my @ports = split (',', $attrval); my @plist = (); foreach my $p (@ports) { - return "Illegal RPC interface $p" if (!exists ($HMCCURPC_RPC_PORT{$p})); - push (@plist, $HMCCURPC_RPC_PORT{$p}); + my $pn = HMCCU_GetRPCServerInfo ($hmccu_hash, $p, 'port'); + return "HMCCURPC: Illegal RPC interface $p" if (!defined ($pn)); + push (@plist, $pn); } - return "No RPC interface specified" if (scalar (@plist) == 0); + return "HMCCURPC: No RPC interface specified" if (scalar (@plist) == 0); $hash->{hmccu}{rpcports} = join (',', @plist); } } @@ -394,55 +382,48 @@ sub HMCCURPC_Set ($@) my $opt = shift @$a; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $options = $ccuflags =~ /expert/ ? "rpcrequest rpcserver:on,off" : ""; + my $options = $ccuflags =~ /expert/ ? "deregister:".join(',', HMCCURPC_GetRPCInterfaceList ($hash)). + " rpcrequest rpcserver:on,off" : ""; my $busyoptions = $ccuflags =~ /expert/ ? "rpcserver:off" : ""; if ($opt ne 'rpcserver' && HMCCURPC_IsRPCStateBlocking ($hash)) { - HMCCURPC_SetState ($hash, "busy"); +# HMCCURPC_SetState ($hash, "busy"); return "HMCCURPC: CCU busy, choose one of $busyoptions"; } - if ($opt eq 'rpcrequest') { + if ($opt eq 'deregister') { + my $interface = shift @$a; + return "Usage: set $name deregister {Interface}" if (!defined ($interface)); + return "HMCCURPC: Can't find HMCCU I/O device" if (!exists ($hash->{IODev})); + + my $port = HMCCU_GetRPCServerInfo ($hash->{IODev}, $interface, 'port'); + return "HMCCURPC: Illegal RPC interface $interface" if (!defined ($port)); + + if (!HMCCURPC_DeRegisterSingleCallback ($hash, $port, 1)) { + return HMCCURPC_SetError ($hash, "Degistering RPC callback failed"); + } + return HMCCURPC_SetState ($hash, "OK"); + } + elsif ($opt eq 'rpcrequest') { my $port = shift @$a; my $request = shift @$a; return "Usage: set $name rpcrequest {port} {request} [{parameter} ...]" if (!defined ($request)); - + return "HMCCURPC: Can't find HMCCU I/O device" if (!exists ($hash->{IODev})); + my $response; - if (HMCCURPC_IsAscRPCPort ($port)) { + if (HMCCU_IsRPCType ($hash->{IODev}, $port, 'A')) { $response = HMCCURPC_SendRequest ($hash, $port, $request, @$a); } - elsif (HMCCURPC_IsBinRPCPort ($port)) { + elsif (HMCCU_IsRPCType ($hash->{IODev}, $port, 'B')) { $response = HMCCURPC_SendBinRequest ($hash, $port, $request, @$a); } else { return HMCCURPC_SetError ($hash, "Invalid RPC port $port"); } - return HMCCURPC_SetError ($hash, "RPC request failed") if (!defined ($response)); - my $result = HMCCU_RefToString ($response); -# if (ref ($response) eq 'ARRAY') { -# $result = join "\n", @$response; -# } -# elsif (ref ($response) eq 'HASH') { -# foreach my $k (keys %$response) { -# $result .= "$k = ".$response->{$k}."\n"; -# } -# } -# elsif (ref ($response) eq 'SCALAR') { -# $result = $$response; -# } -# else { -# if (ref ($response)) { -# $result = "Unknown response from CCU of type ".ref ($response); -# } -# else { -# $result = ($response eq '') ? 'Request returned void' : $response; -# } -# } - - return $result; + return HMCCU_RefToString ($response); } elsif ($opt eq 'rpcserver') { my $action = shift @$a; @@ -452,7 +433,7 @@ sub HMCCURPC_Set ($@) if ($action eq 'on') { return HMCCURPC_SetError ($hash, "RPC server already running") - if ($hash->{RPCState} ne 'stopped'); + if ($hash->{RPCState} ne 'inactive'); my ($rc, $info) = HMCCURPC_StartRPCServer ($hash); return HMCCURPC_SetError ($hash, $info) if (!$rc); } @@ -481,7 +462,7 @@ sub HMCCURPC_Get ($@) my $options = "rpcevents:noArg rpcstate:noArg"; if ($opt ne 'rpcstate' && HMCCURPC_IsRPCStateBlocking ($hash)) { - HMCCURPC_SetState ($hash, "busy"); +# HMCCURPC_SetState ($hash, "busy"); return "HMCCURPC: CCU busy, choose one of rpcstate:noArg"; } @@ -558,8 +539,7 @@ sub HMCCURPC_Notify ($$) AssignIoPort ($hash, $hmccu_hash->{NAME}); } else { - Log3 $name, 0, "HMCCURPC: FHEM initialized but HMCCU IO device not found" - if ($hash->{noiodev} == 0); + Log3 $name, 0, "HMCCURPC: FHEM initialized but HMCCU IO device not found"; } } # return if ($rpcserver eq 'off'); @@ -599,7 +579,11 @@ sub HMCCURPC_Read ($) my $child = $hash->{hmccu}{sockchild}; return if (!defined ($hash->{hmccu}{eventqueue})); my $queue = $hash->{hmccu}{eventqueue}; - my $hmccu_hash = (exists ($hash->{IODev}) && $hash->{noiodev} == 0) ? $hash->{IODev} : $hash; + my $hmccu_hash = (exists ($hash->{IODev})) ? $hash->{IODev} : undef; + if (!defined ($hmccu_hash)) { + Log3 $name, 4, "HMCCURPC: Can't find I/O device"; + return; + } # Get attributes my $rpcmaxevents = AttrVal ($name, 'rpcMaxEvents', $HMCCURPC_MAX_EVENTS); @@ -628,15 +612,20 @@ sub HMCCURPC_Read ($) $evcount++; $hmccu_hash->{ccustate} = 'active' if ($hmccu_hash->{ccustate} ne 'active'); } + elsif ($et eq 'EX') { + last; + } elsif ($et eq 'ND') { $devices{$par[0]}{flag} = 'N'; $devices{$par[0]}{version} = $par[3]; 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]; } else { + $devices{$par[0]}{addtype} = 'chn'; $devices{$par[0]}{usetype} = $par[2]; } $devcount++; @@ -653,15 +642,22 @@ sub HMCCURPC_Read ($) elsif ($et eq 'TO') { $hmccu_hash->{ccustate} = 'timeout'; if ($hash->{RPCState} eq 'running' && $ccuflags =~ /reconnect/) { - if (HMCCU_TCPConnect ($hash->{host}, $par[0])) { - $hmccu_hash->{ccustate} = 'active'; - Log3 $name, 2, "HMCCURPC: Reconnecting to CCU interface ". - $HMCCURPC_RPC_NUMPORT{$par[0]}; - HMCCURPC_RegisterSingleCallback ($hash, $par[0], 1); + my $serveraddr = HMCCU_GetRPCServerInfo ($hmccu_hash, $par[0], 'host'); + my $ifname = HMCCU_GetRPCServerInfo ($hmccu_hash, $par[0], 'name'); + if (defined ($serveraddr) && defined ($ifname)) { + if (HMCCU_TCPConnect ($serveraddr, $par[0])) { + $hmccu_hash->{ccustate} = 'active'; + Log3 $name, 2, "HMCCURPC: Reconnecting to RPC interface $ifname". + " on host $serveraddr"; + HMCCURPC_RegisterSingleCallback ($hash, $par[0], 1); + } + else { + $hmccu_hash->{ccustate} = 'unreachable'; + Log3 $name, 1, "HMCCURPC: CCU not reachable on port ".$par[0]; + } } else { - $hmccu_hash->{ccustate} = 'unreachable'; - Log3 $name, 1, "HMCCURPC: CCU not reachable on port ".$par[0]; + Log3 $name, 1, "HMCCURPC: Can't get ip address for port ".$par[0]; } } } @@ -725,6 +721,7 @@ sub HMCCURPC_SetState ($$) sub HMCCURPC_SetRPCState ($$$) { my ($hash, $state, $msg) = @_; + my $name = $hash->{NAME}; # Search HMCCU device and check for running RPC servers my $hmccu_hash; @@ -732,13 +729,14 @@ sub HMCCURPC_SetRPCState ($$$) $hash->{RPCState} = $state; readingsSingleUpdate ($hash, "rpcstate", $state, 1); + + HMCCURPC_SetState ($hash, 'busy') if ($state ne 'running' && $state ne 'inactive' && + $state ne 'error' && ReadingsVal ($name, 'state', '') ne 'busy'); + Log3 $hash->{NAME}, 1, "HMCCURPC: $msg" if (defined ($msg)); - + # Update internals of I/O device - if (defined ($hmccu_hash)) { - $hmccu_hash->{RPCState} = $state; - readingsSingleUpdate ($hmccu_hash, "rpcstate", $state, 1); - } + HMCCU_SetRPCState ($hmccu_hash, $state) if (defined ($hmccu_hash)); } ###################################################################### @@ -749,28 +747,13 @@ sub HMCCURPC_ResetRPCState ($$) { my ($hash, $state) = @_; - # Search HMCCU device and check for running RPC servers - my $hmccu_hash; - $hmccu_hash = $hash->{IODev} if (exists ($hash->{IODev}) && $hash->{noiodev} == 0); - - $hash->{RPCState} = "stopped"; # RPC server state $hash->{RPCTID} = "0"; # List of RPC server thread IDs $hash->{hmccu}{evtime} = 0; # Timestamp of last event from CCU $hash->{hmccu}{rpcstarttime} = 0; # Timestamp of RPC server start - readingsBeginUpdate ($hash); - readingsBulkUpdate ($hash, "state", $state); - readingsBulkUpdate ($hash, "rpcstate", "stopped"); - readingsEndUpdate ($hash, 1); - - if (defined ($hmccu_hash) && $state ne "initialized") { - $hmccu_hash->{RPCState} = "stopped"; - readingsBeginUpdate ($hmccu_hash); - readingsBulkUpdate ($hmccu_hash, "state", $state); - readingsBulkUpdate ($hmccu_hash, "rpcstate", "stopped"); - readingsEndUpdate ($hmccu_hash, 1); - } + HMCCURPC_SetState ($hash, $state); + HMCCURPC_SetRPCState ($hash, 'inactive', undef); } ###################################################################### @@ -781,7 +764,7 @@ sub HMCCURPC_IsRPCStateBlocking ($) { my ($hash) = @_; - return ($hash->{RPCState} eq "running" || $hash->{RPCState} eq "stopped") ? 0 : 1; + return ($hash->{RPCState} eq "running" || $hash->{RPCState} eq "inactive") ? 0 : 1; } ###################################################################### @@ -793,7 +776,6 @@ sub HMCCURPC_FindHMCCUDevice ($) { my ($hash) = @_; - return undef if ($hash->{noiodev} == 1); return $hash->{IODev} if (defined ($hash->{IODev})); for my $d (keys %defs) { @@ -816,7 +798,7 @@ sub HMCCURPC_ProcessEvent ($$) my $name = $hash->{NAME}; my $rh = \%{$hash->{hmccu}{rpc}}; # Just for code simplification my $hmccu_hash; - $hmccu_hash = $hash->{IODev} if (exists ($hash->{IODev}) && $hash->{noiodev} == 0); + $hmccu_hash = $hash->{IODev} if (exists ($hash->{IODev})); # Number of arguments in RPC events (without event type and clkey) my %rpceventargs = ( @@ -842,6 +824,9 @@ sub HMCCURPC_ProcessEvent ($$) my $clkey = shift @t; my $tc = scalar (@t); + # Log event + Log3 $name, 2, "HMCCURPC: CCUEvent = $event" if ($ccuflags =~ /logEvents/); + # Check event data if (!defined ($clkey)) { Log3 $name, 2, "HMCCURPC: Syntax error in RPC event data"; @@ -850,7 +835,7 @@ sub HMCCURPC_ProcessEvent ($$) # Check for valid server if (!exists ($rh->{$clkey})) { - Log3 $name, 0, "HMCCURPC: Received SL event for unknown RPC server $clkey"; + Log3 $name, 0, "HMCCURPC: Received event of type $et for unknown RPC server $clkey"; return undef; } @@ -868,9 +853,6 @@ sub HMCCURPC_ProcessEvent ($$) return undef; } - # Log event - Log3 $name, 2, "HMCCURPC: CCUEvent = $event" if ($ccuflags =~ /logEvents/); - # Update statistic counters $rh->{$clkey}{rec}{$et}++; $rh->{$clkey}{evtime} = time (); @@ -895,18 +877,21 @@ sub HMCCURPC_ProcessEvent ($$) # # RPC server enters server loop # Input: SL|clkey|Tid - # Output: SL, clkey, countWorking + # Output: SL, clkey, countWorking, countRunning, ClientsUpdated, UpdateErrors # if ($t[0] == $rh->{$clkey}{tid}) { - Log3 $name, 1, "HMCCURPC: Received SL event. RPC server $clkey enters server loop"; + Log3 $name, 1, "HMCCURPC: Received SL event. Process $clkey enters server loop"; $rh->{$clkey}{state} = $clkey eq 'DATA' ? 'running' : 'working'; my ($run, $alld) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_DATA, 'running', undef); my ($work, $alls) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_SERVER, 'working', undef); if ($work == $alls && $run == $alld) { Log3 $name, 1, "HMCCURPC: All threads working"; - HMCCURPC_RegisterCallback ($hash); - } - return ($et, $clkey, $work); + if (!HMCCURPC_RegisterCallback ($hash)) { + Log3 $name, 1, "HMCCURPC: No RPC callbacks registered"; + } + } + my ($srun, $c_ok, $c_err) = HMCCURPC_RPCServerStarted ($hash, $hmccu_hash); + return ($et, $clkey, $work, $srun, $c_ok, $c_err); } else { Log3 $name, 0, "HMCCURPC: Received SL event. Wrong TID=".$t[0]." for RPC server $clkey"; @@ -917,38 +902,23 @@ sub HMCCURPC_ProcessEvent ($$) # # RPC server initialized # Input: IN|clkey|INIT|State - # Output: IN, clkey, Running, ClientsUpdated, UpdateErrors + # Output: IN, clkey, countRunning, ClientsUpdated, UpdateErrors # - my $c_ok = 0; - my $c_err = 0; Log3 $name, 1, "HMCCURPC: Received IN event. RPC server $clkey running."; return ($et, $clkey, 0, 0, 0) if ($rh->{$clkey}{state} eq 'running'); $rh->{$clkey}{state} = "running"; # Set binary RPC interfaces to 'running' if all ascii interfaces are in state 'running' - my ($runa, $alla) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_ASCII, 'running', undef); - if ($runa == $alla) { - foreach my $sn (keys %{$rh}) { - $rh->{$sn}{state} = "running" - if ($rh->{$sn}{type} == $HMCCURPC_THREAD_BINARY && $rh->{$sn}{state} eq 'registered'); - } - } - - # Check if all RPC servers were initialized. Set overall status - my ($run, $all) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_ALL, 'running', undef); - if ($run == $all) { - $hash->{hmccu}{rpcstarttime} = 0; - HMCCURPC_SetRPCState ($hash, "running", "All RPC servers running"); - HMCCURPC_SetState ($hash, "OK"); - if (defined ($hmccu_hash)) { - HMCCU_SetState ($hmccu_hash, "OK"); - ($c_ok, $c_err) = HMCCU_UpdateClients ($hmccu_hash, '.*', 'Attr', 0); - Log3 $name, 2, "HMCCURPC: Updated devices. Success=$c_ok Failed=$c_err"; - } - RemoveInternalTimer ($hash); - DoTrigger ($name, "RPC server running"); - } +# my ($runa, $alla) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_ASCII, 'running', undef); +# if ($runa == $alla) { +# foreach my $sn (keys %{$rh}) { +# $rh->{$sn}{state} = "running" +# if ($rh->{$sn}{type} == $HMCCURPC_THREAD_BINARY && $rh->{$sn}{state} eq 'registered'); +# } +# } + + my ($run, $c_ok, $c_err) = HMCCURPC_RPCServerStarted ($hash, $hmccu_hash); return ($et, $clkey, $run, $c_ok, $c_err); } elsif ($et eq 'EX') { @@ -962,11 +932,11 @@ sub HMCCURPC_ProcessEvent ($$) my $stopped = 0; my $all = 0; - $rh->{$clkey}{state} = 'stopped'; + $rh->{$clkey}{state} = 'inactive'; # Check if all threads were terminated. Set overall status if ($clkey ne 'DATA') { - ($stopped, $all) = HMCCURPC_CleanupThreads ($hash, $HMCCURPC_THREAD_SERVER, 'stopped'); + ($stopped, $all) = HMCCURPC_CleanupThreads ($hash, $HMCCURPC_THREAD_SERVER, 'inactive'); if ($stopped == $all) { # Terminate data processing thread if all server threads stopped Log3 $name, 2, "HMCCURPC: All RPC servers stopped. Terminating data processing thread"; @@ -1050,6 +1020,34 @@ sub HMCCURPC_ProcessEvent ($$) return undef; } +###################################################################### +# Get list of RPC interfaces. +# If no interfaces defined in HMCCURPC device get interfaces list from +# I/O device. +###################################################################### + +sub HMCCURPC_GetRPCInterfaceList ($) +{ + my ($hash) = @_; + my @interfaces = ($HMCCURPC_RPC_INTERFACE_DEFAULT); + + my $hmccu_hash = HMCCURPC_FindHMCCUDevice ($hash); + return @interfaces if (!defined ($hmccu_hash)); + + if (defined ($hash->{hmccu}{rpcports})) { + foreach my $p (split (',', $hash->{hmccu}{rpcports})) { + my $ifname = HMCCU_GetRPCServerInfo ($hmccu_hash, $p, 'name'); + next if (!defined ($ifname) || $ifname eq $HMCCURPC_RPC_INTERFACE_DEFAULT); + push (@interfaces, $ifname); + } + } + else { + @interfaces = HMCCU_GetRPCInterfaceList ($hmccu_hash); + } + + return @interfaces; +} + ###################################################################### # Get list of RPC ports. # If no ports defined in HMCCURPC device get port list from I/O @@ -1096,20 +1094,29 @@ sub HMCCURPC_GetAttribute ($$$$) } ###################################################################### -# Request device list from CCU +# Get event timeout for interface ###################################################################### -sub HMCCURPC_ListDevices ($) +sub HMCCURPC_GetEventTimeout ($$) { - my ($hash, $port) = @_; - my $name = $hash->{NAME}; + my ($evttimeout, $interface) = @_; + + return $evttimeout if ($evttimeout =~ /^[0-9]+$/); + + my $seconds = -1; + my $defseconds = $HMCCURPC_TIMEOUT_EVENT; - my $serveraddr = $hash->{host}; - my $clurl = "http://$serveraddr:$port/"; - $clurl .= $HMCCURPC_RPC_URL{$port} if (exists ($HMCCURPC_RPC_URL{$port})); - - my $rpcclient = RPC::XML::Client->new ($clurl); - my $res = $rpcclient->send_request ("listDevices"); + foreach my $to (split (',', $evttimeout)) { + my ($toint, $tosec) = split (':', $to); + if (!defined ($tosec)) { + $defseconds = $toint if ($toint =~ /^[0-9]+$/); + } + else { + return $tosec if ($toint eq $interface && $tosec =~ /^[0-9]+$/); + } + } + + return $defseconds; } ###################################################################### @@ -1121,12 +1128,15 @@ sub HMCCURPC_ListDevices ($) sub HMCCURPC_RegisterCallback ($) { my ($hash) = @_; + my $name = $hash->{NAME}; my @rpcports = HMCCURPC_GetRPCPortList ($hash); my $regcount = 0; foreach my $port (@rpcports) { - $regcount++ if (HMCCURPC_RegisterSingleCallback ($hash, $port, 0)); + my ($rc, $msg) = HMCCURPC_RegisterSingleCallback ($hash, $port, 0); + Log3 $name, 1, "HMCCURPC: $msg"; + $regcount++ if ($rc); } return $regcount; @@ -1144,47 +1154,42 @@ sub HMCCURPC_RegisterSingleCallback ($$$) my ($hash, $port, $force) = @_; my $name = $hash->{NAME}; - my $serveraddr = $hash->{host}; + return (0, "Can't find IO device") if (!exists ($hash->{IODev})); + my $hmccu_hash = $hash->{IODev}; my $localaddr = $hash->{hmccu}{localaddr}; - my $rpcserveraddr = AttrVal ($name, 'rpcServerAddr', $localaddr); my $clkey = 'CB'.$port; - return 0 if (!exists ($hash->{hmccu}{rpc}{$clkey})); - return 0 if ($hash->{hmccu}{rpc}{$clkey}{state} ne 'working' && $force == 0); + return (0, "RPC server $clkey not found") if (!exists ($hash->{hmccu}{rpc}{$clkey})); + return (0, "RPC server $clkey not in state working") if ($hash->{hmccu}{rpc}{$clkey}{state} ne 'working' && $force == 0); - my $cburl = ''; - if (HMCCURPC_IsAscRPCPort ($port)) { - $cburl = "http://$rpcserveraddr:".$hash->{hmccu}{rpc}{$clkey}{cbport}."/fh".$port; - } - else { - $cburl = "xmlrpc_bin://$rpcserveraddr:".$hash->{hmccu}{rpc}{$clkey}{cbport}; - } - - my $clurl = "http://$serveraddr:$port/"; - $clurl .= $HMCCURPC_RPC_URL{$port} if (exists ($HMCCURPC_RPC_URL{$port})); + my $cburl = HMCCU_GetRPCCallbackURL ($hash, $localaddr, $hash->{hmccu}{rpc}{$clkey}{cbport}, $clkey, $port); + my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url'); + my $rpctype = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'type'); + return (0, "Can't get RPC parameters for ID $clkey") if (!defined ($cburl) || !defined ($clurl) || !defined ($rpctype)); + my $rpcflags = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'flags'); $hash->{hmccu}{rpc}{$clkey}{port} = $port; $hash->{hmccu}{rpc}{$clkey}{clurl} = $clurl; $hash->{hmccu}{rpc}{$clkey}{cburl} = $cburl; - $hash->{hmccu}{rpc}{$clkey}{state} = 'registered' if ($force == 0); Log3 $name, 2, "HMCCURPC: Registering callback $cburl with ID $clkey at $clurl"; my $rc; - if (HMCCURPC_IsAscRPCPort ($port)) { + if ($rpctype eq 'A') { $rc = HMCCURPC_SendRequest ($hash, $port, "init", $cburl, $clkey); } else { $rc = HMCCURPC_SendBinRequest ($hash, $port, "init", $BINRPC_STRING, $cburl, $BINRPC_STRING, $clkey); } - + if (defined ($rc)) { - Log3 $name, 1, "HMCCURPC: RPC callback with URL $cburl registered"; - return 1; + if ($force == 0) { + $hash->{hmccu}{rpc}{$clkey}{state} = $rpcflags =~ /forceInit/ ? 'running' : 'registered'; + } + return (1, "RPC callback with URL $cburl for ID $clkey registered"); } else { - Log3 $name, 1, "HMCCURPC: Failed to register callback for ID $clkey"; - return 0; + return (0, "Failed to register callback for ID $clkey"); } } @@ -1202,42 +1207,79 @@ sub HMCCURPC_DeRegisterCallback ($) foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { my $rpchash = \%{$hash->{hmccu}{rpc}{$clkey}}; - next if ($rpchash->{state} ne 'registered' && $rpchash->{state} ne 'running'); - if (exists ($rpchash->{cburl}) && $rpchash->{cburl} ne '') { - Log3 $name, 1, "HMCCURPC: Deregistering RPC server ".$rpchash->{cburl}. - " with ID $clkey at ".$rpchash->{clurl}; - if (HMCCURPC_IsAscRPCPort ($rpchash->{port})) { - HMCCURPC_SendRequest ($hash, $rpchash->{port}, "init", $rpchash->{cburl}); - } - else { - HMCCURPC_SendBinRequest ($hash, $rpchash->{port}, "init", $BINRPC_STRING, $rpchash->{cburl}); - } - $rpchash->{port} = 0; - $rpchash->{cburl} = ''; - $rpchash->{clurl} = ''; - $rpchash->{cbport} = 0; - $rpchash->{state} = 'deregistered'; - - Log3 $name, 1, "HMCCURPC: RPC callback for server $clkey deregistered"; - $deregcount++; + if (defined ($rpchash->{port})) { + $deregcount++ if (HMCCURPC_DeRegisterSingleCallback ($hash, $rpchash->{port}, 0)); } } return $deregcount; } +###################################################################### +# Deregister single RPC callback +###################################################################### + +sub HMCCURPC_DeRegisterSingleCallback ($$$) +{ + my ($hash, $port, $force) = @_; + my $name = $hash->{NAME}; + + return 0 if (!exists ($hash->{IODev})); + my $hmccu_hash = $hash->{IODev}; + + my $clkey = 'CB'.$port; + my $localaddr = $hash->{hmccu}{localaddr}; + my $cburl = ''; + my $clurl = ''; + my $rpchash; + + if (exists ($hash->{hmccu}{rpc}{$clkey})) { + $rpchash = \%{$hash->{hmccu}{rpc}{$clkey}}; + return 0 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})); + } + else { + return 0 if ($force == 0); + } + + $cburl = HMCCU_GetRPCCallbackURL ($hash, $localaddr, $rpchash->{cbport}, $clkey, $port) if ($cburl eq ''); + $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url') if ($clurl eq ''); + return 0 if ($cburl eq '' || $clurl eq ''); + + Log3 $name, 1, "HMCCURPC: Deregistering RPC server $cburl with ID $clkey at $clurl"; + if (HMCCU_IsRPCType ($hmccu_hash, $port, 'A')) { + HMCCURPC_SendRequest ($hash, $port, "init", $cburl); + } + else { + HMCCURPC_SendBinRequest ($hash, $port, "init", $BINRPC_STRING, $cburl); + } + + if (defined ($rpchash)) { + $rpchash->{port} = 0; + $rpchash->{cburl} = ''; + $rpchash->{clurl} = ''; + $rpchash->{cbport} = 0; + $rpchash->{state} = 'deregistered'; + } + + Log3 $name, 1, "HMCCURPC: RPC callback for server $clkey deregistered"; + return 1; +} + ###################################################################### # Initialize RPC server for specified CCU port # Return server object or undef on error ###################################################################### -sub HMCCURPC_InitRPCServer ($$$) +sub HMCCURPC_InitRPCServer ($$$$) { - my ($name, $serverport, $callbackport) = @_; + my ($name, $serverport, $callbackport, $prot) = @_; my $clkey = 'CB'.$serverport; my $server; - - if (HMCCURPC_IsBinRPCPort ($serverport)) { + + # Create binary RPC server + if ($prot eq 'B') { $server->{__daemon} = IO::Socket::INET->new (LocalPort => $callbackport, Type => SOCK_STREAM, Reuse => 1, Listen => SOMAXCONN); if (!($server->{__daemon})) { @@ -1247,7 +1289,7 @@ sub HMCCURPC_InitRPCServer ($$$) return $server; } - # Create RPC server + # Create ASCII RPC server $server = RPC::XML::Server->new (port => $callbackport); if (!ref($server)) { Log3 $name, 1, "HMCCURPC: Can't create RPC callback server $clkey on port $callbackport. Port in use?"; @@ -1255,34 +1297,45 @@ sub HMCCURPC_InitRPCServer ($$$) } Log3 $name, 2, "HMCCURPC: Callback server $clkey created. Listening on port $callbackport"; + # Add callbacks + # Signature is: ReturnType ParType1 ... + # ReturnType void = string + # Server parameter is not part of the signature! + # Callback for events + # Parameters: Server, InterfaceId, Address, ValueKey, Value Log3 $name, 4, "HMCCURPC: Adding callback for events for server $clkey"; $server->add_method ( { name=>"event", - signature=> ["string string string string int","string string string string double","string string string string boolean","string string string string i4"], + 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=>\&HMCCURPC_EventCB } ); # Callback for new devices + # Parameters: Server, InterfaceId, DeviceDescriptions[] Log3 $name, 4, "HMCCURPC: Adding callback for new devices for server $clkey"; $server->add_method ( { name=>"newDevices", signature=>["string string array"], - code=>\&HMCCURPC_NewDevicesCB + code=>\&HMCCURPC_NewDevicesCB } ); # Callback for deleted devices + # Parameters: Server, InterfaceId, Addresses[] Log3 $name, 4, "HMCCURPC: Adding callback for deleted devices for server $clkey"; $server->add_method ( { name=>"deleteDevices", signature=>["string string array"], - code=>\&HMCCURPC_DeleteDevicesCB + code=>\&HMCCURPC_DeleteDevicesCB } ); # Callback for modified devices + # Parameters: Server, InterfaceId, Address, Hint Log3 $name, 4, "HMCCURPC: Adding callback for modified devices for server $clkey"; $server->add_method ( { name=>"updateDevice", @@ -1292,6 +1345,7 @@ sub HMCCURPC_InitRPCServer ($$$) ); # Callback for replaced devices + # Parameters: Server, InterfaceId, OldAddress, NewAddress Log3 $name, 4, "HMCCURPC: Adding callback for replaced devices for server $clkey"; $server->add_method ( { name=>"replaceDevice", @@ -1301,15 +1355,17 @@ sub HMCCURPC_InitRPCServer ($$$) ); # Callback for readded devices + # Parameters: Server, InterfaceId, Addresses[] Log3 $name, 4, "HMCCURPC: Adding callback for readded devices for server $clkey"; $server->add_method ( - { name=>"replaceDevice", + { name=>"readdedDevice", signature=>["string string array"], code=>\&HMCCURPC_ReaddDeviceCB } ); # Dummy implementation, always return an empty array + # Parameters: Server, InterfaceId Log3 $name, 4, "HMCCURPC: Adding callback for list devices for server $clkey"; $server->add_method ( { name=>"listDevices", @@ -1333,25 +1389,24 @@ sub HMCCURPC_StartRPCServer ($) my ($hash) = @_; my $name = $hash->{NAME}; - # Search HMCCU device and check for running RPC servers - my $hmccu_hash = $hash->{IODev} if (exists ($hash->{IODev}) && $hash->{noiodev} == 0); - if (defined ($hmccu_hash)) { - my @hm_pids = (); - my @ex_pids = (); - return (0, "RPC server already running for device ".$hmccu_hash->{NAME}) - if (HMCCU_IsRPCServerRunning ($hmccu_hash, \@hm_pids, \@ex_pids)); - } + return (0, "Can't find HMCCU I/O device") if (!exists ($hash->{IODev})); + + my $hmccu_hash = $hash->{IODev}; + my @hm_pids = (); + my @ex_pids = (); + return (0, "RPC server already running for device ".$hmccu_hash->{NAME}) + if (HMCCU_IsRPCServerRunning ($hmccu_hash, \@hm_pids, \@ex_pids)); # Get parameters and attributes my %thrpar; my @rpcports = HMCCURPC_GetRPCPortList ($hash); my $localaddr = HMCCURPC_GetAttribute ($hash, 'rpcServerAddr', 'rpcserveraddr', ''); - my $rpcserverport = HMCCURPC_GetAttribute ($hash, 'rpcServerPort', 'rpcserverport', 5400); + my $rpcserverport = HMCCURPC_GetAttribute ($hash, 'rpcServerPort', 'rpcserverport', $HMCCURPC_SERVER_PORT); + my $evttimeout = HMCCURPC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPC_TIMEOUT_EVENT); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); $thrpar{socktimeout} = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPC_TIMEOUT_WRITE); $thrpar{conntimeout} = AttrVal ($name, 'rpcConnTimeout', $HMCCURPC_TIMEOUT_CONNECTION); $thrpar{acctimeout} = AttrVal ($name, 'rpcAcceptTimeout', $HMCCURPC_TIMEOUT_ACCEPT); - $thrpar{evttimeout} = AttrVal ($name, 'rpcEventTimeout', $HMCCURPC_TIMEOUT_EVENT); $thrpar{waittime} = AttrVal ($name, 'rpcWaitTime', $HMCCURPC_TIME_WAIT); $thrpar{queuesize} = AttrVal ($name, 'rpcQueueSize', $HMCCURPC_MAX_QUEUESIZE); $thrpar{triggertime} = AttrVal ($name, 'rpcTriggerTime', $HMCCURPC_TIME_TRIGGER); @@ -1359,13 +1414,13 @@ sub HMCCURPC_StartRPCServer ($) $thrpar{name} = $name; my $ccunum = $hash->{CCUNum}; - my $serveraddr = $hash->{host}; + my $serveraddr = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcports[0], 'host'); my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); # Get or detect local IP address if ($localaddr eq '') { my $socket = IO::Socket::INET->new (PeerAddr => $serveraddr, PeerPort => $rpcports[0]); - return (0, "Can't connect to CCU port ".$rpcports[0]) if (!$socket); + return (0, "Can't connect to RPC host $serveraddr port ".$rpcports[0]) if (!$socket); $localaddr = $socket->sockhost (); close ($socket); } @@ -1374,7 +1429,7 @@ sub HMCCURPC_StartRPCServer ($) # Create socket pair for communication between data processing thread and FHEM my ($sockchild, $sockparent); return (0, "Can't create I/O socket pair") if (!socketpair ($sockchild, $sockparent, - AF_UNIX, SOCK_STREAM || SOCK_NONBLOCK, PF_UNSPEC)); + AF_UNIX, SOCK_STREAM, PF_UNSPEC)); $sockchild->autoflush (1); $sockparent->autoflush (1); $hash->{hmccu}{sockchild} = $sockchild; @@ -1419,7 +1474,14 @@ sub HMCCURPC_StartRPCServer ($) foreach my $port (@rpcports) { my $clkey = 'CB'.$port; my $callbackport = $rpcserverport+$port+($ccunum*10); - my $interface = $HMCCURPC_RPC_NUMPORT{$port}; + my $interface = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'name'); + my $flags = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'flags'); + + # Additional interface specific thread parameters + $thrpar{interface} = $interface; + $thrpar{flags} = $flags; + $thrpar{type} = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'type'); + $thrpar{evttimeout} = HMCCURPC_GetEventTimeout ($evttimeout, $interface); # Start RPC server thread my $thr = threads->create ('HMCCURPC_HandleConnection', @@ -1433,8 +1495,9 @@ sub HMCCURPC_StartRPCServer ($) $thr->tid (); # Store thread parameters - $hash->{hmccu}{rpc}{$clkey}{type} = HMCCURPC_IsBinRPCPort ($port) ? + $hash->{hmccu}{rpc}{$clkey}{type} = HMCCU_IsRPCType ($hmccu_hash, $port, 'B') ? $HMCCURPC_THREAD_BINARY : $HMCCURPC_THREAD_ASCII; + $hash->{hmccu}{rpc}{$clkey}{flags} = $flags; $hash->{hmccu}{rpc}{$clkey}{child} = $thr; $hash->{hmccu}{rpc}{$clkey}{cbport} = $callbackport; $hash->{hmccu}{rpc}{$clkey}{tid} = $thr->tid (); @@ -1460,9 +1523,9 @@ sub HMCCURPC_StartRPCServer ($) } $hash->{RPCTID} = join (',', @hm_tids); - $hash->{hmccu}{rpcstarttime} = time (); +# $hash->{hmccu}{rpcstarttime} = time (); - # Trigger Timer function for checking successful RPC start + # Trigger timer function for checking successful RPC start # Timer will be removed if event 'IN' is reveived InternalTimer (gettimeofday()+$HMCCURPC_INIT_INTERVAL3*$run, "HMCCURPC_IsRPCServerRunning", $hash, 0); @@ -1473,6 +1536,38 @@ sub HMCCURPC_StartRPCServer ($) return ($run, undef); } +###################################################################### +# Set overall status if all RPC servers are running and update all +# FHEM devices. +# Return (running servers, updated devices, failed updates) +###################################################################### + +sub HMCCURPC_RPCServerStarted ($$) +{ + my ($hash, $hmccu_hash) = @_; + my $name = $hash->{NAME}; + + my $c_ok = 0; + my $c_err = 0; + + # Check if all RPC servers were initialized. Set overall status + my ($run, $all) = HMCCURPC_CheckThreadState ($hash, $HMCCURPC_THREAD_ALL, 'running', undef); + if ($run == $all) { +# $hash->{hmccu}{rpcstarttime} = 0; + HMCCURPC_SetRPCState ($hash, "running", "All RPC servers running"); + HMCCURPC_SetState ($hash, "OK"); + if (defined ($hmccu_hash)) { + HMCCU_SetState ($hmccu_hash, "OK"); + ($c_ok, $c_err) = HMCCU_UpdateClients ($hmccu_hash, '.*', 'Attr', 0, undef); + Log3 $name, 2, "HMCCURPC: Updated devices. Success=$c_ok Failed=$c_err"; + } + RemoveInternalTimer ($hash); + DoTrigger ($name, "RPC server running"); + } + + return ($run, $c_ok, $c_err); +} + ###################################################################### # Stop I/O Handling ###################################################################### @@ -1536,7 +1631,7 @@ sub HMCCURPC_TerminateThreads ($$) ###################################################################### # Cleanup threads in specified state. # Parameter state is a regular expression. -# Return number of deleted threads +# Return number of deleted threads and number of active threads ###################################################################### sub HMCCURPC_CleanupThreads ($$$) @@ -1552,25 +1647,29 @@ sub HMCCURPC_CleanupThreads ($$$) # Check if threads has been stopped my @thrlist = keys %{$hash->{hmccu}{rpc}}; foreach my $clkey (@thrlist) { - next if ($hash->{hmccu}{rpc}{$clkey}{state} eq 'inactive'); + my $tst = $hash->{hmccu}{rpc}{$clkey}{state}; + next if ($tst eq 'inactive'); next if (!($hash->{hmccu}{rpc}{$clkey}{type} & $mode)); $all++; if (exists ($hash->{hmccu}{rpc}{$clkey}{child})) { my $thr = $hash->{hmccu}{rpc}{$clkey}{child}; if (defined ($thr)) { - if ($thr->is_running () || $hash->{hmccu}{rpc}{$clkey}{state} !~ /$state/) { - Log3 $name, 1, "HMCCURPC: Thread $clkey with TID=".$thr->tid(). - " still running. Can't delete it"; + my $tid = $thr->tid(); + if ($thr->is_running () || $tst !~ /$state/) { + Log3 $name, 1, "HMCCURPC: Thread $clkey with TID=$tid still running. Can't delete it"; next; } - if ($hash->{hmccu}{rpc}{$clkey}{state} eq 'stopped' && $ccuflags !~ /keepThreads/) { - Log3 $name, 2, "HMCCURPC: Thread $clkey with TID=".$thr->tid (). - " has been stopped. Deleting it"; - undef $hash->{hmccu}{rpc}{$clkey}{child}; + if ($ccuflags !~ /keepThreads/) { + if ($tst eq 'inactive' || $tst eq 'stopping') { + Log3 $name, 2, "HMCCURPC: Thread $clkey with TID=$tid stopped. Deleting it"; + delete $hash->{hmccu}{rpc}{$clkey}{child}; + } + else { + Log3 $name, 2, "HMCCURPC: Thread $clkey with TID=$tid is in state $tst. Can't delete it"; + } } else { - Log3 $name, 2, "HMCCURPC: Thread $clkey with TID=".$thr->tid (). - " is in state ".$hash->{hmccu}{rpc}{$clkey}{state}.". Can't delete it"; + Log3 $name, 2, "HMCCURPC: Flag keepThreads set. Keeping thread $clkey with TID=$tid"; } # delete $hash->{hmccu}{rpc}{$clkey}; } @@ -1602,20 +1701,20 @@ sub HMCCURPC_CheckThreadState ($$$$) $state = '' if (!defined ($state)); foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { - next if ($hash->{hmccu}{rpc}{$clkey}{state} eq 'inactive'); + my $tst = $hash->{hmccu}{rpc}{$clkey}{state}; + next if ($tst eq 'inactive'); next if (!($hash->{hmccu}{rpc}{$clkey}{type} & $mode)); $all++; if ($state eq 'running' || $state eq '.*') { next if (!exists ($hash->{hmccu}{rpc}{$clkey}{child})); my $thr = $hash->{hmccu}{rpc}{$clkey}{child}; - if (defined ($thr) && $thr->is_running () && - ($state eq '' || $hash->{hmccu}{rpc}{$clkey}{state} =~ /$state/)) { + if (defined ($thr) && $thr->is_running () && ($state eq '' || $tst =~ /$state/)) { $count++; push (@$tids, $thr->tid()) if (defined ($tids)); } } else { - $count++ if ($hash->{hmccu}{rpc}{$clkey}{state} =~ /$state/); + $count++ if ($tst =~ /$state/); } } @@ -1662,7 +1761,7 @@ sub HMCCURPC_Housekeeping ($) HMCCURPC_CleanupThreadIO ($hash); my $count = HMCCURPC_TerminateThreads ($hash, $HMCCURPC_THREAD_ALL); - sleep (2) if ($count > 0); + sleep (1) if ($count > 0); my ($del, $total) = HMCCURPC_CleanupThreads ($hash, $HMCCURPC_THREAD_ALL, '.*'); $count = $total-$del; @@ -1702,7 +1801,7 @@ sub HMCCURPC_StopRPCServer ($) # Give threads the chance to terminate sleep (1); } - elsif ($run == 0 && $hash->{RPCState} ne 'stopped') { + elsif ($run == 0 && $hash->{RPCState} ne 'inactive') { Log3 $name, 2, "HMCCURPC: Found no running threads. Cleaning up ..."; HMCCURPC_CleanupThreadIO ($hash); HMCCURPC_CleanupThreads ($hash, $HMCCURPC_THREAD_ALL, '.*'); @@ -1712,28 +1811,6 @@ sub HMCCURPC_StopRPCServer ($) return 1; } -###################################################################### -# Check if port is valid and an ascii RPC port -###################################################################### - -sub HMCCURPC_IsAscRPCPort ($) -{ - my ($port) = @_; - - return exists ($HMCCURPC_RPC_PROT{$port}) && $HMCCURPC_RPC_PROT{$port} eq 'A' ? 1 : 0; -} - -###################################################################### -# Check if port is valid and a binary RPC port -###################################################################### - -sub HMCCURPC_IsBinRPCPort ($) -{ - my ($port) = @_; - - return exists ($HMCCURPC_RPC_PROT{$port}) && $HMCCURPC_RPC_PROT{$port} eq 'B' ? 1 : 0; -} - ###################################################################### # Send ascii RPC request to CCU # Return response or undef on error. @@ -1743,16 +1820,20 @@ sub HMCCURPC_SendRequest ($@) { my ($hash, $port, $request, @param) = @_; my $name = $hash->{NAME}; - my $serveraddr = $hash->{host}; - - return undef if (!HMCCURPC_IsAscRPCPort ($port)); - - Log3 $name, 4, "HMCCURPC: Send ASCII RPC request $request to $serveraddr:$port"; - my $clurl = "http://$serveraddr:$port/"; - $clurl .= $HMCCURPC_RPC_URL{$port} if (exists ($HMCCURPC_RPC_URL{$port})); + return undef if (!exists ($hash->{IODev})); + return undef if (!HMCCU_IsRPCType ($hash->{IODev}, $port, 'A')); + my $clurl = HMCCU_GetRPCServerInfo ($hash->{IODev}, $port, 'url'); + return undef if (!defined ($clurl)); + + Log3 $name, 4, "HMCCURPC: Send ASCII RPC request $request to $clurl"; + my $rpcclient = RPC::XML::Client->new ($clurl); - return $rpcclient->simple_request ($request, @param); + my $rc = $rpcclient->simple_request ($request, @param); + + Log3 $name, 2, "HMCCURPC: RPC request error ".$RPC::XML::ERROR if (!defined ($rc)); + + return $rc; } ###################################################################### @@ -1765,9 +1846,11 @@ sub HMCCURPC_SendBinRequest ($@) { my ($hash, $port, $request, @param) = @_; my $name = $hash->{NAME}; - my $serveraddr = $hash->{host}; - return undef if (!HMCCURPC_IsBinRPCPort ($port)); + return undef if (!exists ($hash->{IODev})); + return undef if (!HMCCU_IsRPCType ($hash->{IODev}, $port, 'B')); + my $serveraddr = HMCCU_GetRPCServerInfo ($hash->{IODev}, $port, 'host'); + return undef if (!defined ($serveraddr)); my $verbose = GetVerbose ($name); @@ -1807,12 +1890,13 @@ sub HMCCURPC_SendBinRequest ($@) # Process binary RPC request ###################################################################### -sub HMCCURPC_ProcessRequest ($$) +sub HMCCURPC_ProcessRequest ($$$) { - my ($server, $connection) = @_; + my ($server, $connection, $rpcflags) = @_; my $name = $server->{hmccu}{name}; my $clkey = $server->{hmccu}{clkey}; - my @methodlist = ('listDevices', 'listMethods', 'system.multicall'); + my $port = $server->{hmccu}{port}; + my @methodlist = ('listDevices', 'system.listMethods', 'system.multicall'); my $verbose = GetVerbose ($name); # Read request @@ -1830,21 +1914,14 @@ sub HMCCURPC_ProcessRequest ($$) return if (!defined ($method)); Log3 $name, 4, "CCURPC: request method = $method"; - if ($method eq 'listmethods') { + if ($method =~ /listmethods/i) { $connection->send (HMCCURPC_EncodeResponse ($BINRPC_ARRAY, \@methodlist)); } - elsif ($method eq 'listdevices') { + elsif ($method =~ /listdevices/i) { HMCCURPC_ListDevicesCB ($server, $clkey); $connection->send (HMCCURPC_EncodeResponse ($BINRPC_ARRAY, undef)); } elsif ($method eq 'system.multicall') { - # Send INIT to FHEM when we receive the first event from CCU/CUxD because some binary - # RPC clients won't send a ListDevices request - if ($server->{hmccu}{running} == 0) { - $server->{hmccu}{running} = 1; - Log3 $name, 1, "CCURPC: Binary RPC $clkey. Sending init to HMCCU"; - HMCCURPC_Write ($server, "IN", $clkey, "INIT|1"); - } return if (ref ($params) ne 'ARRAY'); my $a = $$params[0]; foreach my $s (@$a) { @@ -1876,6 +1953,9 @@ sub HMCCURPC_HandleConnection ($$$$) my $evttimeout = $thrpar->{evttimeout}; my $conntimeout = $thrpar->{conntimeout}; + my $iface = $thrpar->{interface}; + my $rpcflags = $thrpar->{flags}; + my $prot = $thrpar->{type}; my $run = 1; my $tid = threads->tid (); @@ -1884,10 +1964,8 @@ sub HMCCURPC_HandleConnection ($$$$) my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); # Initialize RPC server - my $iface = $HMCCURPC_RPC_NUMPORT{$port}; - my $prot = $HMCCURPC_RPC_PROT{$port}; - Log3 $name, 2, "CCURPC: Initializing RPC server $clkey for interface $iface"; - my $rpcsrv = HMCCURPC_InitRPCServer ($name, $port, $callbackport); + Log3 $name, 2, "CCURPC: Initializing RPC server $clkey for interface $iface."; + my $rpcsrv = HMCCURPC_InitRPCServer ($name, $port, $callbackport, $prot); if (!defined ($rpcsrv)) { Log3 $name, 1, "CCURPC: Can't initialize RPC server $clkey for interface $iface"; return; @@ -1905,6 +1983,7 @@ sub HMCCURPC_HandleConnection ($$$$) $rpcsrv->{hmccu}{statistics} = $thrpar->{statistics}; $rpcsrv->{hmccu}{running} = 0; $rpcsrv->{hmccu}{evttime} = time (); + $rpcsrv->{hmccu}{port} = $port; # Initialize statistic counters foreach my $et (@eventtypes) { @@ -1917,8 +1996,16 @@ sub HMCCURPC_HandleConnection ($$$$) $SIG{INT} = sub { $run = 0; }; HMCCURPC_Write ($rpcsrv, "SL", $clkey, $tid); - Log3 $name, 2, "CCURPC: $clkey accepting connections. TID=$tid"; - + Log3 $name, 2, "CCURPC: $clkey accepting connections. TID=$tid, EventTimeout=$evttimeout"; + + # Send INIT to FHEM if flag forceInit ist set. Some RPC clients won't send a ListDevice + # request + if ($rpcflags =~ /forceInit/ && $rpcsrv->{hmccu}{running} == 0) { + $rpcsrv->{hmccu}{running} = 1; +# Log3 $name, 1, "CCURPC: RPC $clkey. Forced init to HMCCURPC"; +# HMCCURPC_Write ($rpcsrv, "IN", $clkey, "INIT|1"); + } + $rpcsrv->{__daemon}->timeout ($thrpar->{acctimeout}); while ($run) { @@ -1932,13 +2019,15 @@ sub HMCCURPC_HandleConnection ($$$$) next if (! $connection); last if (! $run); $connection->timeout ($conntimeout); + if ($prot eq 'A') { Log3 $name, 4, "CCURPC: $clkey processing CCU request"; $rpcsrv->process_request ($connection); } else { - HMCCURPC_ProcessRequest ($rpcsrv, $connection); + HMCCURPC_ProcessRequest ($rpcsrv, $connection, $rpcflags); } + shutdown ($connection, 2); close ($connection); undef $connection; @@ -2265,7 +2354,7 @@ sub HMCCURPC_EventCB ($$$$$) my ($server, $cb, $devid, $attr, $val) = @_; my $name = $server->{hmccu}{name}; my $etime = time (); - + HMCCURPC_Write ($server, "EV", $cb, $etime."|".$devid."|".$attr."|".$val); # Never remove this statement! @@ -2281,10 +2370,15 @@ sub HMCCURPC_ListDevicesCB ($$) my ($server, $cb) = @_; my $name = $server->{hmccu}{name}; - $server->{hmccu}{running} = 1; - $cb = "unknown" if (!defined ($cb)); - Log3 $name, 1, "CCURPC: $cb ListDevices. Sending init to HMCCU"; - HMCCURPC_Write ($server, "IN", $cb, "INIT|1"); + if ($server->{hmccu}{running} == 0) { + $server->{hmccu}{running} = 1; + $cb = "unknown" if (!defined ($cb)); + Log3 $name, 1, "CCURPC: $cb ListDevices. Sending init to HMCCU"; + HMCCURPC_Write ($server, "IN", $cb, "INIT|1"); + } + else { + Log3 $name, 1, "CCURPC: $cb ListDevices ignored. Server already running."; + } return RPC::XML::array->new (); } @@ -2767,23 +2861,24 @@ sub HMCCURPC_DecodeResponse ($) Define

        - define <name> HMCCURPC {<HostOrIP>|iodev=<DeviceName>|standalone=< - HostOrIP>} + define <name> HMCCURPC {<HostOrIP>|iodev=<DeviceName>}

        Examples:
        define myccurpc HMCCURPC 192.168.1.10
        define myccurpc HMCCURPC iodev=myccudev
        - define myccurpc HMCCURPC standalone=192.168.1.10

        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 option standalone is - specified RPC servers will operate without I/O device (for development purposes). + The I/O device can also be specified with parameter iodev.

      Set

        +
      • set <name> deregister <interface>
        + Deregister RPC server for interface. Parameter interface is a valid + CCU interface name (i.e. BidCos-RF). +

      • set <name> rpcrequest <port> <method> [<parameters>]
        Send RPC request to CCU. The result is displayed in FHEM browser window. Parameter <port> is a valid RPC port (i.e. 2001 for BidCos). @@ -2820,13 +2915,15 @@ sub HMCCURPC_DecodeResponse ($)
      • rpcConnTimeout <seconds>
        Specify timeout of CCU connection handling. Default is 10 second.

      • -
      • rpcEventTimeout <seconds>
        +
      • rpcEventTimeout {<seconds>|<interface:seconds>}[,...]
        Specify timeout for CCU events. Default is 600 seconds. If timeout occurs an event - is triggered. If set to 0 the timeout is ignored. + is triggered. If set to 0 the timeout is ignored. If no interface is specified + timeout is applied to all interfaces. For valid values for interface see + attribute rpcInterfaces.

      • -
      • rpcInterfaces { BidCos-Wired, BidCos-RF, HmIP-RF, VirtualDevices, CUxD, Homegear }
        +
      • rpcInterfaces { BidCos-Wired, BidCos-RF, HmIP-RF, VirtualDevices, CUxD, Homegear, HVL }
        Select RPC interfaces. If attribute is missing the corresponding attribute of I/O device - (HMCCU device) is used. Default is BidCos-RF. + (HMCCU device) is used. Interface BidCos-RF is default and always active.

      • rpcMaxEvents <count>
        Specify maximum number of events read by FHEM during one I/O loop. If FHEM performance diff --git a/fhem/FHEM/88_HMCCURPCPROC.pm b/fhem/FHEM/88_HMCCURPCPROC.pm new file mode 100755 index 000000000..73c02bf02 --- /dev/null +++ b/fhem/FHEM/88_HMCCURPCPROC.pm @@ -0,0 +1,2619 @@ +############################################################################## +# +# 88_HMCCURPCPROC.pm +# +# $Id$ +# +# Version 1.0 +# +# Subprocess based RPC Server module for HMCCU. +# +# (c) 2018 by zap (zap01 t-online de) +# +############################################################################## +# +# Required perl modules: +# +# RPC::XML::Client +# RPC::XML::Server +# +############################################################################## + + +package main; + +use strict; +use warnings; + +use RPC::XML::Client; +use RPC::XML::Server; +use SetExtensions; + + +###################################################################### +# Constants +###################################################################### + +# HMCCURPC version +my $HMCCURPCPROC_VERSION = '1.0'; + +# 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; + +# Timeout for established CCU connection +my $HMCCURPCPROC_TIMEOUT_CONNECTION = 1; + +# Timeout for TriggerIO() +my $HMCCURPCPROC_TIMEOUT_WRITE = 0.001; + +# Timeout for accepting incoming connections (0 = default) +my $HMCCURPCPROC_TIMEOUT_ACCEPT = 1; + +# Timeout for incoming CCU events +my $HMCCURPCPROC_TIMEOUT_EVENT = 600; + +# Send statistic information after specified amount of events +my $HMCCURPCPROC_STATISTICS = 500; + +# Default RPC Port = BidCos-RF +my $HMCCURPCPROC_RPC_PORT_DEFAULT = 2001; + +# Default RPC server base port +my $HMCCURPCPROC_SERVER_PORT = 5400; + +# Delay for RPC server start after FHEM is initialized +my $HMCCURPCPROC_INIT_INTERVAL0 = 12; + +# Delay for RPC server cleanup after top +my $HMCCURPCPROC_INIT_INTERVAL2 = 30; + +# Delay for RPC server functionality check after start +my $HMCCURPCPROC_INIT_INTERVAL3 = 25; + +# 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; + +# Message types +my $BINRPC_REQUEST = 0x42696E00; +my $BINRPC_RESPONSE = 0x42696E01; +my $BINRPC_REQUEST_HEADER = 0x42696E40; +my $BINRPC_ERROR = 0x42696EFF; + + +###################################################################### +# Functions +###################################################################### + +# Standard functions +sub HMCCURPCPROC_Initialize ($); +sub HMCCURPCPROC_Define ($$); +sub HMCCURPCPROC_Undef ($$); +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 server functions +sub HMCCURPCPROC_RegisterCallback ($$); +sub HMCCURPCPROC_DeRegisterCallback ($$); +sub HMCCURPCPROC_InitRPCServer ($$$$); +sub HMCCURPCPROC_StartRPCServer ($); +sub HMCCURPCPROC_RPCServerStarted ($); +sub HMCCURPCPROC_RPCServerStopped ($); +sub HMCCURPCPROC_CleanupProcess ($); +sub HMCCURPCPROC_CleanupIO ($); +sub HMCCURPCPROC_TerminateProcess ($); +sub HMCCURPCPROC_CheckProcessState ($$); +sub HMCCURPCPROC_IsRPCServerRunning ($); +sub HMCCURPCPROC_Housekeeping ($); +sub HMCCURPCPROC_StopRPCServer ($); +sub HMCCURPCPROC_SendRequest ($@); +sub HMCCURPCPROC_SetRPCState ($$$$); +sub HMCCURPCPROC_ResetRPCState ($); +sub HMCCURPCPROC_IsRPCStateBlocking ($); + +# Helper functions +sub HMCCURPCPROC_GetAttribute ($$$$); +sub HMCCURPCPROC_HexDump ($$); + +# RPC server functions +sub HMCCURPCPROC_ProcessRequest ($$); +sub HMCCURPCPROC_HandleConnection ($$$$); +sub HMCCURPCPROC_SendQueue ($$$$); +sub HMCCURPCPROC_SendData ($$); +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 ($$); + +# Binary RPC encoding functions +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->{DefFn} = "HMCCURPCPROC_Define"; + $hash->{UndefFn} = "HMCCURPCPROC_Undef"; + $hash->{SetFn} = "HMCCURPCPROC_Set"; + $hash->{GetFn} = "HMCCURPCPROC_Get"; + $hash->{ReadFn} = "HMCCURPCPROC_Read"; + $hash->{AttrFn} = "HMCCURPCPROC_Attr"; + $hash->{ShutdownFn} = "HMCCURPCPROC_Shutdown"; + + $hash->{parseParams} = 1; + + $hash->{AttrList} = "ccuflags:multiple-strict,expert,reconnect,logEvents,ccuInit,queueEvents". + " rpcMaxEvents rpcQueueSend rpcQueueSize rpcMaxIOErrors". + " rpcServerAddr rpcServerPort rpcWriteTimeout rpcAcceptTimeout". + " rpcConnTimeout rpcStatistics rpcEventTimeout ". + $readingFnAttributes; +} + +###################################################################### +# Define device +###################################################################### + +sub HMCCURPCPROC_Define ($$) +{ + my ($hash, $a, $h) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash; + my $iface; + my $usage = "Usage: define $name HMCCURPCPROC { CCUHost | iodev=Name } { RPCPort | RPCInterface }"; + + if (exists ($h->{iodev})) { + my $ioname = $h->{iodev}; + return $usage if (scalar (@$a) < 3); + return "HMCCU I/O device $ioname not found" if (!exists ($defs{$ioname})); + return "Device $ioname is no HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); + $hmccu_hash = $defs{$ioname}; + $hash->{host} = $hmccu_hash->{host}; + $iface = $$a[2]; + } + else { + return $usage if (scalar (@$a) < 4); + $hash->{host} = $$a[2]; + $iface = $$a[3]; + } + + # Find IO device + for my $d (keys %defs) { + my $dh = $defs{$d}; + next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME})); + if ($dh->{TYPE} eq 'HMCCU' && $dh->{host} eq $hash->{host}) { + $hmccu_hash = $dh; + last; + } + } + return "Can't find HMCCU I/O device" if (!defined ($hmccu_hash)); + + # Check if interface is valid + my $ifname = HMCCU_GetRPCServerInfo ($hmccu_hash, $iface, 'name'); + my $ifport = HMCCU_GetRPCServerInfo ($hmccu_hash, $iface, 'port'); + return "Invalid port or interface $iface" if (!defined ($ifname) || !defined ($ifport)); + + # Check if RPC device with same interface already exists + for 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) { + return "RPC device for CCU/port already exists" + if ($hash->{host} eq $dh->{host} && $dh->{rpcport} == $ifport); + } + } + + # Set I/O device and store reference for RPC device in I/O device + AssignIoPort ($hash, $hmccu_hash->{NAME}); + $hmccu_hash->{hmccu}{interfaces}{$ifname}{device} = $name; + + # Store internals + $hash->{rpcport} = $ifport; + $hash->{rpcinterface} = $ifname; + $hash->{ccuip} = $hmccu_hash->{ccuip}; + $hash->{ccutype} = $hmccu_hash->{ccutype}; + $hash->{CCUNum} = $hmccu_hash->{CCUNum}; + $hash->{ccustate} = $hmccu_hash->{ccustate}; + $hash->{version} = $HMCCURPCPROC_VERSION; + + Log3 $name, 1, "HMCCURPCPROC: [$name] Initialized version $HMCCURPCPROC_VERSION for interface $ifname"; + + # Set some attributes + $attr{$name}{stateFormat} = "rpcstate/state"; + $attr{$name}{verbose} = 2; + + HMCCURPCPROC_ResetRPCState ($hash); + + return undef; +} + +###################################################################### +# Delete device +###################################################################### + +sub HMCCURPCPROC_Undef ($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash = $hash->{IODev}; + my $ifname = $hash->{rpcinterface}; + + # Shutdown RPC server + HMCCURPCPROC_Shutdown ($hash); + + # Delete RPC device name in I/O device + if (exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{device}) && + $hmccu_hash->{hmccu}{interfaces}{$ifname}{device} eq $name) { + delete $hmccu_hash->{hmccu}{interfaces}{$ifname}{device}; + } + + return undef; +} + +###################################################################### +# Shutdown FHEM +###################################################################### + +sub HMCCURPCPROC_Shutdown ($) +{ + my ($hash) = @_; + + # Shutdown RPC server + HMCCURPCPROC_StopRPCServer ($hash); + RemoveInternalTimer ($hash); + + return undef; +} + +###################################################################### +# Set attribute +###################################################################### + +sub HMCCURPCPROC_Attr ($@) +{ + my ($cmd, $name, $attrname, $attrval) = @_; + my $hash = $defs{$name}; + + if ($cmd eq 'set') { + if (($attrname eq 'rpcAcceptTimeout' || $attrname eq 'rpcMaxEvents') && $attrval == 0) { + return "HMCCURPCPROC: [$name] Value for attribute $attrname must be greater than 0"; + } + } + + return undef; +} + +###################################################################### +# Set commands +###################################################################### + +sub HMCCURPCPROC_Set ($@) +{ + my ($hash, $a, $h) = @_; + my $hmccu_hash = $hash->{IODev}; + my $name = shift @$a; + my $opt = shift @$a; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $options = $ccuflags =~ /expert/ ? "cleanup:noArg deregister: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 '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} [{parameter} ...]", 2) + if (!defined ($request)); + + my $response = HMCCURPCPROC_SendRequest ($hash, $request, @$a); + return HMCCURPCPROC_SetError ($hash, "RPC request failed", 2) if (!defined ($response)); + return HMCCU_RefToString ($response); + } + 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'); + $hmccu_hash->{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') { + $hmccu_hash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; + HMCCURPCPROC_StopRPCServer ($hash); + } + + return undef; + } + else { + return "HMCCURPCPROC: Unknown argument $opt, choose one of ".$options; + } +} + +###################################################################### +# Get commands +###################################################################### + +sub HMCCURPCPROC_Get ($@) +{ + my ($hash, $a, $h) = @_; + my $name = shift @$a; + my $opt = shift @$a; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $options = "rpcevents:noArg rpcstate: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 'rpcevents') { + my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); + my $clkey = 'CB'.$hash->{rpcport}; + $result = "Event statistics for server $clkey\n"; + $result .= "Average event delay = ".$hash->{hmccu}{rpc}{avgdelay}."\n" + if (defined ($hash->{hmccu}{rpc}{avgdelay})); + $result .= "========================================\n"; + $result .= "ET Sent by RPC server Received by FHEM\n"; + $result .= "----------------------------------------\n"; + foreach my $et (@eventtypes) { + 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"; + } + return $result eq '' ? "No event statistics found" : $result; + } + elsif ($opt eq 'rpcstate') { + my $clkey = 'CB'.$hash->{rpcport}; + $result = "ID RPC-Process State \n"; + $result .= "-----------------------\n"; + my $sid = defined ($hash->{hmccu}{rpc}{pid}) ? sprintf ("%2d", $hash->{hmccu}{rpc}{pid}) : "N/A"; + my $sname = sprintf ("%-6s", $clkey); + $result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n"; + return $result; + } + else { + return "HMCCURPCPROC: Unknown argument $opt, choose one of ".$options; + } +} + +###################################################################### +# Read data from processes +###################################################################### + +sub HMCCURPCPROC_Read ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash = $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 = (); + + Log3 $name, 4, "HMCCURPCPROC: [$name] Read called"; + + # Check if child socket exists + if (!defined ($hash->{hmccu}{sockchild})) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Child socket does not exist"; + return; + } + + # Get attributes + my $rpcmaxevents = AttrVal ($name, 'rpcMaxEvents', $HMCCURPCPROC_MAX_EVENTS); + my $ccuflags = AttrVal ($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)) { + Log3 $name, 4, "HMCCURPCPROC: [$name] Read stopped after $eventcount events $err"; + last; + } + + Log3 $name, 4, "HMCCURPCPROC: [$name] 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]}{$par[2]} = $par[3]; + $evcount++; + $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); + } + elsif ($et eq 'EX') { + # I/O already cleaned up. Leave Read() + last; + } + elsif ($et eq 'ND') { + $devices{$par[0]}{flag} = 'N'; + $devices{$par[0]}{version} = $par[3]; + 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]; + } + else { + $devices{$par[0]}{addtype} = 'chn'; + $devices{$par[0]}{usetype} = $par[2]; + } + $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) { + Log3 $name, 4, "HMCCURPCPROC: [$name] Read stopped after $rpcmaxevents events"; + last; + } + } + + # Update device table and client device readings + HMCCU_UpdateDeviceTable ($hmccu_hash, \%devices) if ($devcount > 0); + HMCCU_UpdateMultipleDevices ($hmccu_hash, \%events) if ($evcount > 0); + + Log3 $name, 4, "HMCCURPCPROC: [$name] Read finished"; +} + +###################################################################### +# 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 $name = $hash->{NAME}; + my $type = $hash->{TYPE}; + my $msg; + + $msg = defined ($text) ? $text : "unknown error"; + $msg = $type.": [".$name."] ". $msg; + + HMCCURPCPROC_SetState ($hash, "error"); + Log3 $name, (defined($level) ? $level : 1), $msg; + + return $msg; +} + +###################################################################### +# Set state of device +###################################################################### + +sub HMCCURPCPROC_SetState ($$) +{ + my ($hash, $state) = @_; + my $name = $hash->{NAME}; + + if (defined ($state)) { + readingsSingleUpdate ($hash, "state", $state, 1); + Log3 $name, 4, "HMCCURPCPROC: [$name] 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 $hmccu_hash = $hash->{IODev}; + + $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'); + + Log3 $name, (defined($level) ? $level : 1), "HMCCURPCPROC: [$name] $msg" if (defined ($msg)); + Log3 $name, 4, "HMCCURPCPROC: [$name] Set rpcstate to $state"; + + # Set state of interface in I/O device + HMCCU_SetRPCState ($hmccu_hash, $state, $hash->{rpcinterface}); + + return undef; +} + +###################################################################### +# Reset RPC State +###################################################################### + +sub HMCCURPCPROC_ResetRPCState ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, "HMCCURPCPROC: [$name] Reset RPC state"; + + $hash->{RPCPID} = "0"; + $hash->{hmccu}{rpc}{pid} = 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 ($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}; + my $rh = \%{$hash->{hmccu}{rpc}}; # Just for code simplification + my $hmccu_hash = $hash->{IODev}; + + # Number of arguments in RPC events (without event type and clkey) + my %rpceventargs = ( + "EV", 4, + "ND", 6, + "DD", 1, + "RD", 2, + "RA", 1, + "UD", 2, + "IN", 2, + "EX", 2, + "SL", 1, + "TO", 1, + "ST", 11 + ); + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $evttimeout = HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', + $HMCCURPCPROC_TIMEOUT_EVENT); + + # Parse event + return undef if (!defined ($event) || $event eq ''); + my @t = split (/\|/, $event); + my $et = shift @t; + my $clkey = shift @t; + my $tc = scalar (@t); + + # Log event + Log3 $name, 2, "HMCCURPCPROC: [$name] CCUEvent = $event" if ($ccuflags =~ /logEvents/); + + # Check event data + if (!defined ($clkey)) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Syntax error in RPC event data"; + return undef; + } + + # Check for valid server + if ($clkey ne $rpcname) { + Log3 $name, 0, "HMCCURPCPROC: [$name] Received SL 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; + Log3 $name, 2, "HMCCURPCPROC: [$name] Received unknown event from CCU: ".$et; + return undef; + } + + # Check event parameters + if ($tc != $rpceventargs{$et}) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Wrong number of 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'); + Log3 $name, 2, "HMCCURPCPROC: [$name] Received CENTRAL event. ".$t[2]."=".$t[3] if ($t[1] eq 'CENTRAL'); + my ($add, $chn) = split (/:/, $t[1]); + return defined ($chn) ? ($et, $clkey, $add, $chn, $t[2], $t[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 { + Log3 $name, 0, "HMCCURPCPROC: [$name] 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 + # Output: ND, clkey, DevAdd, C/D, Type, Version, Firmware, RxMode + # + return ($et, $clkey, $t[1], $t[0], $t[2], $t[3], $t[4], $t[5]); + } + 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], $t[1]); + } + elsif ($et eq 'RD') { + # + # CCU device replaced + # Input: RD|clkey|Address1|Address2 + # Output: RD, clkey, Address1, Address2 + # + return ($et, $clkey, $t[0], $t[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; + my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); + for (my $i=0; $i{hmccu}{rpc}{snd}{$eventtypes[$i]} += $t[$i]; + } + return @res; + } + elsif ($et eq 'TO') { + # + # Event timeout + # Input: TO|clkey|Time + # Output: TO, clkey, Port, Time + # + if ($evttimeout > 0) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Received no events from interface $clkey for ".$t[0]." seconds"; + $hash->{ccustate} = 'timeout'; + if ($hash->{RPCState} eq 'running' && $ccuflags =~ /reconnect/) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Reconnecting to CCU interface ".$hash->{rpcinterface}; + my ($rc, $rcmsg) = HMCCURPCPROC_RegisterCallback ($hash, 2); + if ($rc) { + $hash->{ccustate} = 'active'; + } + else { + Log3 $name, 1, "HMCCURPCPROC: [$name] $rcmsg"; + } + } + 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 $hmccu_hash = $hash->{IODev}; + + my $value = AttrVal ($name, $attr, 'null'); + return $value if ($value ne 'null'); + + $value = AttrVal ($hmccu_hash->{NAME}, $ioattr, 'null'); + return $value if ($value ne 'null'); + + return $default; +} + +###################################################################### +# 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 $hmccu_hash = $hash->{IODev}; + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + + my $port = $hash->{rpcport}; + my $serveraddr = $hash->{host}; + my $localaddr = $hash->{hmccu}{localaddr}; + my $clkey = 'CB'.$port; + + return (0, "RPC server $clkey not in state working") + if ($hash->{hmccu}{rpc}{state} ne 'working' && $force == 0); + + if ($force == 2) { + return (0, "CCU port $port not reachable") if (!HMCCU_TCPConnect ($hash->{host}, $port)); + } + + my $cburl = HMCCU_GetRPCCallbackURL ($hmccu_hash, $localaddr, $hash->{hmccu}{rpc}{cbport}, $clkey, $port); + my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url'); + my $rpctype = HMCCU_GetRPCServerInfo ($hmccu_hash, $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; + + Log3 $name, 2, "HMCCURPCPROC: [$name] Registering callback $cburl of type $rpctype with ID $clkey at $clurl"; + my $rc; + if ($rpctype eq 'A') { + $rc = HMCCURPCPROC_SendRequest ($hash, "init", $cburl, $clkey); + } + else { + $rc = HMCCURPCPROC_SendRequest ($hash, "init", $BINRPC_STRING, $cburl, $BINRPC_STRING, $clkey); + } + + if (defined ($rc)) { + return (1, $ccuflags !~ /ccuInit/ ? 'running' : 'registered'); + } + else { + return (0, "Failed to register callback for ID $clkey"); + } +} + +###################################################################### +# Deregister RPC callbacks at CCU +###################################################################### + +sub HMCCURPCPROC_DeRegisterCallback ($$) +{ + my ($hash, $force) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash = $hash->{IODev}; + + my $port = $hash->{rpcport}; + my $clkey = 'CB'.$port; + 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 ($hmccu_hash, $localaddr, $rpchash->{cbport}, $clkey, $port) if ($cburl eq ''); + $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url') if ($clurl eq ''); + return (0, "Can't get RPC parameters for ID $clkey") if ($cburl eq '' || $clurl eq ''); + + Log3 $name, 1, "HMCCURPCPROC: [$name] Deregistering RPC server $cburl with ID $clkey at $clurl"; + my $rc; + if (HMCCU_IsRPCType ($hmccu_hash, $port, 'A')) { + $rc = HMCCURPCPROC_SendRequest ($hash, "init", $cburl); + } + else { + $rc = HMCCURPCPROC_SendRequest ($hash, "init", $BINRPC_STRING, $cburl); + } + + if (defined ($rc)) { + 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'); + } + else { + return (0, "Failed to deregister RPC server $clkey"); + } +} + +###################################################################### +# Initialize RPC server for specified CCU port +# Return server object or undef on error +###################################################################### + +sub HMCCURPCPROC_InitRPCServer ($$$$) +{ + my ($name, $serverport, $callbackport, $prot) = @_; + my $clkey = 'CB'.$serverport; + my $server; + + # Create binary RPC server + if ($prot eq 'B') { + $server->{__daemon} = IO::Socket::INET->new (LocalPort => $callbackport, + Type => SOCK_STREAM, Reuse => 1, Listen => SOMAXCONN); + if (!($server->{__daemon})) { + Log3 $name, 1, "HMCCURPCPROC: [$name] Can't create RPC callback server $clkey on port $callbackport. Port in use?"; + return undef; + } + return $server; + } + + # Create XML RPC server + $server = RPC::XML::Server->new (port => $callbackport); + if (!ref($server)) { + Log3 $name, 1, "HMCCURPCPROC: [$name] Can't create RPC callback server $clkey on port $callbackport. Port in use?"; + return undef; + } + Log3 $name, 2, "HMCCURPCPROC: [$name] Callback server $clkey created. Listening on port $callbackport"; + + # Callback for events + Log3 $name, 4, "HMCCURPCPROC: [$name] 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 + Log3 $name, 4, "HMCCURPCPROC: [$name] Adding callback for new devices for server $clkey"; + $server->add_method ( + { name=>"newDevices", + signature=>["string string array"], + code=>\&HMCCURPCPROC_NewDevicesCB + } + ); + + # Callback for deleted devices + Log3 $name, 4, "HMCCURPCPROC: [$name] Adding callback for deleted devices for server $clkey"; + $server->add_method ( + { name=>"deleteDevices", + signature=>["string string array"], + code=>\&HMCCURPCPROC_DeleteDevicesCB + } + ); + + # Callback for modified devices + Log3 $name, 4, "HMCCURPCPROC: [$name] Adding callback for modified devices for server $clkey"; + $server->add_method ( + { name=>"updateDevice", + signature=>["string string string int"], + code=>\&HMCCURPCPROC_UpdateDeviceCB + } + ); + + # Callback for replaced devices + Log3 $name, 4, "HMCCURPCPROC: [$name] 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 + Log3 $name, 4, "HMCCURPCPROC: [$name] Adding callback for readded devices for server $clkey"; + $server->add_method ( + { name=>"replaceDevice", + signature=>["string string array"], + code=>\&HMCCURPCPROC_ReaddDeviceCB + } + ); + + # Dummy implementation, always return an empty array + Log3 $name, 4, "HMCCURPCPROC: [$name] Adding callback for list devices for server $clkey"; + $server->add_method ( + { name=>"listDevices", + signature=>["array string"], + code=>\&HMCCURPCPROC_ListDevicesCB + } + ); + + return $server; +} + +###################################################################### +# Start RPC server processes +# 1 process for processing event data in event queue +# 1 process per CCU RPC interface for receiving data +# Return (State, Msg) +###################################################################### + +sub HMCCURPCPROC_StartRPCServer ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash = $hash->{IODev}; + + # Check if RPC server is already running + return (0, "RPC server already running") if (HMCCURPCPROC_CheckProcessState ($hash, 'running')); + + # Get parameters and attributes + my %procpar; + my $localaddr = HMCCURPCPROC_GetAttribute ($hash, 'rpcServerAddr', 'rpcserveraddr', ''); + my $rpcserverport = HMCCURPCPROC_GetAttribute ($hash, 'rpcServerPort', 'rpcserverport', $HMCCURPCPROC_SERVER_PORT); + my $evttimeout = HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); + my $ccunum = $hash->{CCUNum}; + my $rpcport = $hash->{rpcport}; + my $serveraddr = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'host'); + my $interface = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'name'); + + # Store parameters for child process + $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{evttimeout} = AttrVal ($name, 'rpcEventTimeout', $HMCCURPCPROC_TIMEOUT_EVENT); + $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{evttimeout} = AttrVal ($name, 'rpcEventTimeout', $HMCCURPCPROC_TIMEOUT_EVENT); + $procpar{ccuflags} = AttrVal ($name, 'ccuflags', 'null'); + $procpar{interface} = $interface; + $procpar{flags} = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'flags'); + $procpar{type} = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'type'); + $procpar{name} = $name; + + my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); + + # Detect local IP address + if ($localaddr eq '') { + my $socket = IO::Socket::INET->new (PeerAddr => $serveraddr, PeerPort => $rpcport); + return (0, "Can't connect to CCU port $rpcport") if (!$socket); + $localaddr = $socket->sockhost (); + close ($socket); + } + $hash->{hmccu}{localaddr} = $localaddr; + + # 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 + my $pid = $$; + $hash->{FD} = fileno $sockchild; + $selectlist{"RPC.$name.$pid"} = $hash; + + # Initialize RPC server + my $err = ''; + my %srvprocpar; + my $clkey = 'CB'.$rpcport; + my $callbackport = $rpcserverport+$rpcport+($ccunum*10); + + # 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); + # Exit child process + close ($sockparent); + close ($sockchild); + exit (0); + } + + # Parent process + Log3 $name, 2, "HMCCURPCPROC: [$name] RPC server process started for interface $interface with PID=$rpcpid"; + + # Store process parameters + $hash->{hmccu}{rpc}{cbport} = $callbackport; + $hash->{hmccu}{rpc}{pid} = $rpcpid; + $hash->{hmccu}{rpc}{state} = 'initialized'; + + # Reset statistic counter + foreach my $et (@eventtypes) { + $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 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 $hmccu_hash = $hash->{IODev}; + my $clkey = 'CB'.$hash->{rpcport}; + my $ifname = $hash->{rpcinterface}; + + # Check if RPC servers are running. Set overall status + if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { + $hash->{hmccu}{rpcstarttime} = time (); + HMCCURPCPROC_SetState ($hash, "OK"); + + if ($hmccu_hash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { + my ($c_ok, $c_err) = HMCCU_UpdateClients ($hmccu_hash, '.*', 'Attr', 0, $ifname); + Log3 $name, 2, "HMCCURPCPROC: [$name] Updated devices. Success=$c_ok Failed=$c_err"; + } + + RemoveInternalTimer ($hash); + 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 = 'CB'.$hash->{rpcport}; + + HMCCURPCPROC_CleanupProcess ($hash); + HMCCURPCPROC_CleanupIO ($hash); + + HMCCURPCPROC_ResetRPCState ($hash); + HMCCURPCPROC_SetState ($hash, "OK"); + + RemoveInternalTimer ($hash); + DoTrigger ($name, "RPC server $clkey stopped"); +} + +###################################################################### +# Stop I/O Handling +###################################################################### + +sub HMCCURPCPROC_CleanupIO ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $pid = $$; + if (exists ($selectlist{"RPC.$name.$pid"})) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Stop I/O handling"; + delete $selectlist{"RPC.$name.$pid"}; + delete $hash->{FD} if (defined ($hash->{FD})); + } + if (defined ($hash->{hmccu}{sockchild})) { + Log3 $name, 2, "HMCCURPCPROC: [$name] Close child socket"; + $hash->{hmccu}{sockchild}->close (); + delete $hash->{hmccu}{sockchild}; + } + if (defined ($hash->{hmccu}{sockparent})) { + Log3 $name, 2, "HMCCURPCPROC: [$name] 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 $name = $hash->{NAME}; + my $clkey = 'CB'.$hash->{rpcport}; + +# return 0 if ($hash->{hmccu}{rpc}{state} eq 'inactive'); + + 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 $name = $hash->{NAME}; + my $clkey = 'CB'.$hash->{rpcport}; + +# return 1 if ($hash->{hmccu}{rpc}{state} eq 'inactive'); + + my $pid = $hash->{hmccu}{rpc}{pid}; + if (defined ($pid) && kill (0, $pid)) { + Log3 $name, 1, "HMCCURPCPROC: [$name] Process $clkey with PID=$pid". + " still running. Killing it."; + kill ('KILL', $pid); + sleep (1); + if (kill (0, $pid)) { + Log3 $name, 1, "HMCCURPCPROC: [$name] Can't kill process $clkey with PID=$pid"; + return 0; + } + } + + 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 $prcname = 'CB'.$hash->{rpcport}; + + 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, $cleanup) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 2, "HMCCURPCPROC: [$name] Checking if RPC server process is running"; + if (!HMCCURPCPROC_CheckProcessState ($hash, 'running')) { + Log3 $name, 1, "HMCCURPCPROC: [$name] RPC server process not running. Cleaning up"; + HMCCURPCPROC_Housekeeping ($hash); + return 0; + } + + Log3 $name, 2, "HMCCURPCPROC: [$name] RPC server process running"; + return 1; +} + +###################################################################### +# Cleanup RPC server environment. +###################################################################### + +sub HMCCURPCPROC_Housekeeping ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 1, "HMCCURPCPROC: [$name] 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. +###################################################################### + +sub HMCCURPCPROC_StopRPCServer ($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $clkey = 'CB'.$hash->{rpcport}; + + if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { + Log3 $name, 1, "HMCCURPCPROC: [$name] Stopping RPC server $clkey"; + HMCCURPCPROC_SetState ($hash, "busy"); + + # Deregister callback URLs in CCU + my ($rc, $err) = HMCCURPCPROC_DeRegisterCallback ($hash, 0); + Log3 $name, 1, "HMCCURPCPROC: [$name] $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 + InternalTimer (gettimeofday()+$HMCCURPCPROC_INIT_INTERVAL2, "HMCCURPCPROC_Housekeeping", + $hash, 0); + + # Give process the chance to terminate + sleep (1); + return 1; + } + else { + Log3 $name, 2, "HMCCURPCPROC: [$name] Found no running processes. Cleaning up ..."; + HMCCURPCPROC_Housekeeping ($hash); + return 0; + } +} + +###################################################################### +# Send RPC request to CCU. +# Return response or undef on error. +###################################################################### + +sub HMCCURPCPROC_SendRequest ($@) +{ + my ($hash, $request, @param) = @_; + my $name = $hash->{NAME}; + my $hmccu_hash = $hash->{IODev}; + my $port = $hash->{rpcport}; + + my $rc; + + if (HMCCU_IsRPCType ($hmccu_hash, $port, 'A')) { + my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url'); + return HMCCU_Log ($hash, 2, "Can't get client URL for port $port", undef) + if (!defined ($clurl)); + + Log3 $name, 4, "HMCCURPCPROC: [$name] Send ASCII RPC request $request to $clurl"; + my $rpcclient = RPC::XML::Client->new ($clurl); + $rc = $rpcclient->simple_request ($request, @param); + Log3 $name, 2, "HMCCURPCPROC: [$name] RPC request error ".$RPC::XML::ERROR if (!defined ($rc)); + } + elsif (HMCCU_IsRPCType ($hmccu_hash, $port, 'B')) { + my $serveraddr = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'host'); + return HMCCU_Log ($hash, 2, "Can't get server address for port $port", undef) + if (!defined ($serveraddr)); + + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $verbose = GetVerbose ($name); + + Log3 $name, 4, "HMCCURPCPROC: [$name] Send binary RPC request $request to $serveraddr:$port"; + my $encreq = HMCCURPCPROC_EncodeRequest ($request, \@param); + return HMCCU_Log ($hash, 2, "Error encoding binary request", undef) if ($encreq eq ''); + + # auto-flush on socket + $| = 1; + + # create a connecting socket + my $socket = new IO::Socket::INET (PeerHost => $serveraddr, PeerPort => $port, + Proto => 'tcp'); + return HMCCU_Log ($hash, 2, "Can't create socket for $serveraddr:$port", undef) if (!$socket); + + my $size = $socket->send ($encreq); + if (defined ($size)) { + my $encresp = <$socket>; + $socket->close (); + + if (defined ($encresp)) { + if ($ccuflags =~ /logEvents/ && $verbose >= 4) { + Log3 $name, 4, "HMCCURPCPROC: [$name] Response"; + HMCCURPCPROC_HexDump ($name, $encresp); + } + my ($response, $err) = HMCCURPCPROC_DecodeResponse ($encresp); + return $response; + } + else { + return ''; + } + } + + $socket->close (); + } + else { + Log3 $name, 2, "HMCCURPCPROC: [$name] Unknown RPC server type"; + } + + return $rc; +} + +###################################################################### +# 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.multicall'); + my $verbose = GetVerbose ($name); + + # Read request + my $request = ''; + while (my $packet = <$connection>) { + $request .= $packet; + } + return if (!defined ($request) || $request eq ''); + + if ($server->{hmccu}{ccuflags} =~ /logEvents/ && $verbose >= 4) { + Log3 $name, 4, "CCURPC: [$name] $clkey raw request:"; + HMCCURPCPROC_HexDump ($name, $request); + } + + # Decode request + my ($method, $params) = HMCCURPCPROC_DecodeRequest ($request); + return if (!defined ($method)); + Log3 $name, 4, "CCURPC: [$name] request method = $method"; + + if ($method eq 'listmethods') { + $connection->send (HMCCURPCPROC_EncodeResponse ($BINRPC_ARRAY, \@methodlist)); + } + elsif ($method eq 'listdevices') { + HMCCURPCPROC_ListDevicesCB ($server, $clkey); + $connection->send (HMCCURPCPROC_EncodeResponse ($BINRPC_ARRAY, 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})); + next if ($s->{methodName} ne 'event'); + next if (scalar (@{$s->{params}}) < 4); + HMCCURPCPROC_EventCB ($server, $clkey, + ${$s->{params}}[1], ${$s->{params}}[2], ${$s->{params}}[3]); + Log3 $name, 4, "CCURPC: [$name] 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 $ioerrors = 0; + my $sioerrors = 0; + my $run = 1; + my $pid = $$; + my $clkey = 'CB'.$port; + + my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); + + # Initialize RPC server + Log3 $name, 2, "CCURPC: [$name] Initializing RPC server $clkey for interface $iface"; + my $rpcsrv = HMCCURPCPROC_InitRPCServer ($name, $port, $callbackport, $prot); + if (!defined ($rpcsrv)) { + Log3 $name, 1, "CCURPC: [$name] Can't initialize RPC server $clkey for interface $iface"; + return; + } + if (!($rpcsrv->{__daemon})) { + Log3 $name, 1, "CCURPC: [$name] Server socket not found for port $port"; + return; + } + + # 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 (@eventtypes) { + $rpcsrv->{hmccu}{rec}{$et} = 0; + $rpcsrv->{hmccu}{snd}{$et} = 0; + } + $rpcsrv->{hmccu}{rec}{total} = 0; + $rpcsrv->{hmccu}{snd}{total} = 0; + + # Signal handler + $SIG{INT} = sub { $run = 0; Log3 $name, 2, "CCURPC: [$name] $clkey received signal INT"; }; + + HMCCURPCPROC_Write ($rpcsrv, "SL", $clkey, $pid); + Log3 $name, 2, "CCURPC: [$name] $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) { + Log3 $name, 4, "CCURPC: [$name] 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) { + Log3 $name, 2, "CCURPC: [$name] Sending data to FHEM failed $ioerrors times. $m"; + $ioerrors = 0; + } + } + } + + # Next statement blocks for rpcAcceptTimeout seconds + Log3 $name, 5, "CCURPC: [$name] RPC server $clkey accepting connections"; + my $connection = $rpcsrv->{__daemon}->accept (); + next if (! $connection); + last if (! $run); + $connection->timeout ($conntimeout) if ($conntimeout > 0.0); + + Log3 $name, 4, "CCURPC: [$name] 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; + } + + Log3 $name, 1, "CCURPC: [$name] 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) { + Log3 $name, 4, "CCURPC: [$name] 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 (@eventtypes) { + Log3 $name, 4, "CCURPC: [$name] $clkey event type = $et: ".$rpcsrv->{hmccu}{rec}{$et}; + } + Log3 $name, 2, "CCURPC: [$name] 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 = "Sent incomplete data"; + } + + return ($bytes, $err); +} + +###################################################################### +# Check if file descriptor is readable and read data. +# Return data and error message. +###################################################################### + +sub HMCCURPCPROC_ReceiveData ($$) +{ + my ($fh, $socktimeout) = @_; + + my $header; + my $data; + my $err = ''; + + # Check if data is available + my $fd = fileno ($fh); + my $rin = ''; + vec ($rin, $fd, 1) = 1; + my $nfound = select ($rin, undef, undef, $socktimeout); + if ($nfound < 0) { + return (undef, $!); + } + elsif ($nfound == 0) { + return (undef, "read: no data"); + } + + # Read datagram size + my $sbytes = sysread ($fh, $header, 4); + if (!defined ($sbytes)) { + return (undef, $!); + } + elsif ($sbytes != 4) { + return (undef, "read: 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, "read: incomplete data"); + } + + return ($data, $err); +} + +###################################################################### +# 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}) { + Log3 $name, 1, "CCURPC: [$name] $cb maximum queue size reached. Dropping event."; + return; + } + + Log3 $name, 2, "CCURPC: [$name] 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 ($et ne 'ND' && $server->{hmccu}{ccuflags} !~ /queueEvents/) { + ($rc, $err) = HMCCURPCPROC_SendData ($server->{hmccu}{sockparent}, $ev); + Log3 $name, 3, "CCURPC: [$name] 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}; + + my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); + + if (defined ($server->{hmccu}{eventqueue})) { + my $queue = $server->{hmccu}{eventqueue}; + + # Send statistic info + my $st = $server->{hmccu}{snd}{total}; + foreach my $et (@eventtypes) { + $st .= '|'.$server->{hmccu}{snd}{$et}; + $server->{hmccu}{snd}{$et} = 0; + } + + Log3 $name, 4, "CCURPC: [$name] Event statistics = $st"; + push (@{$queue}, "ST|$clkey|$st"); + } +} + +###################################################################### +# Helper functions +###################################################################### + +###################################################################### +# Dump variable content as hex/ascii combination +###################################################################### + +sub HMCCURPCPROC_HexDump ($$) +{ + my ($name, $data) = @_; + + my $offset = 0; + + foreach my $chunk (unpack "(a16)*", $data) { + my $hex = unpack "H*", $chunk; # hexadecimal magic + $chunk =~ tr/ -~/./c; # replace unprintables + $hex =~ s/(.{1,8})/$1 /gs; # insert spaces + Log3 $name, 4, sprintf "0x%08x (%05u) %-*s %s", $offset, $offset, 36, $hex, $chunk; + $offset += 16; + } +} + +###################################################################### +# Callback functions +###################################################################### + +###################################################################### +# Callback for new devices +###################################################################### + +sub HMCCURPCPROC_NewDevicesCB ($$$) +{ + my ($server, $cb, $a) = @_; + my $name = $server->{hmccu}{name}; + my $devcount = scalar (@$a); + + Log3 $name, 2, "CCURPC: [$name] $cb NewDevice received $devcount device and channel specifications"; + foreach my $dev (@$a) { + my $msg = ''; + if ($dev->{ADDRESS} =~ /:[0-9]{1,2}$/) { + $msg = "C|".$dev->{ADDRESS}."|".$dev->{TYPE}."|".$dev->{VERSION}."|null|null"; + } + 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; + } + 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); + + Log3 $name, 2, "CCURPC: [$name] $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}; + + Log3 $name, 2, "CCURPC: [$name] $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}; + + Log3 $name, 2, "CCURPC: [$name] $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); + + Log3 $name, 2, "CCURPC: [$name] $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) = @_; + my $name = $server->{hmccu}{name}; + + if ($server->{hmccu}{ccuflags} =~ /ccuInit/) { + $cb = "unknown" if (!defined ($cb)); + Log3 $name, 1, "CCURPC: [$name] $cb ListDevices. Sending init to HMCCU"; + HMCCURPCPROC_Write ($server, "IN", $cb, "INIT|1"); + } + + return RPC::XML::array->new (); +} + + +###################################################################### +# Binary RPC encoding functions +###################################################################### + +###################################################################### +# Encode integer (type = 1) +###################################################################### + +sub HMCCURPCPROC_EncInteger ($) +{ + my ($v) = @_; + + return pack ('Nl', $BINRPC_INTEGER, $v); +} + +###################################################################### +# Encode bool (type = 2) +###################################################################### + +sub HMCCURPCPROC_EncBool ($) +{ + my ($v) = @_; + + 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 +###################################################################### + +sub HMCCURPCPROC_EncName ($) +{ + my ($v) = @_; + + return pack ('N', length ($v)).$v; +} + +###################################################################### +# Encode double (type = 4) +###################################################################### + +sub HMCCURPCPROC_EncDouble ($) +{ + my ($v) = @_; + + my $s = $v < 0 ? -1.0 : 1.0; + my $l = log (abs($v))/log (2); + my $f = $l; + + if ($l-int ($l) > 0) { + $f = ($l < 0) ? -int (abs ($l)+1.0) : int ($l); + } + my $e = $f+1; + my $m = int ($s*$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. Array must contain (type, value) pairs +###################################################################### + +sub HMCCURPCPROC_EncArray ($) +{ + my ($a) = @_; + + my $r = ''; + my $s = 0; + + if (defined ($a)) { + while (my $t = shift @$a) { + my $e = shift @$a; + if ($e) { + $r .= HMCCURPCPROC_EncType ($t, $e); + $s++; + } + } + } + + return pack ('NN', $BINRPC_ARRAY, $s).$r; +} + +###################################################################### +# Encode struct (type = 257) +# Input is hash reference. Hash elements: +# hash->{$element}{T} = Type +# hash->{$element}{V} = Value +###################################################################### + +sub HMCCURPCPROC_EncStruct ($) +{ + my ($h) = @_; + + my $r = ''; + my $s = 0; + + foreach my $k (keys %{$h}) { + $r .= HMCCURPCPROC_EncName ($k); + $r .= HMCCURPCPROC_EncType ($h->{$k}{T}, $h->{$k}{V}); + $s++; + } + + return pack ('NN', $BINRPC_STRUCT, $s).$r; +} + +###################################################################### +# Encode any type +# Input is type and value +# Return encoded data or empty string on error +###################################################################### + +sub HMCCURPCPROC_EncType ($$) +{ + my ($t, $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); + } + else { + return ''; + } +} + +###################################################################### +# Encode RPC request with method and optional parameters. +# Headers are not supported. +# Input is method name and reference to parameter array. +# Array must contain (type, value) pairs +# Return encoded data or empty string on error +###################################################################### + +sub HMCCURPCPROC_EncodeRequest ($$) +{ + my ($method, $args) = @_; + + # Encode method + my $m = HMCCURPCPROC_EncName ($method); + + # Encode parameters + my $r = ''; + my $s = 0; + + if (defined ($args)) { + while (my $t = shift @$args) { + my $e = shift @$args; + last if (!defined ($e)); + $r .= HMCCURPCPROC_EncType ($t, $e); + $s++; + } + } + + # Method, ParameterCount, Parameters + $r = $m.pack ('N', $s).$r; + + # Identifier, ContentLength, Content + # Ggf. +8 + $r = pack ('NN', $BINRPC_REQUEST, length ($r)+8).$r; + + return $r; +} + +###################################################################### +# Encode RPC response +# Input is type and value +###################################################################### + +sub HMCCURPCPROC_EncodeResponse ($$) +{ + my ($t, $v) = @_; + + if (defined ($t) && defined ($v)) { + my $r = HMCCURPCPROC_EncType ($t, $v); + # Ggf. +8 + 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 ('N', substr ($d, $i, 4)); + my $e = unpack ('N', substr ($d, $i+4, 4)); + + return (sprintf ("%.6f",$m/0x40000000*(2**$e)), 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) +###################################################################### + +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, undef) 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, 1); + } + 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, undef); +} + + +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. Supported interfaces or + ports are: + + + + + + + + +
            PortInterface
            2000BidCos-Wired
            2001BidCos-RF
            2010HmIP-RF
            7000HVL
            8701CUxD
            9292Virtual
            +
          +
          + + + Set

          +
            +
          • set <name> rpcrequest <method> [<parameters>]
            + 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. +

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

          • +
          + + + Get

          +
            +
          • get <name> rpcevent
            + Show RPC server events statistics. +

          • +
          • 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
            + queueEvents - Always write events into queue and send them asynchronously to FHEM. + Frequency of event transmission to FHEM depends on attribute rpcConnTimeout.
            + reconnect - Try to re-register at CCU if no events received for rpcEventTimeout seconds
            +

          • +
          • 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 600 seconds. If timeout occurs an event + is triggered. If set to 0 the timeout is ignored. If ccuflag reconnect is set 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. +

          • +
          • 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. +

          • +
          • 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 readable or writeable. Default value + is 0.001 seconds. +
          • +
          +
        + +=end html +=cut + + diff --git a/fhem/FHEM/HMCCUConf.pm b/fhem/FHEM/HMCCUConf.pm index 28ed5a18e..c9c3ee6c2 100644 --- a/fhem/FHEM/HMCCUConf.pm +++ b/fhem/FHEM/HMCCUConf.pm @@ -4,7 +4,7 @@ # # $Id$ # -# Version 4.1.002 +# Version 4.2 # # Configuration parameters for HomeMatic devices. # @@ -71,21 +71,30 @@ use vars qw(%HMCCU_SCRIPTS); substitute => "STATE!(0|false):off,(1|true):on;WORKING!(0|false):no,(1|true):yes", webCmd => "press" }, - "HM-LC-Sw1-Pl-2|HMIP-PS" => { + "HM-LC-Sw1-Pl-2|HM-LC-Sw1-Pl-DN-R1" => { _description => "Steckdose", - _channels => "1,3", + _channels => "1", ccureadingfilter => "STATE", - controldatapoint => "STATE", statedatapoint => "STATE", statevals => "on:true,off:false", substitute => "STATE!(1|true):on,(0|false):off", - webCmd => "control", - widgetOverride => "control:uzsuToggle,off,on" + webCmd => "devstate", + widgetOverride => "devstate:uzsuToggle,off,on" + }, + "HMIP-PS" => { + _description => "Steckdose", + _channels => "3", + ccureadingfilter => "STATE", + statedatapoint => "STATE", + statevals => "on:true,off:false", + substitute => "STATE!(1|true):on,(0|false):off", + webCmd => "devstate", + widgetOverride => "devstate:uzsuToggle,off,on" }, "HM-LC-Dim1L-Pl|HM-LC-Dim1L-Pl-2|HM-LC-Dim1L-CV|HM-LC-Dim2L-CV|HM-LC-Dim2L-SM|HM-LC-Dim1L-Pl-3|HM-LC-Dim1L-CV-2" => { _description => "Funk-Anschnitt-Dimmaktor", _channels => "1", - ccureadingfilter => "(^LEVEL$|DIRECTION)", + ccureadingfilter => "(^LEVEL\$|DIRECTION)", ccuscaleval => "LEVEL:0:1:0:100", cmdIcon => "on:general_an off:general_aus", controldatapoint => "LEVEL", @@ -101,7 +110,7 @@ use vars qw(%HMCCU_SCRIPTS); "HM-LC-Dim1PWM-CV|HM-LC-Dim1PWM-CV-2" => { _description => "Funk-PWM-Dimmaktor", _channels => "1", - ccureadingfilter => "(^LEVEL$|DIRECTION)", + ccureadingfilter => "(^LEVEL\$|DIRECTION)", ccuscaleval => "LEVEL:0:1:0:100", cmdIcon => "on:general_an off:general_aus", controldatapoint => "LEVEL", @@ -173,6 +182,28 @@ use vars qw(%HMCCU_SCRIPTS); webCmd => "control", widgetOverride => "control:uzsuToggle,off,on" }, + "HM-LC-Sw2PBU-FM" => { + _description => "Funk-Schaltaktor 2-fach", + _channels => "1,2", + ccureadingfilter => "STATE", + controldatapoint => "STATE", + statedatapoint => "STATE", + statevals => "on:true,off:false", + substitute => "STATE!(true|1):on,(false|0):off", + webCmd => "control", + widgetOverride => "control:uzsuToggle,off,on" + }, + "HmIP-BSM" => { + _description => "Schalt-Mess-Aktor", + _channels => "4", + ccureadingfilter => "STATE", + statedatapoint => "STATE", + controldatapoint => "STATE", + statevals => "on:true,off:false", + substitute => "STATE!(true|1):on,(false|0):off", + webCmd => "control", + widgetOverride => "control:uzsuToggle,off,on" + }, "HM-SCI-3-FM" => { _description => "3 Kanal Schliesserkontakt", _channels => "1,2,3", @@ -293,6 +324,14 @@ use vars qw(%HMCCU_SCRIPTS); statedatapoint => "STATE", substitute => "ERROR_ALARM_TEST!0:no,1:failed;ERROR_SMOKE_CHAMBER!0:no,1:degraded" }, + "HmIP-SWSD" => { + _description => "Funk-Rauchmelder", + _channels => "1", + ccureadingfilter => "(ALARM_STATUS|TEST_RESULT|ERROR_CODE)", + eventMap => "/datapoint SMOKE_DETECTOR_COMMAND 0:reservedAlarmOff/datapoint SMOKE_DETECTOR_COMMAND 1:intrusionAlarmOff/datapoint SMOKE_DETECTOR_COMMAND 2:intrusionAlarmOn/datapoint SMOKE_DETECTOR_COMMAND 3:smokeTest/datapoint SMOKE_DETECTOR_COMMAND 4:comTest/datapoint SMOKE_DETECTOR_COMMAND 5:comTestRepeat/", + statedatapoint => "SMOKE_DETECTOR_ALARM_STATUS", + substitute => "SMOKE_DETECTOR_ALARM_STATUS!0:noAlarm,1:primaryAlarm,2:intrusionAlarm,3:secondaryAlarm;SMOKE_DETECTOR_TEST_RESULT!0:none,1:smokeTestOK,2:smokeTestFailed,3:comTestSent,4:comTestOK" + }, "HM-Sec-SFA-SM" => { _description => "Alarmsirene", _channels => "1", @@ -340,9 +379,9 @@ use vars qw(%HMCCU_SCRIPTS); %HMCCU_DEV_DEFAULTS = ( "CCU2" => { _description => "HomeMatic CCU2", - "ccudef-readingfilter" => '^(LOW_?BAT|UNREACH)$', + "ccudef-readingfilter" => '^(LOW_?BAT|UNREACH)\$', "ccudef-readingformat" => 'datapoint', - "ccudef-readingname" => '^(.+\.)?AES_KEY$:sign;^(.+\.)?LOW_?BAT$:battery;^(.+\.)?BATTERY_STATE$:batteryLevel;^(.+\.)?UNREACH$:Activity;^(.+\.)?TEMPERATURE$:+temperature;^(.+\.)?SET_TEMPERATURE$:+desired-temp;^(.+\.)?HUMIDITY$:+humidity;^(.+\.)?LEVEL$:+pct;^(.+\.)?CONTROL_MODE$:+controlMode', + "ccudef-readingname" => '^(.+\.)?AES_KEY\$:sign;^(.+\.)?LOW_?BAT\$:battery;^(.+\.)?BATTERY_STATE\$:batteryLevel;^(.+\.)?UNREACH\$:Activity;^(.+\.)?TEMPERATURE\$:+temperature;^(.+\.)?SET_TEMPERATURE\$:+desired-temp;^(.+\.)?HUMIDITY\$:+humidity;^(.+\.)?LEVEL\$:+pct;^(.+\.)?CONTROL_MODE\$:+controlMode', "ccudef-substitute" => 'AES_KEY!(0|false):off,(1|true):on;LOWBAT,LOW_BAT!(0|false):ok,(1|true):low;UNREACH!(0|false):alive,(1|true):dead;MOTION!(0|false):noMotion,(1|true):motion;DIRECTION!0:stop,1:up,2:down,3:undefined;WORKING!0:false,1:true;INHIBIT!(0|false):unlocked,(1|true):locked' }, "HM-Sec-SCo|HM-Sec-SC|HM-Sec-SC-2|HMIP-SWDO" => { @@ -388,25 +427,23 @@ use vars qw(%HMCCU_SCRIPTS); substitute => "STATE!(0|false):off,(1|true):on;WORKING!(0|false):no,(1|true):yes", webCmd => "press" }, - "HM-LC-Sw1-Pl-2" => { + "HM-LC-Sw1-Pl-2|HM-LC-Sw1-Pl-DN-R1" => { _description => "Steckdose", ccureadingfilter => "STATE", - controldatapoint => "1.STATE", statedatapoint => "1.STATE", statevals => "on:true,off:false", substitute => "STATE!(1|true):on,(0|false):off", - webCmd => "control", - widgetOverride => "control:uzsuToggle,off,on" + webCmd => "devstate", + widgetOverride => "devstate:uzsuToggle,off,on" }, "HMIP-PS" => { _description => "Steckdose IP", ccureadingfilter => "STATE", - controldatapoint => "3.STATE", statedatapoint => "3.STATE", statevals => "on:1,off:0", substitute => "STATE!(1|true):on,(0|false):off", - webCmd => "control", - widgetOverride => "control:uzsuToggle,off,on" + webCmd => "devstate", + widgetOverride => "devstate:uzsuToggle,off,on" }, "HM-ES-PMSw1-Pl|HM-ES-PMSw1-Pl-DN-R1|HM-ES-PMSw1-Pl-DN-R2|HM-ES-PMSw1-Pl-DN-R3|HM-ES-PMSw1-Pl-DN-R4|HM-ES-PMSw1-Pl-DN-R5" => { _description => "Steckdose mit Energiemessung", @@ -421,7 +458,7 @@ use vars qw(%HMCCU_SCRIPTS); }, "HMIP-PSM" => { _description => "Steckdose mit Energiemessung IP", - ccureadingfilter => "(STATE|CURRENT|^ENERGY_COUNTER$|POWER)", + ccureadingfilter => "(STATE|CURRENT|^ENERGY_COUNTER\$|POWER)", controldatapoint => "3.STATE", statedatapoint => "3.STATE", statevals => "on:true,off:false", @@ -431,7 +468,7 @@ use vars qw(%HMCCU_SCRIPTS); widgetOverride => "control:uzsuToggle,off,on" }, "HM-LC-Dim1L-Pl|HM-LC-Dim1L-Pl-2|HM-LC-Dim1L-CV|HM-LC-Dim2L-CV|HM-LC-Dim2L-SM|HM-LC-Dim1L-Pl-3|HM-LC-Dim1L-CV-2" => { _description => "Funk-Anschnitt-Dimmaktor", - ccureadingfilter => "(^LEVEL$|DIRECTION)", + ccureadingfilter => "(^LEVEL\$|DIRECTION)", ccuscaleval => "LEVEL:0:1:0:100", cmdIcon => "on:general_an off:general_aus", controldatapoint => "1.LEVEL", @@ -446,7 +483,7 @@ use vars qw(%HMCCU_SCRIPTS); }, "HM-LC-Dim1PWM-CV|HM-LC-Dim1PWM-CV-2" => { _description => "Funk-PWM-Dimmaktor", - ccureadingfilter => "(^LEVEL$|DIRECTION)", + ccureadingfilter => "(^LEVEL\$|DIRECTION)", ccuscaleval => "LEVEL:0:1:0:100", cmdIcon => "on:general_an off:general_aus", controldatapoint => "1.LEVEL", @@ -506,6 +543,26 @@ use vars qw(%HMCCU_SCRIPTS); webCmd => "control", widgetOverride => "control:uzsuToggle,off,on" }, + "HM-LC-Sw2PBU-FM" => { + _description => "Funk-Schaltaktor 2-fach", + ccureadingfilter => "STATE", + controldatapoint => "1.STATE", + statedatapoint => "1.STATE", + statevals => "on:true,off:false", + substitute => "STATE!(true|1):on,(false|0):off", + webCmd => "control", + widgetOverride => "control:uzsuToggle,off,on" + }, + "HmIP-BSM" => { + _description => "Schalt-Mess-Aktor", + ccureadingfilter => "(STATE|PRESS)", + statedatapoint => "4.STATE", + controldatapoint => "4.STATE", + statevals => "on:true,off:false", + substitute => "STATE!(true|1):on,(false|0):off", + webCmd => "control", + widgetOverride => "control:uzsuToggle,off,on" + }, "HM-LC-SW4-BA-PCB|HM-SCI-3-FM" => { _description => "4 Kanal Funk Schaltaktor für Batteriebetrieb, 3 Kanal Schließerkontakt", ccureadingfilter => "STATE", @@ -679,6 +736,13 @@ use vars qw(%HMCCU_SCRIPTS); statedatapoint => "1.STATE", substitute => "STATE!(0|false):ok,(1|true):alarm" }, + "HmIP-SWSD" => { + _description => "Funk-Rauchmelder", + ccureadingfilter => "(ALARM_STATUS|TEST_RESULT|ERROR_CODE)", + eventMap => "/datapoint 1.SMOKE_DETECTOR_COMMAND 0:reservedAlarmOff/datapoint 1.SMOKE_DETECTOR_COMMAND 1:intrusionAlarmOff/datapoint 1.SMOKE_DETECTOR_COMMAND 2:intrusionAlarmOn/datapoint 1.SMOKE_DETECTOR_COMMAND 3:smokeTest/datapoint 1.SMOKE_DETECTOR_COMMAND 4:comTest/datapoint 1.SMOKE_DETECTOR_COMMAND 5:comTestRepeat/", + statedatapoint => "SMOKE_DETECTOR_ALARM_STATUS", + substitute => "SMOKE_DETECTOR_ALARM_STATUS!0:noAlarm,1:primaryAlarm,2:intrusionAlarm,3:secondaryAlarm;SMOKE_DETECTOR_TEST_RESULT!0:none,1:smokeTestOK,2:smokeTestFailed,3:comTestSent,4:comTestOK" + }, "HM-Sec-SFA-SM" => { _description => "Alarmsirene", ccureadingfilter => "STATE", @@ -931,16 +995,42 @@ if (odev) { } else { WriteLine ("ERROR: Device not found"); +} + ) + }, + "GetDevice" => { + description => "Query CCU device or channel", + syntax => "name", + parameters => 1, + code => qq( +object odev=dom.GetObject("\$name"); +if (odev) { + if (odev.IsTypeOf (OT_CHANNEL)) { + string devid = odev.Device(); + odev = dom.GetObject (devid); + } + + string intid=odev.Interface(); + string intna=dom.GetObject(intid).Name(); + string chnid; + integer cc=0; + foreach (chnid, odev.Channels()) { + object ochn=dom.GetObject(chnid); + WriteLine("C;" # ochn.Address() # ";" # ochn.Name() # ";" # ochn.ChnDirection()); + cc=cc+1; + } + WriteLine("D;" # intna # ";" # odev.Address() # ";" # odev.Name() # ";" # odev.HssType() # ";" # cc); } ) }, "GetDeviceList" => { - description => "Query CCU devices and channels", + description => "Query CCU devices, channels and interfaces", syntax => "", parameters => 0, code => qq( string devid; string chnid; +string sifId; foreach(devid, root.Devices().EnumUsedIDs()) { object odev=dom.GetObject(devid); string intid=odev.Interface(); @@ -952,6 +1042,12 @@ foreach(devid, root.Devices().EnumUsedIDs()) { cc=cc+1; } WriteLine("D;" # intna # ";" # odev.Address() # ";" # odev.Name() # ";" # odev.HssType() # ";" # cc); +} +foreach(sifId, root.Interfaces().EnumIDs()) { + object oIf=dom.GetObject(sifId); + if (oIf) { + WriteLine("I;" # oIf.Name() # ';' # oIf.InterfaceInfo() # ';' # oIf.InterfaceUrl()); + } } ) }, @@ -1025,6 +1121,8 @@ string sDevList = "\$list"; foreach (sDevice, sDevList.Split(",")) { object odev = dom.GetObject (sDevice); if (odev) { + string intid = odev.Interface(); + string intna = dom.GetObject(intid).Name(); string sType = odev.HssType(); foreach (chnid, odev.Channels()) { object ochn = dom.GetObject(chnid); @@ -1035,7 +1133,7 @@ foreach (sDevice, sDevList.Split(",")) { object oDP = dom.GetObject(sDPId); if (oDP) { string sDPName = oDP.Name().StrValueByIndex(".",2); - WriteLine (sAddr # ";" # sType # ";" # sChnNo # ";" # sDPName # ";" # oDP.ValueType() # ";" # oDP.Operations()); + WriteLine (intna # ";" # sAddr # ";" # sType # ";" # sChnNo # ";" # sDPName # ";" # oDP.ValueType() # ";" # oDP.Operations()); } } } @@ -1062,6 +1160,50 @@ foreach (sChannel, sChnList.Split(",")) { } } } +} + ) + }, + "GetInterfaceList" => { + description => "Get CCU RPC interfaces", + syntax => "", + parameters => 0, + code => qq( +string sifId; +foreach(sifId, root.Interfaces().EnumIDs()) { + object oIf = dom.GetObject(sifId); + if (oIf) { + WriteLine (oIf.Name() # ';' # oIf.InterfaceInfo() # ';' # oIf.InterfaceUrl()); + } +} + ) + }, + "ClearUnreachable" => { + description => "Clear device unreachable alarms in CCU", + syntax => "", + parameters => 0, + code => qq( +string itemID; +string address; +object aldp_obj; +foreach(itemID, dom.GetObject(ID_DEVICES).EnumUsedIDs()) { + address = dom.GetObject(itemID).Address(); + aldp_obj = dom.GetObject("AL-" # address # ":0.STICKY_UNREACH"); + if (aldp_obj) { + if (aldp_obj.Value()) { + aldp_obj.AlReceipt(); + } + } +} + ) + }, + "GetNameByAddress" => { + description => "Get device or channel name by address", + syntax => "iface, address", + parameters => 2, + code => qq( +object lObjDevice = xmlrpc.GetObjectByHSSAddress(interfaces.Get("\$iface"),"\$address"); +if (lObjDevice) { + WriteLine (lObjDevice.Name()); } ) }