mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-04-09 07:44:19 +00:00
HMCCU: New commands and bug fixes
git-svn-id: https://svn.fhem.de/fhem/trunk@18552 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
parent
32fa778897
commit
62e257e40c
@ -1,5 +1,6 @@
|
||||
# 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.
|
||||
- feature: 88_HMCCU: New commands and bug fixes
|
||||
- changed: 93_DbLog: CommandRef revised
|
||||
- feature: 98_HTTPMOD: starting with featurelevel >5.9 enableCookies,
|
||||
enableControlSet, handleRedirects and
|
||||
|
@ -4,7 +4,7 @@
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
# Version 4.3.010
|
||||
# Version 4.3.011
|
||||
#
|
||||
# Module for communication between FHEM and Homematic CCU2/3.
|
||||
#
|
||||
@ -51,7 +51,7 @@ my %HMCCU_CUST_CHN_DEFAULTS;
|
||||
my %HMCCU_CUST_DEV_DEFAULTS;
|
||||
|
||||
# HMCCU version
|
||||
my $HMCCU_VERSION = '4.3.010';
|
||||
my $HMCCU_VERSION = '4.3.011';
|
||||
|
||||
# Default RPC interface and port (BidCos-RF)
|
||||
my $HMCCU_RPC_PORT_DEFAULT = 2001;
|
||||
@ -73,6 +73,11 @@ my $HMCCU_TIMEOUT_EVENT = 600;
|
||||
my $HMCCU_STATISTICS = 500;
|
||||
my $HMCCU_TIMEOUT_REQUEST = 4;
|
||||
|
||||
# ReGa Ports
|
||||
my %HMCCU_REGA_PORT = (
|
||||
'http' => 8181, 'https' => '48181'
|
||||
);
|
||||
|
||||
# RPC port name by port number
|
||||
my %HMCCU_RPC_NUMPORT = (
|
||||
2000 => 'BidCos-Wired', 2001 => 'BidCos-RF', 2010 => 'HmIP-RF', 9292 => 'VirtualDevices',
|
||||
@ -87,10 +92,15 @@ my %HMCCU_RPC_PORT = (
|
||||
|
||||
# RPC flags
|
||||
my %HMCCU_RPC_FLAG = (
|
||||
2000 => 'forceASCII', 2001 => 'forceASCII', 2003 => '_', 2010 => '_',
|
||||
2000 => 'forceASCII', 2001 => 'forceASCII', 2003 => '_', 2010 => 'forceASCII',
|
||||
7000 => 'forceInit', 8701 => 'forceInit', 9292 => '_'
|
||||
);
|
||||
|
||||
my %HMCCU_RPC_SSL = (
|
||||
2000 => 1, 2001 => 1, 2010 => 1, 9292 => 1,
|
||||
'BidCos-Wired' => 1, 'BidCos-RF' => 1, 'HmIP-RF' => 1, 'VirtualDevices' => 1
|
||||
);
|
||||
|
||||
# Initial intervals for registration of RPC callbacks and reading RPC queue
|
||||
#
|
||||
# X = Start RPC server
|
||||
@ -323,14 +333,19 @@ sub HMCCU_QueueEnq ($$);
|
||||
sub HMCCU_QueueDeq ($);
|
||||
|
||||
# Helper functions
|
||||
sub HMCCU_BuildURL ($$);
|
||||
sub HMCCU_CalculateReading ($$);
|
||||
sub HMCCU_CorrectName ($);
|
||||
sub HMCCU_Encrypt ($);
|
||||
sub HMCCU_Decrypt ($);
|
||||
sub HMCCU_DeleteReadings ($$);
|
||||
sub HMCCU_EncodeEPDisplay ($);
|
||||
sub HMCCU_ExprMatch ($$$);
|
||||
sub HMCCU_ExprNotMatch ($$$);
|
||||
sub HMCCU_GetDutyCycle ($);
|
||||
sub HMCCU_GetHMState ($$$);
|
||||
sub HMCCU_GetIdFromIP ($$);
|
||||
sub HMCCU_GetTimeSpec ($);
|
||||
sub HMCCU_CorrectName ($);
|
||||
sub HMCCU_RefToString ($);
|
||||
sub HMCCU_ResolveName ($$);
|
||||
sub HMCCU_TCPConnect ($$);
|
||||
@ -392,8 +407,17 @@ sub HMCCU_Define ($$)
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
return "Specify CCU hostname or IP address as a parameter" if (scalar (@$a) < 3);
|
||||
|
||||
$hash->{host} = $$a[2];
|
||||
|
||||
# Setup http or ssl connection
|
||||
if ($$a[2] =~ /^(https?):\/\/(.+)/) {
|
||||
$hash->{prot} = $1;
|
||||
$hash->{host} = $2;
|
||||
}
|
||||
else {
|
||||
$hash->{prot} = 'http';
|
||||
$hash->{host} = $$a[2];
|
||||
}
|
||||
|
||||
$hash->{Clients} = ':HMCCUDEV:HMCCUCHN:HMCCURPC:HMCCURPCPROC:';
|
||||
$hash->{hmccu}{ccu}{delay} = exists ($h->{ccudelay}) ? $h->{ccudelay} : $HMCCU_CCU_BOOT_DELAY;
|
||||
$hash->{hmccu}{ccu}{timeout} = exists ($h->{waitforccu}) ? $h->{waitforccu} : $HMCCU_CCU_PING_TIMEOUT;
|
||||
@ -408,12 +432,12 @@ sub HMCCU_Define ($$)
|
||||
HMCCU_Log ($hash, 1, "Forced delayed initialization", 0);
|
||||
}
|
||||
else {
|
||||
if (HMCCU_TCPPing ($hash->{host}, 8181, $hash->{hmccu}{ccu}{timeout})) {
|
||||
if (HMCCU_TCPPing ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}, $hash->{hmccu}{ccu}{timeout})) {
|
||||
$hash->{ccustate} = 'active';
|
||||
}
|
||||
else {
|
||||
$hash->{ccustate} = 'unreachable';
|
||||
HMCCU_Log ($hash, 1, "CCU port 8181 is not reachable", 0);
|
||||
HMCCU_Log ($hash, 1, "CCU port ".$HMCCU_REGA_PORT{$hash->{prot}}." is not reachable", 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,7 +505,7 @@ sub HMCCU_Define ($$)
|
||||
# Initialization of FHEM device.
|
||||
# Called during Define() or by HMCCU after CCU ready.
|
||||
# Return 0 on successful initialization or >0 on error:
|
||||
# 1 = CCU port 8181 is not reachable.
|
||||
# 1 = CCU port 8181 or 48181 is not reachable.
|
||||
# 2 = Error while reading device list from CCU.
|
||||
######################################################################
|
||||
|
||||
@ -492,9 +516,9 @@ sub HMCCU_InitDevice ($)
|
||||
|
||||
if ($hash->{hmccu}{ccu}{delayed} == 1) {
|
||||
HMCCU_Log ($hash, 1, "HMCCU: Initializing devices", 0);
|
||||
if (!HMCCU_TCPPing ($hash->{host}, 8181, $hash->{hmccu}{ccu}{timeout})) {
|
||||
if (!HMCCU_TCPPing ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}, $hash->{hmccu}{ccu}{timeout})) {
|
||||
$hash->{ccustate} = 'unreachable';
|
||||
HMCCU_Log ($hash, 1, "HMCCU: CCU port 8181 is not reachable", 0);
|
||||
HMCCU_Log ($hash, 1, "HMCCU: CCU port ".$HMCCU_REGA_PORT{$hash->{prot}}." is not reachable", 0);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@ -1169,12 +1193,12 @@ sub HMCCU_Detail ($$$$)
|
||||
<table class="block wide">
|
||||
<tr class="odd">
|
||||
<td><div class="col1">
|
||||
> <a target="_blank" href="http://$hash->{host}">CCU WebUI</a>
|
||||
> <a target="_blank" href="$hash->{prot}://$hash->{host}">CCU WebUI</a>
|
||||
</div></td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td><div class="col1">
|
||||
> <a target="_blank" href="http://$hash->{host}/addons/cuxd/index.ccc">CUxD Config</a>
|
||||
> <a target="_blank" href="$hash->{prot}://$hash->{host}/addons/cuxd/index.ccc">CUxD Config</a>
|
||||
</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -1379,8 +1403,8 @@ sub HMCCU_Set ($@)
|
||||
my ($hash, $a, $h) = @_;
|
||||
my $name = shift @$a;
|
||||
my $opt = shift @$a;
|
||||
my $options = "var delete execute hmscript cleardefaults:noArg defaults:noArg ".
|
||||
"importdefaults rpcregister:all rpcserver:on,off,restart ackmessages:noArg";
|
||||
my $options = "var clear delete execute hmscript cleardefaults:noArg defaults:noArg ".
|
||||
"importdefaults rpcregister:all rpcserver:on,off,restart ackmessages:noArg authentication";
|
||||
my @ifList = HMCCU_GetRPCInterfaceList ($hash);
|
||||
if (scalar (@ifList) > 0) {
|
||||
my $ifStr = join (',', @ifList);
|
||||
@ -1389,7 +1413,9 @@ sub HMCCU_Set ($@)
|
||||
my $usage = "HMCCU: Unknown argument $opt, choose one of $options";
|
||||
my $host = $hash->{host};
|
||||
|
||||
return "HMCCU: I/O device not initialized. Try again later." if ($hash->{hmccu}{ccu}{delayed});
|
||||
return undef if ($hash->{hmccu}{ccu}{delayed});
|
||||
return "HMCCU: CCU is unreachable, choose one of initialize:noArg" if ($hash->{ccustate} eq 'unreachable');
|
||||
return undef if ($hash->{ccustate} ne 'active');
|
||||
return "HMCCU: CCU busy, choose one of rpcserver:off"
|
||||
if ($opt ne 'rpcserver' && HMCCU_IsRPCStateBlocking ($hash));
|
||||
|
||||
@ -1419,7 +1445,7 @@ sub HMCCU_Set ($@)
|
||||
$vartype = shift @$a if (scalar (@$a) == 3);
|
||||
my $objname = shift @$a;
|
||||
my $objvalue = shift @$a;
|
||||
$usage = "set $name $opt [{'bool'|'list'|'number'|'test'}] variable value [param=value [...]]";
|
||||
$usage = "set $name $opt [{'bool'|'list'|'number'|'text'}] variable value [param=value [...]]";
|
||||
|
||||
return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue));
|
||||
|
||||
@ -1432,6 +1458,48 @@ sub HMCCU_Set ($@)
|
||||
return HMCCU_SetError ($hash, $result) if ($result < 0);
|
||||
return HMCCU_SetState ($hash, "OK");
|
||||
}
|
||||
# elsif ($opt eq 'test') {
|
||||
# my $backend = shift @$a;
|
||||
# my $url = defined ($backend) ? HMCCU_BuildURL ($hash, $backend) : '';
|
||||
# return "URL=$url";
|
||||
# }
|
||||
elsif ($opt eq 'initialize') {
|
||||
return HMCCU_SetError ($hash, "State of CCU must be unreachable")
|
||||
if ($hash->{ccustate} ne 'unreachable');
|
||||
my $err = HMCCU_InitDevice ($hash);
|
||||
return HMCCU_SetError ($hash, "CCU not reachable") if ($err == 1);
|
||||
return HMCCU_SetError ($hash, "Can't read device list from CCU") if ($err == 2);
|
||||
return HMCCU_SetState ($hash, "OK");
|
||||
}
|
||||
elsif ($opt eq 'authentication') {
|
||||
my $username = shift @$a;
|
||||
my $password = shift @$a;
|
||||
$usage = "set $name $opt username password";
|
||||
|
||||
if (!defined ($username)) {
|
||||
setKeyValue ($name."_username", undef);
|
||||
setKeyValue ($name."_password", undef);
|
||||
return "Credentials for CCU authentication deleted";
|
||||
}
|
||||
|
||||
return HMCCU_SetError ($hash, $usage) if (!defined ($password));
|
||||
|
||||
my $encuser = HMCCU_Encrypt ($username);
|
||||
my $encpass = HMCCU_Encrypt ($password);
|
||||
return HMCCU_SetError ($hash, "Encryption of credentials failed") if ($encuser eq '' || $encpass eq '');
|
||||
|
||||
my $err = setKeyValue ($name."_username", $encuser);
|
||||
return HMCCU_SetError ($hash, "Can't store credentials. $err") if (defined ($err));
|
||||
$err = setKeyValue ($name."_password", $encpass);
|
||||
return HMCCU_SetError ($hash, "Can't store credentials. $err") if (defined ($err));
|
||||
|
||||
return "Credentials for CCU authentication stored";
|
||||
}
|
||||
elsif ($opt eq 'clear') {
|
||||
my $rnexp = shift @$a;
|
||||
HMCCU_DeleteReadings ($hash, $rnexp);
|
||||
return HMCCU_SetState ($hash, "OK");
|
||||
}
|
||||
elsif ($opt eq 'datapoint') {
|
||||
return HMCCU_SetError ($hash, "Command set datapoint is no longer supported by I/O device");
|
||||
}
|
||||
@ -1626,11 +1694,12 @@ sub HMCCU_Get ($@)
|
||||
my $opt = shift @$a;
|
||||
|
||||
my $options = "defaults:noArg exportdefaults devicelist dump dutycycle:noArg vars update".
|
||||
" updateccu parfile configdesc firmware rpcevents:noArg rpcstate:noArg deviceinfo";
|
||||
" updateccu parfile configdesc firmware rpcevents:noArg rpcstate:noArg deviceinfo".
|
||||
" ccumsg:alarm,service";
|
||||
my $usage = "HMCCU: Unknown argument $opt, choose one of $options";
|
||||
my $host = $hash->{host};
|
||||
|
||||
return "HMCCU: I/O device not initialized. Try again later." if ($hash->{hmccu}{ccu}{delayed});
|
||||
return undef if ($hash->{hmccu}{ccu}{delayed} || $hash->{ccustate} ne 'active');
|
||||
return "HMCCU: CCU busy, choose one of rpcstate:noArg"
|
||||
if ($opt ne 'rpcstate' && HMCCU_IsRPCStateBlocking ($hash));
|
||||
|
||||
@ -1876,7 +1945,7 @@ sub HMCCU_Get ($@)
|
||||
# Define new client device
|
||||
my $ret = CommandDefine (undef, $devname." $defmod ".$add);
|
||||
if ($ret) {
|
||||
Log3 $name, 2, "HMCCU: Define command failed $devname $defmod $ccuname";
|
||||
Log3 $name, 2, "HMCCU: [$name] Define command failed $devname $defmod $ccuname";
|
||||
Log3 $name, 2, "$defmod: $ret";
|
||||
$result .= "\nCan't create device $devname. $ret";
|
||||
next;
|
||||
@ -1996,6 +2065,24 @@ sub HMCCU_Get ($@)
|
||||
return HMCCU_SetError ($hash, $rc) if ($rc < 0);
|
||||
return HMCCU_SetState ($hash, "OK", $res);
|
||||
}
|
||||
elsif ($opt eq 'ccumsg') {
|
||||
my $msgtype = shift @$a;
|
||||
$usage = "Usage: get $name $opt {service|alarm}";
|
||||
return HMCCU_SetError ($hash, $usage) if (!defined ($msgtype));
|
||||
|
||||
my $script = ($msgtype eq 'service') ? "!GetServiceMessages" : "!GetAlarms";
|
||||
my $res = HMCCU_HMScriptExt ($hash, $script, undef);
|
||||
|
||||
return HMCCU_SetError ($hash, "Error") if ($res eq '' || $res =~ /^ERROR:.*/);
|
||||
|
||||
# Generate event for each message
|
||||
foreach my $msg (split /\n/, $res) {
|
||||
next if ($msg =~ /^[0-9]+$/);
|
||||
DoTrigger ($name, $msg);
|
||||
}
|
||||
|
||||
return HMCCU_SetState ($hash, "OK", $res);
|
||||
}
|
||||
else {
|
||||
if (exists ($hash->{hmccu}{agg})) {
|
||||
my @rules = keys %{$hash->{hmccu}{agg}};
|
||||
@ -2186,9 +2273,7 @@ sub HMCCU_FilterReading ($$$)
|
||||
return 1 if (!defined ($hmccu_hash));
|
||||
|
||||
my $grf = AttrVal ($hmccu_hash->{NAME}, 'ccudef-readingfilter', '.*');
|
||||
# $grf = '.*' if ($grf eq '');
|
||||
my $rf = AttrVal ($name, 'ccureadingfilter', $grf);
|
||||
# $rf = $grf.";".$rf if ($rf ne $grf && $grf ne '.*' && $grf ne '');
|
||||
$rf = $grf.";".$rf if ($rf ne $grf && $grf ne '.*');
|
||||
|
||||
my $chnnam = '';
|
||||
@ -3525,9 +3610,12 @@ sub HMCCU_GetRPCCallbackURL ($$$$$)
|
||||
|
||||
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".
|
||||
|
||||
my $url = $hmccu_hash->{hmccu}{interfaces}{$ifname}{prot}."://$localaddr:$cbport/fh".
|
||||
$hmccu_hash->{hmccu}{interfaces}{$ifname}{port};
|
||||
$url =~ s/^https/http/;
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
@ -3535,6 +3623,7 @@ sub HMCCU_GetRPCCallbackURL ($$$$$)
|
||||
# Parameter iface can be a port number or an interface name.
|
||||
# Valid values for info are:
|
||||
# url, port, prot, host, type, name, flags, device.
|
||||
# Return undef for invalid interface or info token.
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_GetRPCServerInfo ($$$)
|
||||
@ -3760,7 +3849,7 @@ sub HMCCU_StopExtRPCServer ($)
|
||||
next;
|
||||
}
|
||||
$hash->{hmccu}{interfaces}{$ifname}{manager} = 'HMCCU';
|
||||
$rc &= HMCCURPCPROC_StopRPCServer ($defs{$rpcdev});
|
||||
$rc &= HMCCURPCPROC_StopRPCServer ($defs{$rpcdev}, undef);
|
||||
}
|
||||
|
||||
return $rc;
|
||||
@ -3867,7 +3956,7 @@ sub HMCCU_StartIntRPCServer ($)
|
||||
# Initialize statistic counters
|
||||
HMCCU_ResetCounters ($hash);
|
||||
|
||||
Log3 $name, 0, "RPC server(s) starting";
|
||||
Log3 $name, 0, "HMCCU: [$name] RPC server(s) starting";
|
||||
DoTrigger ($name, "RPC server starting");
|
||||
|
||||
InternalTimer (gettimeofday()+$rpcinterval, 'HMCCU_ReadRPCQueue', $hash, 0);
|
||||
@ -4252,6 +4341,7 @@ sub HMCCU_GetDeviceList ($)
|
||||
# Delete old entries
|
||||
%{$hash->{hmccu}{dev}} = ();
|
||||
%{$hash->{hmccu}{adr}} = ();
|
||||
%{$hash->{hmccu}{interfaces}} = ();
|
||||
%{$hash->{hmccu}{grp}} = ();
|
||||
%{$hash->{hmccu}{prg}} = ();
|
||||
$hash->{hmccu}{updatetime} = time ();
|
||||
@ -4319,6 +4409,10 @@ sub HMCCU_GetDeviceList ($)
|
||||
$port -= 30000;
|
||||
$ifurl =~ s/:3$port/:$port/;
|
||||
}
|
||||
# if ($hash->{prot} eq 'https') {
|
||||
# # For secure connections add 40000 to port
|
||||
# $ifurl =~ s/:$port/:4$port/;
|
||||
# }
|
||||
if ($hash->{ccuip} ne 'N/A') {
|
||||
$ifurl =~ s/127\.0\.0\.1/$hash->{ccuip}/;
|
||||
$ipaddr =~ s/127\.0\.0\.1/$hash->{ccuip}/;
|
||||
@ -5121,6 +5215,7 @@ sub HMCCU_GetRPCDevice ($$$)
|
||||
my $rpcdevname;
|
||||
my $rpcdevtype = 'HMCCURPCPROC';
|
||||
my $rpchost = $hash->{host};
|
||||
my $rpcprot = $hash->{prot};
|
||||
|
||||
my $ccuflags = HMCCU_GetFlags ($name);
|
||||
|
||||
@ -5156,8 +5251,12 @@ sub HMCCU_GetRPCDevice ($$$)
|
||||
my $devhash = $defs{$dev};
|
||||
next if ($devhash->{TYPE} ne $rpcdevtype);
|
||||
my $ip = 'null';
|
||||
my $addrnum = inet_aton ($devhash->{host});
|
||||
$ip = inet_ntoa ($addrnum) if (defined ($addrnum));
|
||||
if (!exists ($devhash->{rpcip})) {
|
||||
$ip = HMCCU_Resolve ($devhash->{host}, 'null');
|
||||
}
|
||||
else {
|
||||
$ip = $devhash->{rpcip};
|
||||
}
|
||||
next if ($devhash->{host} ne $rpchost && $ip ne $rpchost);
|
||||
# next if ($rpcdevtype eq 'HMCCURPCPROC' && $devhash->{rpcinterface} ne $ifname);
|
||||
next if ($devhash->{rpcinterface} ne $ifname);
|
||||
@ -5181,15 +5280,20 @@ sub HMCCU_GetRPCDevice ($$$)
|
||||
|
||||
# Create RPC device
|
||||
if ($create) {
|
||||
my $alias = "CCU RPC";
|
||||
my $rpccreate = "d_rpc $rpcdevtype ".$hash->{host};
|
||||
my $alias = "CCU RPC $ifname";
|
||||
my $rpccreate = '';
|
||||
$rpcdevname = "d_rpc";
|
||||
if (defined ($ifname)) {
|
||||
$rpcdevname = makeDeviceName ("d_rpc".$ifname);
|
||||
$alias .= " $ifname";
|
||||
$rpccreate = "$rpcdevname $rpcdevtype $rpchost $ifname";
|
||||
}
|
||||
|
||||
# Ensure unique device name by appending last 2 digits of CCU IP address
|
||||
$rpcdevname .= HMCCU_GetIdFromIP ($hash->{ccuip}, '') if (exists ($hash->{ccuip}));
|
||||
|
||||
# Build device name and define command
|
||||
$rpcdevname = makeDeviceName ($rpcdevname.$ifname);
|
||||
$rpccreate = "$rpcdevname $rpcdevtype $rpcprot://$rpchost $ifname";
|
||||
return (HMCCU_Log ($hash, 2, "Device $rpcdevname already exists. Please delete or rename it.", ''), 0)
|
||||
if (exists ($defs{"$rpcdevname"}));
|
||||
|
||||
# Create RPC device
|
||||
HMCCU_Log ($hash, 1, "Creating new RPC device $rpcdevname", undef);
|
||||
my $ret = CommandDefine (undef, $rpccreate);
|
||||
if (!defined ($ret)) {
|
||||
@ -5237,7 +5341,7 @@ sub HMCCU_AssignIODevice ($$$)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($type eq 'HMCCURPCPROC' && defined ($ifname)) {
|
||||
if ($type eq 'HMCCURPCPROC' && defined ($ifname) && exists ($hmccu_hash->{hmccu}{interfaces}{$ifname})) {
|
||||
# Register RPC device
|
||||
$hmccu_hash->{hmccu}{interfaces}{$ifname}{device} = $name;
|
||||
}
|
||||
@ -5794,7 +5898,7 @@ sub HMCCU_ReadRPCQueue ($)
|
||||
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) ne '' ? 'timeout' : 'unreachable';
|
||||
$hash->{ccustate} = HMCCU_TCPConnect ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}) ne '' ? 'timeout' : 'unreachable';
|
||||
Log3 $name, 2, "HMCCU: Received no events from CCU since $rpcevtimeout seconds";
|
||||
DoTrigger ($name, "No events from CCU since $rpcevtimeout seconds");
|
||||
}
|
||||
@ -5858,6 +5962,7 @@ sub HMCCU_ReadRPCQueue ($)
|
||||
######################################################################
|
||||
# Execute Homematic command on CCU.
|
||||
# If parameter mode is 1 an empty string is a valid result.
|
||||
# Return undef on error.
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_HMCommand ($$$)
|
||||
@ -5868,20 +5973,26 @@ sub HMCCU_HMCommand ($$$)
|
||||
|
||||
my $io_hash = HMCCU_GetHash ($cl_hash);
|
||||
my $ccureqtimeout = AttrVal ($io_hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST);
|
||||
my $url = "http://".$io_hash->{host}.":8181/tclrega.exe";
|
||||
my $url = HMCCU_BuildURL ($io_hash, 'rega');
|
||||
my $value;
|
||||
|
||||
HMCCU_Trace ($cl_hash, 2, $fnc, "URL=$url, cmd=$cmd");
|
||||
|
||||
my $response = GetFileFromURL ($url, $ccureqtimeout, $cmd);
|
||||
if (defined ($response)) {
|
||||
my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST" };
|
||||
$param->{sslargs} = { SSL_verify_mode => 0 };
|
||||
my ($err, $response) = HttpUtils_BlockingGet ($param);
|
||||
|
||||
# my $response = GetFileFromURL ($url, $ccureqtimeout, $cmd);
|
||||
if ($err eq '') {
|
||||
$value = $response;
|
||||
$value =~ s/<xml>(.*)<\/xml>//;
|
||||
$value =~ s/\r//g;
|
||||
HMCCU_Trace ($cl_hash, 2, $fnc, "Response=$response, Value=".(defined ($value) ? $value : "undef"));
|
||||
}
|
||||
else {
|
||||
HMCCU_Trace ($cl_hash, 2, $fnc, "Response=undef");
|
||||
HMCCU_Log ($io_hash, 2, "Error during HTTP request: $err", undef);
|
||||
HMCCU_Trace ($cl_hash, 2, $fnc, "Response=$response");
|
||||
return undef;
|
||||
}
|
||||
|
||||
if ($mode == 1) {
|
||||
@ -5904,18 +6015,20 @@ sub HMCCU_HMCommandNB ($$$)
|
||||
|
||||
my $io_hash = HMCCU_GetHash ($cl_hash);
|
||||
my $ccureqtimeout = AttrVal ($io_hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST);
|
||||
my $url = "http://".$io_hash->{host}.":8181/tclrega.exe";
|
||||
my $url = HMCCU_BuildURL ($io_hash, 'rega');
|
||||
|
||||
HMCCU_Trace ($cl_hash, 2, $fnc, "URL=$url");
|
||||
|
||||
if (defined ($cbfunc)) {
|
||||
my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST",
|
||||
callback => $cbfunc, devhash => $cl_hash };
|
||||
$param->{sslargs} = { SSL_verify_mode => 0 };
|
||||
HttpUtils_NonblockingGet ($param);
|
||||
}
|
||||
else {
|
||||
my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST",
|
||||
callback => \&HMCCU_HMCommandCB, devhash => $cl_hash };
|
||||
$param->{sslargs} = { SSL_verify_mode => 0 };
|
||||
HttpUtils_NonblockingGet ($param);
|
||||
}
|
||||
}
|
||||
@ -5957,6 +6070,8 @@ sub HMCCU_HMScriptExt ($$$)
|
||||
return HMCCU_LogError ($hash, 2, "CCU host name not defined") if (!exists ($hash->{host}));
|
||||
my $host = $hash->{host};
|
||||
|
||||
my $ccureqtimeout = AttrVal ($hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST);
|
||||
|
||||
if ($hmscript =~ /^!(.*)$/) {
|
||||
# Internal script
|
||||
$scrname = $1;
|
||||
@ -6004,19 +6119,25 @@ sub HMCCU_HMScriptExt ($$$)
|
||||
}
|
||||
|
||||
# Execute script on CCU
|
||||
my $url = "http://".$host.":8181/tclrega.exe";
|
||||
my $ua = new LWP::UserAgent ();
|
||||
my $response = $ua->post($url, Content => $code);
|
||||
if ($response->is_success ()) {
|
||||
my $output = $response->content;
|
||||
my $url = HMCCU_BuildURL ($hash, 'rega');
|
||||
my $param = { url => $url, timeout => $ccureqtimeout, data => $code, method => "POST" };
|
||||
$param->{sslargs} = { SSL_verify_mode => 0 };
|
||||
my ($err, $response) = HttpUtils_BlockingGet ($param);
|
||||
|
||||
# my $ua = new LWP::UserAgent ();
|
||||
# my $response = $ua->post($url, Content => $code);
|
||||
# if ($response->is_success ()) {
|
||||
# my $output = $response->content;
|
||||
if ($err eq '') {
|
||||
my $output = $response;
|
||||
$output =~ s/<xml>.*<\/xml>//;
|
||||
$output =~ s/\r//g;
|
||||
return $output;
|
||||
}
|
||||
else {
|
||||
my $msg = $response->status_line();
|
||||
Log3 $name, 2, "HMCCU: HMScript failed. $msg";
|
||||
return "ERROR: HMScript failed. $msg";
|
||||
# my $msg = $response->status_line();
|
||||
HMCCU_Log ($hash, 2, "HMScript failed. $err", undef);
|
||||
return "ERROR: HMScript failed. $err";
|
||||
}
|
||||
}
|
||||
|
||||
@ -6027,9 +6148,28 @@ sub HMCCU_HMScriptExt ($$$)
|
||||
sub HMCCU_BulkUpdate ($$$$)
|
||||
{
|
||||
my ($hash, $reading, $orgval, $subval) = @_;
|
||||
|
||||
my $excl = AttrVal ($hash->{NAME}, 'substexcl', '');
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
my $excl = AttrVal ($name, 'substexcl', '');
|
||||
#
|
||||
# For later use: Suppress reading update
|
||||
#
|
||||
# my $suppress = AttrVal ($name, 'ccusuppress', '');
|
||||
#
|
||||
# if ($suppress ne '') {
|
||||
# my $ct = time();
|
||||
# my @srules = split (";", $suppress);
|
||||
#
|
||||
# foreach my $sr (@srules) {
|
||||
# my ($rnexp, $to) = split (":", $sr);
|
||||
# next if (!defined ($to));
|
||||
# if ($reading =~ /$rnexp/) {
|
||||
# my $rt = ReadingsTimestamp ($name, $reading, '');
|
||||
# return if ($rt ne '' && $ct-time_str2num($rt) < $to);
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
readingsBulkUpdate ($hash, $reading, ($excl ne '' && $reading =~ /$excl/ ? $orgval : $subval));
|
||||
}
|
||||
|
||||
@ -6664,7 +6804,8 @@ sub HMCCU_RPCGetConfig ($$$$)
|
||||
}
|
||||
}
|
||||
else {
|
||||
my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url');
|
||||
# my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url');
|
||||
my $url = HMCCU_BuildURL ($hmccu_hash, $int);
|
||||
return (-9, '') if (!defined ($url));
|
||||
HMCCU_Trace ($hash, 2, $fnc, "Method=$method Addr=$addr Port=$port");
|
||||
my $client = RPC::XML::Client->new ($url);
|
||||
@ -6793,7 +6934,8 @@ sub HMCCU_RPCSetConfig ($$$)
|
||||
}
|
||||
}
|
||||
else {
|
||||
my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url');
|
||||
# my $url = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'url');
|
||||
my $url = HMCCU_BuildURL ($hmccu_hash, $int);
|
||||
return -9 if (!defined ($url));
|
||||
my $client = RPC::XML::Client->new ($url);
|
||||
$res = $client->simple_request ("putParamset", $addr, "MASTER", $parref);
|
||||
@ -7023,6 +7165,51 @@ sub HMCCU_GetTimeSpec ($)
|
||||
return ($s-$cs);
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Build ReGa or RPC client URL
|
||||
# Parameter backend specifies type of URL, 'rega' or name or port of
|
||||
# RPC interface.
|
||||
# Return empty string on error.
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_BuildURL ($$)
|
||||
{
|
||||
my ($hash, $backend) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
my $url = '';
|
||||
my $username = '';
|
||||
my $password = '';
|
||||
my ($erruser, $encuser) = getKeyValue ($name."_username");
|
||||
my ($errpass, $encpass) = getKeyValue ($name."_password");
|
||||
if (!defined ($erruser) && !defined ($errpass) && defined ($encuser) && defined ($encpass)) {
|
||||
$username = HMCCU_Decrypt ($encuser);
|
||||
$password = HMCCU_Decrypt ($encpass);
|
||||
}
|
||||
my $auth = ($username ne '' && $password ne '') ? "$username:$password".'@' : '';
|
||||
|
||||
if ($backend eq 'rega') {
|
||||
$url = $hash->{prot}."://$auth".$hash->{host}.":".
|
||||
$HMCCU_REGA_PORT{$hash->{prot}}."/tclrega.exe";
|
||||
}
|
||||
else {
|
||||
$url = HMCCU_GetRPCServerInfo ($hash, $backend, 'url');
|
||||
if (defined ($url)) {
|
||||
if (exists ($HMCCU_RPC_SSL{$backend})) {
|
||||
my $p = $hash->{prot} eq 'https' ? '4' : '';
|
||||
$url =~ s/^http:\/\//$hash->{prot}:\/\/$auth/;
|
||||
$url =~ s/:([0-9]+)/:$p$1/;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$url = '';
|
||||
}
|
||||
}
|
||||
|
||||
HMCCU_Log ($hash, 4, "Build URL = $url", undef);
|
||||
return $url;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Calculate special readings. Requires hash of client device, channel
|
||||
# number and datapoint. Supported functions:
|
||||
@ -7184,6 +7371,73 @@ sub HMCCU_CalculateReading ($$)
|
||||
return @result;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Encrypt string with FHEM unique ID
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_Encrypt ($)
|
||||
{
|
||||
my ($istr) = @_;
|
||||
my $ostr = '';
|
||||
|
||||
my $id = getUniqueId();
|
||||
return '' if (!defined ($id) || $id eq '');
|
||||
|
||||
my $key = $id;
|
||||
foreach my $c (split //, $istr) {
|
||||
my $k = chop($key);
|
||||
if ($k eq '') {
|
||||
$key = $id;
|
||||
$k = chop($key);
|
||||
}
|
||||
$ostr .= sprintf ("%.2x",ord($c)^ord($k));
|
||||
}
|
||||
|
||||
return $ostr;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Decrypt string with FHEM unique ID
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_Decrypt ($)
|
||||
{
|
||||
my ($istr) = @_;
|
||||
my $ostr = '';
|
||||
|
||||
my $id = getUniqueId();
|
||||
return '' if (!defined ($id) || $id eq '');
|
||||
|
||||
my $key = $id;
|
||||
for my $c (map { pack('C', hex($_)) } ($istr =~ /(..)/g)) {
|
||||
my $k = chop($key);
|
||||
if ($k eq '') {
|
||||
$key = $id;
|
||||
$k = chop($key);
|
||||
}
|
||||
$ostr .= chr(ord($c)^ord($k));
|
||||
}
|
||||
|
||||
return $ostr;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Delete readings matching regular expression.
|
||||
# Default for rnexp is .*
|
||||
# Readings 'state' and 'control' are ignored.
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_DeleteReadings ($$)
|
||||
{
|
||||
my ($hash, $rnexp) = @_;
|
||||
|
||||
$rnexp = '.*' if (!defined ($rnexp));
|
||||
my @readlist = keys %{$hash->{READINGS}};
|
||||
foreach my $rd (@readlist) {
|
||||
delete ($hash->{READINGS}{$rd}) if ($rd ne 'state' && $rd ne 'control' && $rd =~ /$rnexp/);
|
||||
}
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Encode command string for e-paper display
|
||||
#
|
||||
@ -7380,7 +7634,8 @@ sub HMCCU_GetDutyCycle ($)
|
||||
|
||||
foreach my $port (@rpcports) {
|
||||
next if ($port != 2001 && $port != 2010);
|
||||
my $url = HMCCU_GetRPCServerInfo ($hash, $port, 'url');
|
||||
# my $url = HMCCU_GetRPCServerInfo ($hash, $port, 'url');
|
||||
my $url = HMCCU_BuildURL ($hash, $port);
|
||||
next if (!defined ($url));
|
||||
my $rpcclient = RPC::XML::Client->new ($url);
|
||||
my $response = $rpcclient->simple_request ("listBidcosInterfaces");
|
||||
@ -7445,6 +7700,23 @@ sub HMCCU_TCPConnect ($$)
|
||||
return '';
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Generate a 6 digit Id from last 2 segments of IP address
|
||||
######################################################################
|
||||
|
||||
sub HMCCU_GetIdFromIP ($$)
|
||||
{
|
||||
my ($ip, $default) = @_;
|
||||
|
||||
my @ipseg = split (/\./, $ip);
|
||||
if (scalar (@ipseg) == 4) {
|
||||
return sprintf ("%03d%03d", $ipseg[2], $ipseg[3]);
|
||||
}
|
||||
else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Resolve hostname.
|
||||
# Return value defip if hostname can't be resolved.
|
||||
@ -7806,15 +8078,16 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
|
||||
<a name="HMCCUdefine"></a>
|
||||
<b>Define</b><br/><br/>
|
||||
<ul>
|
||||
<code>define <name> HMCCU <HostOrIP> [<ccu-number>] [waitforccu=<timeout>]
|
||||
<code>define <name> HMCCU [<Protocol>://]<HostOrIP> [<ccu-number>] [waitforccu=<timeout>]
|
||||
[ccudelay=<delay>] [delayedinit=<delay>]</code>
|
||||
<br/><br/>
|
||||
Example:<br/>
|
||||
<code>define myccu HMCCU 192.168.1.10 ccudelay=180</code>
|
||||
<code>define myccu HMCCU https://192.168.1.10 ccudelay=180</code>
|
||||
<br/><br/>
|
||||
The parameter <i>HostOrIP</i> is the hostname or IP address of a Homematic CCU2 or CCU3. If you have
|
||||
more than one CCU you can specifiy a unique CCU number with parameter <i>ccu-number</i>. With
|
||||
option <i>waitforccu</i> HMCCU will wait for the specified time if CCU is not reachable.
|
||||
The parameter <i>HostOrIP</i> is the hostname or IP address of a Homematic CCU2 or CCU3. Optionally
|
||||
the <i>protocol</i> 'http' or 'https' can be specified. Default protocol is 'http'.<br/>
|
||||
If you have more than one CCU you can specifiy a unique CCU number with parameter <i>ccu-number</i>.
|
||||
With option <i>waitforccu</i> HMCCU will wait for the specified time if CCU is not reachable.
|
||||
Parameter <i>timeout</i> should be a multiple of 20 in seconds. Warning: This option will
|
||||
block the start of FHEM for <i>timeout</i> seconds.<br/>
|
||||
The option <i>ccudelay</i> specifies the time for delayed initialization of CCU environment if
|
||||
@ -7846,6 +8119,15 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
|
||||
<ul>
|
||||
<li><b>set <name> ackmessages</b><br/>
|
||||
Acknowledge device was unreachable messages in CCU.
|
||||
</li><br/>
|
||||
<li><b>set <name> authentication [<username> <password>]</b><br/>
|
||||
Set credentials for CCU authentication. Authentication must be activated by setting
|
||||
attribute ccuflags to 'authenticate'.<br/>
|
||||
When executing this command without arguments credentials are deleted.
|
||||
</li><br/>
|
||||
<li><b>set <name> clear [<reading-exp>]</b><br/>
|
||||
Delete readings matching specified reading name expression. Default expression is '.*'.
|
||||
Readings 'state' and 'control' are not deleted.
|
||||
</li><br/>
|
||||
<li><b>set <name> cleardefaults</b><br/>
|
||||
Clear default attributes imported from file.
|
||||
@ -7877,6 +8159,9 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
|
||||
<li><b>set <name> importdefaults <filename></b><br/>
|
||||
Import default attributes from file.
|
||||
</li><br/>
|
||||
<li><b>set <name> initialize</b><br/>
|
||||
Initialize I/O device if state of CCU is unreachable.
|
||||
</li><br/>
|
||||
<li><b>set <name> rpcregister [{all | <interface>}]</b><br/>
|
||||
Register RPC servers at CCU.
|
||||
</li><br/>
|
||||
@ -7899,6 +8184,9 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
|
||||
<li><b>get <name> aggregation {<rule>|all}</b><br/>
|
||||
Process aggregation rule defined with attribute ccuaggregate.
|
||||
</li><br/>
|
||||
<li><b>get <name> ccumsg {service|alarm}</b><br/>
|
||||
Query active service or alarm messages from CCU. Generate FHEM event for each message.
|
||||
</li><br/>
|
||||
<li><b>get <name> configdesc {<device>|<channel>}</b><br/>
|
||||
Get configuration parameter description of CCU device or channel (similar
|
||||
to device settings in CCU). Not every CCU device or channel provides a configuration
|
||||
|
@ -4,9 +4,9 @@
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
# Version 4.3.005
|
||||
# Version 4.3.006
|
||||
#
|
||||
# (c) 2018 zap (zap01 <at> t-online <dot> de)
|
||||
# (c) 2019 zap (zap01 <at> t-online <dot> de)
|
||||
#
|
||||
######################################################################
|
||||
# Client device for Homematic channels.
|
||||
@ -208,8 +208,8 @@ sub HMCCUCHN_Set ($@)
|
||||
my $rocmds = "clear config defaults:noArg";
|
||||
|
||||
# Get I/O device, check device state
|
||||
return HMCCU_SetError ($hash, -19) if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending');
|
||||
return HMCCU_SetError ($hash, -3) if (!defined ($hash->{IODev}));
|
||||
return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' ||
|
||||
!defined ($hash->{IODev}));
|
||||
return undef if ($hash->{statevals} eq 'readonly' && $opt ne '?' &&
|
||||
$opt !~ /^(clear|config|defaults)$/);
|
||||
|
||||
@ -403,11 +403,8 @@ sub HMCCUCHN_Set ($@)
|
||||
}
|
||||
elsif ($opt eq 'clear') {
|
||||
my $rnexp = shift @$a;
|
||||
$rnexp = '.*' if (!defined ($rnexp));
|
||||
my @readlist = keys %{$hash->{READINGS}};
|
||||
foreach my $rd (@readlist) {
|
||||
delete ($hash->{READINGS}{$rd}) if ($rd ne 'state' && $rd ne 'control' && $rd =~ /$rnexp/);
|
||||
}
|
||||
HMCCU_DeleteReadings ($hash, $rnexp);
|
||||
return HMCCU_SetState ($hash, "OK");
|
||||
}
|
||||
elsif ($opt eq 'config') {
|
||||
return HMCCU_SetError ($hash, "Usage: set $name config [device] {parameter}={value} [...]")
|
||||
@ -460,7 +457,8 @@ sub HMCCUCHN_Get ($@)
|
||||
my $name = shift @$a;
|
||||
my $opt = shift @$a;
|
||||
|
||||
return HMCCU_SetError ($hash, -3) if (!defined ($hash->{IODev}));
|
||||
return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' ||
|
||||
!defined ($hash->{IODev}));
|
||||
|
||||
my $disable = AttrVal ($name, "disable", 0);
|
||||
return undef if ($disable == 1);
|
||||
@ -794,16 +792,33 @@ sub HMCCUCHN_Get ($@)
|
||||
If set to 1 values read from CCU will be stored as readings. Default is 1.
|
||||
</li><br/>
|
||||
<li><b>ccureadingfilter <filter-rule[;...]></b><br/>
|
||||
Only datapoints matching specified expression are stored as readings.<br/>
|
||||
Only datapoints matching specified expression <i>RegExp</i> are stored as readings.<br/>
|
||||
Syntax for <i>filter-rule</i> is either:<br/>
|
||||
[N:]{<channel-name>|<channel-number>}!<RegExp> or:<br/>
|
||||
[N:]{<channel-name>}!RegExp> or:<br/>
|
||||
[N:][<channel-number>.]<RegExp><br/>
|
||||
If <i>channel-name</i> or <i>channel-number</i> is specified the following rule
|
||||
applies only to this channel.
|
||||
By default all datapoints will be stored as readings. Attribute ccudef-readingfilter
|
||||
of I/O device will be checked before this attribute.<br/>
|
||||
applies only to this channel.<br/>
|
||||
If a rule starts with 'N:' the filter is negated which means that a reading is
|
||||
stored if rule doesn't match.
|
||||
stored if rule doesn't match.<br/>
|
||||
The following table describes the dependencies between this attribute and attribute
|
||||
ccudef-readingfilter in I/O device. The filtering of readings depends on which attribute
|
||||
is set.<br/>
|
||||
<table>
|
||||
<tr><th>ccureadingfilter<br/>Device</th><th>ccudef-readingfilter<br/>I/O Device</th><th>Update Readings/Datapoints</th></tr>
|
||||
<tr><td>not set</td><td>not set</td><td>all readings</td></tr>
|
||||
<tr><td>not set</td><td>set</td><td>only readings from ccudef-readingfilter</td></tr>
|
||||
<tr><td>set</td><td>not set</td><td>only readings from ccureadingfilter</td></tr>
|
||||
<tr><td>set</td><td>set</td><td>both readings from ccureadingfilter and ccudef-readingfilter</td></tr>
|
||||
</table>
|
||||
So if ccudef-readingfilter is set in I/O device one must also set ccureadingfilter to
|
||||
get updates for additional, device specific readings. Please keep in mind, that readings updates
|
||||
are also affected by attributes event-on-change-reading and event-on-update-reading.<br/><br/>
|
||||
Examples:<br/>
|
||||
<code>
|
||||
attr mydev ccureadingfilter .*<br/>
|
||||
attr mydev ccureadingfilter 1.(^ACTUAL|CONTROL|^SET_TEMP);(^WINDOW_OPEN|^VALVE)<br/>
|
||||
attr mydev ccureadingfilter MyBlindChannel!^LEVEL$<br/>
|
||||
</code>
|
||||
</li><br/>
|
||||
<li><b>ccureadingformat {address[lc] | name[lc] | datapoint[lc]}</b><br/>
|
||||
Set format of reading names. Default for virtual device groups is 'name'. The default for all
|
||||
|
@ -4,9 +4,9 @@
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
# Version 4.3.006
|
||||
# Version 4.3.008
|
||||
#
|
||||
# (c) 2018 zap (zap01 <at> t-online <dot> de)
|
||||
# (c) 2019 zap (zap01 <at> t-online <dot> de)
|
||||
#
|
||||
######################################################################
|
||||
# Client device for Homematic devices.
|
||||
@ -239,7 +239,6 @@ sub HMCCUDEV_InitDevice ($$)
|
||||
# Group specified by CCU virtual group name
|
||||
@devlist = HMCCU_GetGroupMembers ($hmccu_hash, $gdname);
|
||||
$gdcount = scalar (@devlist);
|
||||
return 5 if ($gdcount == 0);
|
||||
}
|
||||
|
||||
return 3 if ($gdcount == 0);
|
||||
@ -345,8 +344,8 @@ sub HMCCUDEV_Set ($@)
|
||||
my $rocmds = "clear config defaults:noArg";
|
||||
|
||||
# Get I/O device, check device state
|
||||
return HMCCU_SetError ($hash, -19) if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending');
|
||||
return HMCCU_SetError ($hash, -3) if (!defined ($hash->{IODev}));
|
||||
return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' ||
|
||||
!defined ($hash->{IODev}));
|
||||
my $hmccu_hash = $hash->{IODev};
|
||||
my $hmccu_name = $hmccu_hash->{NAME};
|
||||
|
||||
@ -580,11 +579,8 @@ sub HMCCUDEV_Set ($@)
|
||||
}
|
||||
elsif ($opt eq 'clear') {
|
||||
my $rnexp = shift @$a;
|
||||
$rnexp = '.*' if (!defined ($rnexp));
|
||||
my @readlist = keys %{$hash->{READINGS}};
|
||||
foreach my $rd (@readlist) {
|
||||
delete ($hash->{READINGS}{$rd}) if ($rd ne 'state' && $rd ne 'control' && $rd =~ /$rnexp/);
|
||||
}
|
||||
HMCCU_DeleteReadings ($hash, $rnexp);
|
||||
return HMCCU_SetState ($hash, "OK");
|
||||
}
|
||||
elsif ($opt eq 'config') {
|
||||
return HMCCU_SetError ($hash, "Usage: set $name config [{channel-number}] {parameter}={value} [...]")
|
||||
@ -645,7 +641,8 @@ sub HMCCUDEV_Get ($@)
|
||||
my $opt = shift @$a;
|
||||
|
||||
# Get I/O device
|
||||
return HMCCU_SetError ($hash, -3) if (!defined ($hash->{IODev}));
|
||||
return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' ||
|
||||
!defined ($hash->{IODev}));
|
||||
my $hmccu_hash = $hash->{IODev};
|
||||
my $hmccu_name = $hmccu_hash->{NAME};
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
# Version 1.5
|
||||
# Version 1.6
|
||||
#
|
||||
# Subprocess based RPC Server module for HMCCU.
|
||||
#
|
||||
@ -35,7 +35,7 @@ use SetExtensions;
|
||||
######################################################################
|
||||
|
||||
# HMCCURPC version
|
||||
my $HMCCURPCPROC_VERSION = '1.5';
|
||||
my $HMCCURPCPROC_VERSION = '1.6';
|
||||
|
||||
# Maximum number of events processed per call of Read()
|
||||
my $HMCCURPCPROC_MAX_EVENTS = 100;
|
||||
@ -107,6 +107,7 @@ sub HMCCURPCPROC_Initialize ($);
|
||||
sub HMCCURPCPROC_Define ($$);
|
||||
sub HMCCURPCPROC_InitDevice ($$);
|
||||
sub HMCCURPCPROC_Undef ($$);
|
||||
sub HMCCURPCPROC_DelayedShutdown ($);
|
||||
sub HMCCURPCPROC_Shutdown ($);
|
||||
sub HMCCURPCPROC_Attr ($@);
|
||||
sub HMCCURPCPROC_Set ($@);
|
||||
@ -134,7 +135,7 @@ sub HMCCURPCPROC_RPCServerStopped ($);
|
||||
sub HMCCURPCPROC_SendRequest ($@);
|
||||
sub HMCCURPCPROC_SetRPCState ($$$$);
|
||||
sub HMCCURPCPROC_StartRPCServer ($);
|
||||
sub HMCCURPCPROC_StopRPCServer ($);
|
||||
sub HMCCURPCPROC_StopRPCServer ($$);
|
||||
sub HMCCURPCPROC_TerminateProcess ($);
|
||||
|
||||
# Helper functions
|
||||
@ -197,6 +198,7 @@ sub HMCCURPCPROC_Initialize ($)
|
||||
$hash->{ReadFn} = "HMCCURPCPROC_Read";
|
||||
$hash->{AttrFn} = "HMCCURPCPROC_Attr";
|
||||
$hash->{ShutdownFn} = "HMCCURPCPROC_Shutdown";
|
||||
$hash->{DelayedShutdownFn} = "HMCCURPCPROC_DelayedShutdown";
|
||||
|
||||
$hash->{parseParams} = 1;
|
||||
|
||||
@ -231,17 +233,32 @@ sub HMCCURPCPROC_Define ($$)
|
||||
$hmccu_hash = $defs{$ioname};
|
||||
if (scalar (@$a) < 4) {
|
||||
$hash->{host} = $hmccu_hash->{host};
|
||||
$hash->{prot} = $hmccu_hash->{prot};
|
||||
$iface = $$a[2];
|
||||
}
|
||||
else {
|
||||
$hash->{host} = $$a[2];
|
||||
if ($$a[2] =~ /^(https?):\/\/(.+)/) {
|
||||
$hash->{prot} = $1;
|
||||
$hash->{host} = $2;
|
||||
}
|
||||
else {
|
||||
$hash->{prot} = 'http';
|
||||
$hash->{host} = $$a[2];
|
||||
}
|
||||
$iface = $$a[3];
|
||||
}
|
||||
$rpcip = HMCCU_ResolveName ($hash->{host}, 'N/A');
|
||||
}
|
||||
else {
|
||||
return $usage if (scalar (@$a) < 4);
|
||||
$hash->{host} = $$a[2];
|
||||
if ($$a[2] =~ /^(https?):\/\/(.+)/) {
|
||||
$hash->{prot} = $1;
|
||||
$hash->{host} = $2;
|
||||
}
|
||||
else {
|
||||
$hash->{prot} = 'http';
|
||||
$hash->{host} = $$a[2];
|
||||
}
|
||||
$iface = $$a[3];
|
||||
$rpcip = HMCCU_ResolveName ($hash->{host}, 'N/A');
|
||||
|
||||
@ -250,12 +267,8 @@ sub HMCCURPCPROC_Define ($$)
|
||||
my $dh = $defs{$d};
|
||||
next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME}));
|
||||
next if ($dh->{TYPE} ne 'HMCCU');
|
||||
|
||||
# The following call will fail during FHEM start if CCU is not ready
|
||||
my $ifhost = HMCCU_GetRPCServerInfo ($dh, $iface, 'host');
|
||||
next if (!defined ($ifhost));
|
||||
if ($dh->{host} eq $hash->{host} || $ifhost eq $hash->{host} || $ifhost eq $rpcip) {
|
||||
$hmccu_hash = $dh;
|
||||
if ($dh->{ccuip} eq $rpcip) {
|
||||
$hmccu_hash = $dh;
|
||||
last;
|
||||
}
|
||||
}
|
||||
@ -338,10 +351,11 @@ sub HMCCURPCPROC_InitDevice ($$) {
|
||||
|
||||
# Get unique ID for RPC server: last 2 segments of local IP address
|
||||
# Do not append random digits because of https://forum.fhem.de/index.php/topic,83544.msg797146.html#msg797146
|
||||
my @ipseg = split (/\./, $dev_hash->{hmccu}{localaddr});
|
||||
return 3 if (scalar (@ipseg) != 4);
|
||||
$dev_hash->{rpcid} = sprintf ("%03d%03d", $ipseg[2], $ipseg[3]);
|
||||
|
||||
my $id1 = HMCCU_GetIdFromIP ($dev_hash->{hmccu}{localaddr}, '');
|
||||
my $id2 = HMCCU_GetIdFromIP ($hmccu_hash->{ccuip}, '');
|
||||
return 3 if ($id1 eq '' || $id2 eq '');
|
||||
$dev_hash->{rpcid} = $id1.$id2;
|
||||
|
||||
# Set I/O device and store reference for RPC device in I/O device
|
||||
my $ioname = $hmccu_hash->{NAME};
|
||||
return 2 if (!HMCCU_AssignIODevice ($dev_hash, $ioname, $ifname));
|
||||
@ -380,10 +394,11 @@ sub HMCCURPCPROC_Undef ($$)
|
||||
my $ifname = $hash->{rpcinterface};
|
||||
|
||||
# Shutdown RPC server
|
||||
HMCCURPCPROC_Shutdown ($hash);
|
||||
HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2);
|
||||
|
||||
# Delete RPC device name in I/O device
|
||||
if (exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{device}) &&
|
||||
if (exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}) &&
|
||||
exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{device}) &&
|
||||
$hmccu_hash->{hmccu}{interfaces}{$ifname}{device} eq $name) {
|
||||
delete $hmccu_hash->{hmccu}{interfaces}{$ifname}{device};
|
||||
}
|
||||
@ -391,6 +406,30 @@ sub HMCCURPCPROC_Undef ($$)
|
||||
return undef;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Delayed shutdown FHEM
|
||||
######################################################################
|
||||
|
||||
sub HMCCURPCPROC_DelayedShutdown ($)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
my $delay = max (AttrVal ("global", "maxShutdownDelay", 10)-2, 0);
|
||||
|
||||
# Shutdown RPC server
|
||||
if (!exists ($hash->{hmccu}{delayedShutdown})) {
|
||||
$hash->{hmccu}{delayedShutdown} = 1;
|
||||
Log3 $name, 1, "HMCCURPCPROC: [$name] Graceful shutdown";
|
||||
HMCCURPCPROC_StopRPCServer ($hash, $delay);
|
||||
}
|
||||
else {
|
||||
Log3 $name, 1, "HMCCURPCPROC: [$name] Shutdown already in progress";
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Shutdown FHEM
|
||||
######################################################################
|
||||
@ -398,9 +437,15 @@ sub HMCCURPCPROC_Undef ($$)
|
||||
sub HMCCURPCPROC_Shutdown ($)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
# Shutdown RPC server
|
||||
HMCCURPCPROC_StopRPCServer ($hash);
|
||||
if (!exists ($hash->{hmccu}{delayedShutdown})) {
|
||||
Log3 $name, 1, "HMCCURPCPROC: [$name] Immediate shutdown";
|
||||
HMCCURPCPROC_StopRPCServer ($hash, 0);
|
||||
}
|
||||
|
||||
# Remove all internal timers
|
||||
RemoveInternalTimer ($hash);
|
||||
|
||||
return undef;
|
||||
@ -511,7 +556,7 @@ sub HMCCURPCPROC_Set ($@)
|
||||
}
|
||||
elsif ($action eq 'off') {
|
||||
$hmccu_hash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC';
|
||||
HMCCURPCPROC_StopRPCServer ($hash);
|
||||
HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2);
|
||||
}
|
||||
|
||||
return undef;
|
||||
@ -565,7 +610,11 @@ sub HMCCURPCPROC_Get ($@)
|
||||
$result .= "--------------------------\n";
|
||||
my $sid = defined ($hash->{hmccu}{rpc}{pid}) ? sprintf ("%5d", $hash->{hmccu}{rpc}{pid}) : "N/A ";
|
||||
my $sname = sprintf ("%-10s", $clkey);
|
||||
$result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n";
|
||||
my $cbport = defined ($hash->{hmccu}{rpc}{cbport}) ? $hash->{hmccu}{rpc}{cbport} : "N/A";
|
||||
my $addr = defined ($hash->{hmccu}{localaddr}) ? $hash->{hmccu}{localaddr} : "N/A";
|
||||
$result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n\n";
|
||||
$result .= "Local address = $addr\n";
|
||||
$result .= "Callback port = $cbport\n";
|
||||
return $result;
|
||||
}
|
||||
else {
|
||||
@ -760,7 +809,8 @@ sub HMCCURPCPROC_IsRPCStateBlocking ($)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
|
||||
return ($hash->{RPCState} eq "running" || $hash->{RPCState} eq "inactive") ? 0 : 1;
|
||||
return (exists ($hash->{RPCState}) &&
|
||||
($hash->{RPCState} eq "running" || $hash->{RPCState} eq "inactive")) ? 0 : 1;
|
||||
}
|
||||
|
||||
######################################################################
|
||||
@ -950,7 +1000,7 @@ sub HMCCURPCPROC_ProcessEvent ($$)
|
||||
# Input: TO|clkey|DiffTime
|
||||
# Output: TO, clkey, Port, DiffTime
|
||||
#
|
||||
if ($evttimeout > 0 && $t[0] > $evttimeout) {
|
||||
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' && $hash->{rpcport} == $defPort) {
|
||||
@ -1021,7 +1071,8 @@ sub HMCCURPCPROC_RegisterCallback ($$)
|
||||
}
|
||||
|
||||
my $cburl = HMCCU_GetRPCCallbackURL ($hmccu_hash, $localaddr, $hash->{hmccu}{rpc}{cbport}, $clkey, $port);
|
||||
my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url');
|
||||
# my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url');
|
||||
my $clurl = HMCCU_BuildURL ($hmccu_hash, $port);
|
||||
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));
|
||||
|
||||
@ -1069,7 +1120,8 @@ sub HMCCURPCPROC_DeRegisterCallback ($$)
|
||||
$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 '');
|
||||
# $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url') if ($clurl eq '');
|
||||
$clurl = HMCCU_BuildURL ($hmccu_hash, $port) 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";
|
||||
@ -1378,6 +1430,9 @@ sub HMCCURPCPROC_RPCServerStopped ($)
|
||||
|
||||
RemoveInternalTimer ($hash);
|
||||
DoTrigger ($name, "RPC server $clkey stopped");
|
||||
|
||||
# Inform FHEM that instance can be shut down
|
||||
CancelDelayedShutdown ($name) if (exists ($hash->{hmccu}{delayedShutdown}));
|
||||
}
|
||||
|
||||
######################################################################
|
||||
@ -1535,13 +1590,16 @@ sub HMCCURPCPROC_Housekeeping ($)
|
||||
|
||||
######################################################################
|
||||
# Stop RPC server processes.
|
||||
# If function is called by Shutdown, parameter wait must be 0
|
||||
######################################################################
|
||||
|
||||
sub HMCCURPCPROC_StopRPCServer ($)
|
||||
sub HMCCURPCPROC_StopRPCServer ($$)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
my ($hash, $wait) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid};
|
||||
|
||||
$wait = $HMCCURPCPROC_INIT_INTERVAL2 if (!defined ($wait));
|
||||
|
||||
if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) {
|
||||
Log3 $name, 1, "HMCCURPCPROC: [$name] Stopping RPC server $clkey";
|
||||
@ -1556,8 +1614,14 @@ sub HMCCURPCPROC_StopRPCServer ($)
|
||||
|
||||
# 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);
|
||||
if ($wait > 0) {
|
||||
Log3 $name, 2, "HMCCURPCPROC: [$name] Scheduling cleanup in $wait seconds";
|
||||
InternalTimer (gettimeofday()+$wait, "HMCCURPCPROC_Housekeeping", $hash, 0);
|
||||
}
|
||||
else {
|
||||
Log3 $name, 2, "HMCCURPCPROC: [$name] Cleaning up immediately";
|
||||
HMCCURPCPROC_Housekeeping ($hash);
|
||||
}
|
||||
|
||||
# Give process the chance to terminate
|
||||
sleep (1);
|
||||
@ -1587,12 +1651,14 @@ sub HMCCURPCPROC_SendRequest ($@)
|
||||
|
||||
if (HMCCU_IsRPCType ($hmccu_hash, $port, 'A')) {
|
||||
# Use XMLRPC
|
||||
my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url');
|
||||
# my $clurl = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'url');
|
||||
my $clurl = HMCCU_BuildURL ($hmccu_hash, $port);
|
||||
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);
|
||||
my $rpcclient = RPC::XML::Client->new ($clurl, useragent => [
|
||||
ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 } ]);
|
||||
$rc = $rpcclient->simple_request ($request, @param);
|
||||
Log3 $name, 2, "HMCCURPCPROC: [$name] RPC request error ".$RPC::XML::ERROR if (!defined ($rc));
|
||||
}
|
||||
|
@ -4,19 +4,11 @@
|
||||
#
|
||||
# $Id$
|
||||
#
|
||||
# Version 4.5
|
||||
# Version 4.6
|
||||
#
|
||||
# Configuration parameters for HomeMatic devices.
|
||||
#
|
||||
# (c) 2018 by zap (zap01 <at> t-online <dot> de)
|
||||
#
|
||||
# Datapoints LOWBAT, LOW_BAT, UNREACH, ERROR.*, SABOTAGE and FAULT.*
|
||||
# must not be specified in attribute ccureadingfilter. They are always
|
||||
# stored as readings.
|
||||
# Datapoints LOWBAT, LOW_BAT and UNREACH must not be specified in
|
||||
# attribute substitute because they are substituted by default.
|
||||
# See also documentation of attributes ccudef-readingname and
|
||||
# ccudef-substitute in module HMCCU.
|
||||
# (c) 2019 by zap (zap01 <at> t-online <dot> de)
|
||||
#
|
||||
#########################################################################
|
||||
|
||||
@ -554,7 +546,7 @@ use vars qw(%HMCCU_SCRIPTS);
|
||||
},
|
||||
"HMIP-PSM" => {
|
||||
_description => "Steckdose mit Energiemessung IP",
|
||||
ccureadingfilter => "(STATE|CURRENT|^ENERGY_COUNTER\$|POWER)",
|
||||
ccureadingfilter => "3.STATE;6.(CURRENT|^ENERGY_COUNTER\$|POWER)",
|
||||
controldatapoint => "3.STATE",
|
||||
statedatapoint => "3.STATE",
|
||||
statevals => "on:true,off:false",
|
||||
@ -1473,25 +1465,21 @@ if(oTmpArray) {
|
||||
object oTmp = dom.GetObject(sTmp);
|
||||
if (oTmp) {
|
||||
if(oTmp.IsTypeOf(OT_ALARMDP) && (oTmp.AlState() == asOncoming)) {
|
||||
boolean collect = true;
|
||||
object trigDP = dom.GetObject(oTmp.AlTriggerDP());
|
||||
object och = dom.GetObject((trigDP.Channel()));
|
||||
object odev = dom.GetObject((och.Device()));
|
||||
var ival = trigDP.Value();
|
||||
time sftime = oTmp.AlOccurrenceTime();
|
||||
time sltime = oTmp.LastTriggerTime();
|
||||
var sdesc = trigDP.HSSID();
|
||||
time sftime = oTmp.AlOccurrenceTime(); ! erste Meldezeit
|
||||
time sltime = oTmp.LastTriggerTime();!letze Meldezeit
|
||||
var sdesc = trigDP.HssType();
|
||||
var sserial = odev.Address();
|
||||
string sAlarmMessage = web.webKeyFromStringTable(sdesc.Name());
|
||||
if(!sAlarmMessage.Length()) {
|
||||
sAlarmMessage = sdesc;
|
||||
}
|
||||
var sname = odev.Name();
|
||||
WriteLine(sftime.Format("%d.%m.%y %H:%M") # ";" # sltime.Format("%d.%m.%y %H:%M") # ";" # sserial # ";" # sname # ";" # sdesc);
|
||||
c = c+1;
|
||||
WriteLine(sftime # ";" # sltime # ";" # sAlarmMessage # ";" # sserial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Write(c);
|
||||
)
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user