mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-05-01 20:20:10 +00:00
EnOcean added
git-svn-id: https://svn.fhem.de/fhem/trunk/fhem@957 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
parent
31cdda04e1
commit
a97dd7932e
@ -29,6 +29,11 @@ TCM120_Initialize($)
|
||||
$hash->{ReadFn} = "TCM120_Read";
|
||||
$hash->{WriteFn} = "TCM120_Write";
|
||||
$hash->{ReadyFn} = "TCM120_Ready";
|
||||
$hash->{Clients} = ":EnOcean:";
|
||||
my %matchList= (
|
||||
"1:EnOcean" => "^EnOcean:0B",
|
||||
);
|
||||
$hash->{MatchList} = \%matchList;
|
||||
|
||||
# Normal devices
|
||||
$hash->{DefFn} = "TCM120_Define";
|
||||
@ -134,8 +139,8 @@ TCM120_Read($)
|
||||
Log $ll5, "$name: wrong checksum: got $crc, computed $mycrc" ;
|
||||
return;
|
||||
}
|
||||
if($net =~ m/^0b/) { # Receive Radio Telegram (RRT)
|
||||
Dispatch($hash, $net, undef);
|
||||
if($net =~ m/^0B/) { # Receive Radio Telegram (RRT)
|
||||
Dispatch($hash, "EnOcean:$net", undef);
|
||||
} else { # Receive Message Telegram (RMT)
|
||||
TCM120_Parse($hash, $net, 0);
|
||||
}
|
||||
@ -187,7 +192,7 @@ TCM120_Parse($$$)
|
||||
my $cmd = $parsetbl{substr($rawmsg, 0, 4)};
|
||||
|
||||
if(!$cmd) {
|
||||
$msg ="$name, Unknown command: $rawmsg";
|
||||
$msg ="Unknown command: $rawmsg";
|
||||
|
||||
} else {
|
||||
if($cmd->{expr}) {
|
||||
@ -198,6 +203,7 @@ TCM120_Parse($$$)
|
||||
$msg .= eval $cmd->{expr};
|
||||
|
||||
} else {
|
||||
return "" if($cmd ->{msg} eq "OK" && !$ret); # SKIP Ok
|
||||
$msg = $cmd->{msg};
|
||||
|
||||
}
|
||||
|
201
FHEM/10_EnOcean.pm
Executable file
201
FHEM/10_EnOcean.pm
Executable file
@ -0,0 +1,201 @@
|
||||
##############################################
|
||||
# CUL HomeMatic handler
|
||||
package main;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
sub EnOcean_Define($$);
|
||||
sub EnOcean_Initialize($);
|
||||
sub EnOcean_Pair(@);
|
||||
sub EnOcean_Parse($$);
|
||||
sub EnOcean_PushCmdStack($$);
|
||||
sub EnOcean_SendCmd($$$$);
|
||||
sub EnOcean_Set($@);
|
||||
sub EnOcean_convTemp($);
|
||||
|
||||
sub
|
||||
EnOcean_Initialize($)
|
||||
{
|
||||
my ($hash) = @_;
|
||||
|
||||
$hash->{Match} = "^EnOcean:0B";
|
||||
$hash->{DefFn} = "EnOcean_Define";
|
||||
$hash->{ParseFn} = "EnOcean_Parse";
|
||||
$hash->{SetFn} = "EnOcean_Set";
|
||||
$hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 " .
|
||||
"showtime:1,0 loglevel:0,1,2,3,4,5,6 model " .
|
||||
"subType:remote,sensor,modem ";
|
||||
}
|
||||
|
||||
|
||||
#############################
|
||||
sub
|
||||
EnOcean_Define($$)
|
||||
{
|
||||
my ($hash, $def) = @_;
|
||||
my @a = split("[ \t][ \t]*", $def);
|
||||
my $name = $hash->{NAME};
|
||||
|
||||
return "wrong syntax: define <name> EnOcean 8-digit-hex-code"
|
||||
if(int(@a)!=3 || $a[2] !~ m/^[A-F0-9]{8}$/i);
|
||||
|
||||
$modules{EnOcean}{defptr}{uc($a[2])} = $hash;
|
||||
AssignIoPort($hash);
|
||||
# Help FHEMWEB split up davices
|
||||
$attr{$name}{subType} = $1 if($name =~ m/EnO_(.*)_$a[2]/);
|
||||
return undef;
|
||||
}
|
||||
|
||||
|
||||
my %sets = ( Btn0=>"10:30", Btn1=>"30:30", Btn2=>"20:30", Btn2=>"70:30",
|
||||
"Btn0,Btn2"=>"15:30", "Btn1,Btn2"=>"35:30",
|
||||
"Btn0,Btn3"=>"17:30", "Btn1,Btn3"=>"37:30",
|
||||
"released"=>"00:20" );
|
||||
|
||||
#############################
|
||||
# Simulate a PTM
|
||||
sub
|
||||
EnOcean_Set($@)
|
||||
{
|
||||
my ($hash, @a) = @_;
|
||||
return "no set value specified" if(@a != 2);
|
||||
|
||||
my $cmd = $a[1];
|
||||
my $arg = $a[2];
|
||||
my $cmdhash = $sets{$cmd};
|
||||
return "Unknown argument $cmd, choose one of " . join(" ", sort keys %sets)
|
||||
if(!defined($cmdhash));
|
||||
|
||||
my $name = $hash->{NAME};
|
||||
my $ll2 = GetLogLevel($name, 2);
|
||||
Log $ll2, "EnOcean: set $name $cmd";
|
||||
|
||||
my ($d1, $status) = split(":", $cmdhash, 2);
|
||||
IOWrite($hash, "", sprintf("6B05%s000000%s%s", $d1, $hash->{DEF}, $status));
|
||||
|
||||
my $tn = TimeNow();
|
||||
$hash->{CHANGED}[0] = $cmd;
|
||||
$hash->{STATE} = $cmd;
|
||||
$hash->{READINGS}{state}{TIME} = $tn;
|
||||
$hash->{READINGS}{state}{VAL} = $cmd;
|
||||
return undef;
|
||||
}
|
||||
|
||||
#############################
|
||||
sub
|
||||
EnOcean_Parse($$)
|
||||
{
|
||||
my ($iohash, $msg) = @_;
|
||||
my %ot = ("05"=>"remote", "06"=>"sensor", "07"=>"sensor",
|
||||
"08"=>"remote", "0A"=>"modem", "0B"=>"modem", );
|
||||
|
||||
$msg =~ m/^EnOcean:0B(..)(........)(........)(..)/;
|
||||
my ($org,$data,$id,$status) = ($1,$2,$3,$4,$5);
|
||||
|
||||
my $ot = $ot{$org};
|
||||
if(!$ot) {
|
||||
Log 2, "Unknown EnOcean ORG: $org received from $id";
|
||||
return "";
|
||||
}
|
||||
|
||||
$id = ($id & 0xffff) if($org eq "0A");
|
||||
$id = (($id & 0xffff0000)>>16) if($org eq "0B");
|
||||
|
||||
my $hash = $modules{EnOcean}{defptr}{$id};
|
||||
if(!$hash) {
|
||||
Log 3, "EnOcean Unknown device with ID $id, please define it";
|
||||
return "UNDEFINED EnO_${ot}_$id EnOcean $id";
|
||||
}
|
||||
|
||||
my $name = $hash->{NAME};
|
||||
my $ll4 = GetLogLevel($name, 4);
|
||||
Log $ll4, "EnOcean: ORG:$org, DATA:$data, ID:$id, STATUS:$status";
|
||||
my @event;
|
||||
|
||||
push @event, "0:rp_counter:".(hex($status)&0xf);
|
||||
|
||||
my $d1 = hex substr($data,0,2);
|
||||
|
||||
#################################
|
||||
if($org eq "05") { # PTM remote. Queer reporting methods.
|
||||
my $nu = ((hex($status)&0x10)>>4);
|
||||
|
||||
push @event, "0:T21:".((hex($status)&0x20)>>5);
|
||||
push @event, "0:NU:$nu";
|
||||
|
||||
if($nu) {
|
||||
$msg = sprintf "Btn%d", ($d1&0xe0)>>5;
|
||||
$msg .= sprintf ",Btn%d", ($d1&0x0e)>>1 if($d1 & 1);
|
||||
|
||||
} else {
|
||||
#confusing for normal use
|
||||
#my $nbu = (($d1&0xe0)>>5);
|
||||
#$msg = sprintf "Buttons %d", $nbu ? ($nbu+1) : 0;
|
||||
$msg = "buttons";
|
||||
|
||||
}
|
||||
$msg .= ($d1&0x10) ? " pressed" : " released";
|
||||
push @event, "1:state:$msg";
|
||||
|
||||
#################################
|
||||
} elsif($org eq "06") {
|
||||
push @event, "1:state:$d1";
|
||||
push @event, "1:sensor1:$d1";
|
||||
|
||||
#################################
|
||||
} elsif($org eq "07") {
|
||||
my $d2 = hex substr($data,2,2);
|
||||
my $d3 = hex substr($data,4,2);
|
||||
my $d4 = hex substr($data,6,2);
|
||||
push @event, "1:state:$d1";
|
||||
push @event, "1:sensor1:$d1";
|
||||
push @event, "1:sensor2:$d2";
|
||||
push @event, "1:sensor3:$d3";
|
||||
push @event, "1:D3:".($d4&0x8)?1:0;
|
||||
push @event, "1:D2:".($d4&0x4)?1:0;
|
||||
push @event, "1:D1:".($d4&0x2)?1:0;
|
||||
push @event, "1:D0:".($d4&0x1)?1:0;
|
||||
|
||||
#################################
|
||||
} elsif($org eq "08") { # CTM remote.
|
||||
# Dont understand the SR bit
|
||||
$msg = sprintf "Btn%d", ($d1&0xe0)>>5;
|
||||
$msg .= ($d1&0x10) ? " pressed" : " released";
|
||||
push @event, "1:state:$msg";
|
||||
|
||||
#################################
|
||||
} elsif($org eq "0A") {
|
||||
push @event, "1:state:Modem:".substr($msg, 12, 6);
|
||||
|
||||
#################################
|
||||
} elsif($org eq "0B") {
|
||||
push @event, "1:state:Modem:ACK";
|
||||
|
||||
}
|
||||
|
||||
my $tn = TimeNow();
|
||||
my @changed;
|
||||
for(my $i = 0; $i < int(@event); $i++) {
|
||||
my ($dochanged, $vn, $vv) = split(":", $event[$i], 3);
|
||||
|
||||
if($dochanged) {
|
||||
if($vn eq "state") {
|
||||
$hash->{STATE} = $vv;
|
||||
push @changed, $vv;
|
||||
|
||||
} else {
|
||||
push @changed, "$vn: $vv";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$hash->{READINGS}{$vn}{TIME} = TimeNow();
|
||||
$hash->{READINGS}{$vn}{VAL} = $vv;
|
||||
}
|
||||
$hash->{CHANGED} = \@changed;
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
1;
|
@ -86,6 +86,7 @@
|
||||
<a href="#ECMD">ECMD</a>
|
||||
<a href="#ECMDDevice">ECMDDevice</a>
|
||||
<a href="#DS18S20">DS18S20</a>
|
||||
<a href="#EnOcean">EnOcean</a>
|
||||
<a href="#EM">EM</a>
|
||||
<a href="#EMEM">EMEM</a>
|
||||
<a href="#EMGZ">EMGZ</a>
|
||||
@ -2720,6 +2721,84 @@ A line ending with \ will be concatenated with the next one, so long lines
|
||||
<br>
|
||||
|
||||
|
||||
<a name="EnOcean"></a>
|
||||
<h3>EnOcean</h3>
|
||||
<ul>
|
||||
Devices sold by numerous hardware verndors (e.g. Eltako, Peha, etc), using
|
||||
the RF Protocol provided by the EnOcean Alliance.
|
||||
This module is currently only tested to work with remotes, but other types of
|
||||
devices should also work. Feedback is welcome.
|
||||
<br><br>
|
||||
<a name="EnOceandefine"></a>
|
||||
<b>Define</b>
|
||||
<ul>
|
||||
<code>define <name> EnOcean <ID></code>
|
||||
<br><br>
|
||||
|
||||
Define an EnOcean device, connected via a <a href="#TCM120">TCM120</a>. The
|
||||
<ID> parameter is an 8 digit hex number. For remotes and sensors the
|
||||
<a href="#autocreate">autocreate</a> module may help you.<br>
|
||||
In order to control devices, you cannot reuse the ID's of other devices
|
||||
(like remotes), instead you have to create your own, which must be in the
|
||||
allowed ID-Range of the underlying IO device. For this first query the
|
||||
TCM120 with the "<code>get <tcm> idbase</code>" command. You can use
|
||||
up to 128 ID's starting with the base shown there. If you are using an
|
||||
ID outside of the allowed range, you'll see an ERR_ID_RANGE message in the
|
||||
fhem log.<br>
|
||||
|
||||
For each ID there is the possibility to use 8 different commands, so
|
||||
in fact you can control 8*128/2=512 switches.<br><br>
|
||||
|
||||
|
||||
Example:
|
||||
<ul>
|
||||
<code>define switch1 EnOcean ffc54500</code><br>
|
||||
</ul>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<a name="EnOceanset"></a>
|
||||
<b>Set</b>
|
||||
<ul>
|
||||
<code>set switch1 <value></code>
|
||||
<br><br>
|
||||
where <code>value</code> is one of Btn0, Btn1, Btn2, Btn3, then certain
|
||||
combinations of these buttons: Btn0,Btn2 Btn0,Btn3 Btn1,Btn2 Btn1,Btn3 and
|
||||
released.<br>
|
||||
In fact we are trying to emulate a PTM100 type remote, which is capable to
|
||||
report either single buttons, some combinations of 2 buttons and the
|
||||
released state.<br>
|
||||
If you define an <a href="#eventMap">eventMap</a> attribute with on/off,
|
||||
then you'll be able to easily set the device from the <a
|
||||
href="#FHEMWEB">WEB</a> frontend.<br><br>
|
||||
Example:
|
||||
<ul><code>
|
||||
set switch1 Btn1<br>
|
||||
set switch1 Btn0,Btn2<br>
|
||||
attr eventMap Btn1:on Btn0,Btn2:off<br>
|
||||
set switch1 on<br>
|
||||
</code></ul>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<a name="EnOceanget"></a>
|
||||
<b>Get</b> <ul>N/A</ul><br>
|
||||
|
||||
<a name="EnOceanattr"></a>
|
||||
<b>Attributes</b>
|
||||
<ul>
|
||||
<li><a href="#eventMap">eventMap</a></li>
|
||||
<li><a href="#IODev">IODev</a></li>
|
||||
<li><a href="#loglevel">loglevel</a></li>
|
||||
<li><a href="#do_not_notify">do_not_notify</a></li>
|
||||
<li><a href="#ignore">ignore</a></li>
|
||||
<li><a href="#showtime">showtime</a></li>
|
||||
<li><a href="#model">model</a></li>
|
||||
<li><a href="#subType">subType</a></li>
|
||||
</ul>
|
||||
<br>
|
||||
</ul>
|
||||
|
||||
<a name="EM"></a>
|
||||
<h3>EM</h3>
|
||||
<ul>
|
||||
@ -5656,6 +5735,8 @@ Readings and STATE of temperature/humidity sensors are compatible with the CUL_W
|
||||
<b>Set </b>
|
||||
<ul>
|
||||
<li>idbase<br>
|
||||
Set the ID base. Note: The firmware executes this command only up to then
|
||||
times to prevent misuse.
|
||||
<li>modem_off<br>
|
||||
<li>modem_on<br>
|
||||
<li>reset<br>
|
||||
@ -5671,6 +5752,9 @@ Readings and STATE of temperature/humidity sensors are compatible with the CUL_W
|
||||
<b>Get</b>
|
||||
<ul>
|
||||
<li>idbase<br>
|
||||
Get the ID base. You need this command in order to control EnOcean
|
||||
devices, see the <a href="#EnOceandefine">EnOcean</a>
|
||||
paragraph.<br><br>
|
||||
<li>modem_status<br>
|
||||
<li>sensitivity<br>
|
||||
<li>sw_ver<br>
|
||||
|
@ -119,7 +119,7 @@
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li>support for a lot of protocols used in house automation like FS20,
|
||||
FHT, HMS, OneWire, X10, S300, EM, HomeMatic, KNX. See the <a
|
||||
FHT, HMS, OneWire, X10, S300, EM, HomeMatic, KNX, EnOcean. See the <a
|
||||
href="#Hardware">Hardware</a> section for more.
|
||||
</li>
|
||||
<li>autocreating devices/logs when receiving data from a new device:
|
||||
@ -207,6 +207,9 @@
|
||||
href="http://www.busware.de">www.busware.de</a>) access to the EIB/KNX
|
||||
protocol.
|
||||
<div id="dist"></div>
|
||||
|
||||
<li>Via a TCM120 (e.g. the BSC BOR) access to the EnOcean protocol.
|
||||
<div id="dist"></div>
|
||||
<div id="dist"></div>
|
||||
<div id="dist"></div>
|
||||
|
||||
|
12
fhem.pl
12
fhem.pl
@ -167,7 +167,7 @@ my $nextat; # Time when next timer will be triggered.
|
||||
my $intAtCnt=0;
|
||||
my %duplicate; # Pool of received msg for multi-fhz/cul setups
|
||||
my $duplidx=0; # helper for the above pool
|
||||
my $cvsid = '$Id: fhem.pl,v 1.147 2011-07-15 08:59:31 rudolfkoenig Exp $';
|
||||
my $cvsid = '$Id: fhem.pl,v 1.148 2011-07-24 11:53:11 rudolfkoenig Exp $';
|
||||
my $namedef =
|
||||
"where <name> is either:\n" .
|
||||
"- a single device name\n" .
|
||||
@ -179,14 +179,14 @@ my $stt_day; # Used by SecondsTillTomorrow()
|
||||
|
||||
$init_done = 0;
|
||||
|
||||
$modules{_internal_}{ORDER} = -1;
|
||||
$modules{_internal_}{LOADED} = 1;
|
||||
$modules{_internal_}{AttrList} =
|
||||
$modules{Global}{ORDER} = -1;
|
||||
$modules{Global}{LOADED} = 1;
|
||||
$modules{Global}{AttrList} =
|
||||
"archivecmd allowfrom archivedir configfile lastinclude logfile " .
|
||||
"modpath nrarchive pidfilename port statefile title userattr " .
|
||||
"verbose:1,2,3,4,5 mseclog version nofork logdir holiday2we " .
|
||||
"autoload_undefined_devices dupTimeout";
|
||||
$modules{_internal_}{AttrFn} = "GlobalAttr";
|
||||
$modules{Global}{AttrFn} = "GlobalAttr";
|
||||
my $commonAttr = "eventMap";
|
||||
|
||||
|
||||
@ -2174,7 +2174,7 @@ doGlobalDef($)
|
||||
|
||||
$devcount = 1;
|
||||
$defs{global}{NR} = $devcount++;
|
||||
$defs{global}{TYPE} = "_internal_";
|
||||
$defs{global}{TYPE} = "Global";
|
||||
$defs{global}{STATE} = "<no definition>";
|
||||
$defs{global}{DEF} = "<no definition>";
|
||||
$defs{global}{NAME} = "global";
|
||||
|
@ -79,7 +79,7 @@ FHEMWEB_Initialize($)
|
||||
$hash->{AttrList}= "loglevel:0,1,2,3,4,5,6 webname fwmodpath fwcompress " .
|
||||
"plotmode:gnuplot,gnuplot-scroll,SVG plotsize refresh " .
|
||||
"touchpad smallscreen plotfork basicAuth basicAuthMsg ".
|
||||
"HTTPS";
|
||||
"stylesheet HTTPS";
|
||||
|
||||
###############
|
||||
# Initialize internal structures
|
||||
@ -329,7 +329,7 @@ FW_AnswerCall($)
|
||||
return 1;
|
||||
|
||||
} elsif($arg =~ m,^$FW_ME/icons/(.*)$, ||
|
||||
$arg =~ m,^$FW_ME/(.*.png)$,) {
|
||||
$arg =~ m,^$FW_ME/(.*.png)$,i) {
|
||||
my $img = $1;
|
||||
my $cachable = 1;
|
||||
if(!open(FH, "$FW_dir/$img")) {
|
||||
@ -427,6 +427,7 @@ FW_AnswerCall($)
|
||||
pO "<meta http-equiv=\"refresh\" content=\"$rf\">" if($rf);
|
||||
my $stylecss = ($FW_ss ? "style_smallscreen.css" :
|
||||
$FW_tp ? "style_touchpad.css" : "style.css");
|
||||
$stylecss = AttrVal($FW_wname, "stylecss", $stylecss);
|
||||
pO "<link href=\"$FW_ME/$stylecss\" rel=\"stylesheet\"/>";
|
||||
pO "<script type=\"text/javascript\" src=\"$FW_ME/svg.js\"></script>"
|
||||
if($FW_plotmode eq "SVG");
|
||||
@ -800,20 +801,24 @@ FW_showRoom()
|
||||
|
||||
############################
|
||||
# Print the table headers
|
||||
my @sortedDevs = sort keys %{$FW_types{$type}};
|
||||
my $allSets = " " . getAllSets($sortedDevs[0]) . " ";
|
||||
|
||||
my @roomDevs = grep { $FW_room && ($FW_room eq "all" ||
|
||||
$FW_rooms{$FW_room}{$_}) }
|
||||
sort keys %{$FW_types{$type}};
|
||||
my $allSets = " " . getAllSets($roomDevs[0]) . " ";
|
||||
my $hasOnOff = ($allSets =~ m/ on / && $allSets =~ m/ off /);
|
||||
if(!$hasOnOff) { # Check the eventMap
|
||||
my $em = AttrVal($roomDevs[0], "eventMap", "") . " ";
|
||||
$hasOnOff = ($em =~ m/:on / && $em =~ m/:off /);
|
||||
}
|
||||
|
||||
|
||||
my $th;
|
||||
my $id = "class=\"block\"";
|
||||
if($hasOnOff) {
|
||||
$th = "$type</th><th>State</th><th colspan=\"2\">Set to";
|
||||
} elsif($type eq "FHT") {
|
||||
$th = "FHT dev.</th><th>Measured</th><th>Set to";
|
||||
} elsif($allSets =~ m/ desired-temp /) {
|
||||
$th = "Device</th><th>Measured</th><th>Set to";
|
||||
} elsif($type eq "at") { $th = "Scheduled commands (at)";
|
||||
} elsif($type eq "FileLog") { $th = "Logs";
|
||||
} elsif($type eq "_internal_") { $th = "Global variables";
|
||||
} elsif($type eq "weblink") { $th = ""; $id = "";
|
||||
} else {
|
||||
$th = $type;
|
||||
@ -822,7 +827,7 @@ FW_showRoom()
|
||||
pO " <tr><th>$th</th></tr>" if($th);
|
||||
|
||||
my $row=1;
|
||||
foreach my $d (@sortedDevs) {
|
||||
foreach my $d (@roomDevs) {
|
||||
next if($FW_room && $FW_room ne "all" &&
|
||||
!$FW_rooms{$FW_room}{$d});
|
||||
|
||||
@ -831,18 +836,13 @@ FW_showRoom()
|
||||
my $v = $defs{$d}{STATE};
|
||||
|
||||
if($hasOnOff) {
|
||||
|
||||
my $iv = $v; # icon value
|
||||
my $iname = "";
|
||||
|
||||
if(defined(AttrVal($d, "showtime", undef))) {
|
||||
|
||||
$v = $defs{$d}{READINGS}{state}{TIME};
|
||||
|
||||
} elsif($iv) {
|
||||
|
||||
$iname = FW_dev2image($d);
|
||||
|
||||
}
|
||||
$v = "" if(!defined($v));
|
||||
|
||||
@ -858,8 +858,7 @@ FW_showRoom()
|
||||
pH "cmd.$d=set $d off$rf", "off", 1;
|
||||
}
|
||||
|
||||
} elsif($type eq "FHT") {
|
||||
|
||||
} elsif($allSets =~ m/ desired-temp /) {
|
||||
$v = ReadingsVal($d, "measured-temp", "");
|
||||
|
||||
$v =~ s/ .*//;
|
||||
|
Loading…
x
Reference in New Issue
Block a user