fhem-mirror/FHEM/36_ShellyMonitor.pm
2021-02-17 12:40:44 +00:00

1191 lines
44 KiB
Perl

##############################################
# 36_ShellyMonitor.pm
#
# Parses the MultiCast "COAP" messages of Shellys and updates
# devices accordingly
#
# $Id$
#
package main;
use strict;
use warnings;
no warnings 'portable'; # Support for 64-bit ints required
use vars qw{%attr %defs};
my $COIOT_OPTION_GLOBAL_DEVID = 3332;
my $COIOT_OPTION_STATUS_VALIDITY = $COIOT_OPTION_GLOBAL_DEVID+80;
my $COIOT_OPTION_STATUS_SERIAL = $COIOT_OPTION_GLOBAL_DEVID+88;
my $SHELLY_DEF_SEN = {
# Old version 1:
"111" => { "type"=>"P", "desc"=>"power_0", "unit"=>"W"},
"121" => { "type"=>"P", "desc"=>"power_1", "unit"=>"W"},
"131" => { "type"=>"P", "desc"=>"power_2", "unit"=>"W"},
"141" => { "type"=>"P", "desc"=>"power_3", "unit"=>"W"},
"112" => { "type"=>"S", "desc"=>"output_0"},
"122" => { "type"=>"S", "desc"=>"output_1"},
"132" => { "type"=>"S", "desc"=>"output_2"},
"142" => { "type"=>"S", "desc"=>"output_3"},
"113" => { "type"=>"T", "desc"=>"deviceTemp", "unit"=>"C"},
"114" => { "type"=>"T", "desc"=>"deviceTempF", "unit"=>"F"},
"214" => { "type"=>"E", "desc"=>"energy_0", "unit"=>"Wmin"},
# Version 2, since FW >= 1.6
"1101" => { "type"=>"S", "desc"=>"output_0"},
# Used by Shelly 2 SHSW-21 roller-mode, Shelly 2.5 SHSW-25 roller-mode:
"1102" => { "type"=>"S", "desc"=>"roller"},
# Used by Shelly 2 SHSW-21 roller-mode, Shelly 2.5 SHSW-25 roller-mode:
"1103" => { "type"=>"S", "desc"=>"rollerPos"},
# Used by Shelly Air SHAIR-1:
"1104" => { "type"=>"S", "desc"=>"totalWorkTime", "unit"=>"s"},
# Used by Shelly Gas SHGS-1:
"1105" => { "type"=>"S", "desc"=>"valve"},
# Used by Shelly 2.5 SHSW-25 relay-mode, Shelly 2 SHSW-21 relay-mode, Shelly RGBW2-white SHRGBW2-white, Shelly 2LED SH2LED-1:
"1201" => { "type"=>"S", "desc"=>"output_1"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"1301" => { "type"=>"S", "desc"=>"output_2"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"1401" => { "type"=>"S", "desc"=>"output_3"},
"2101" => { "type"=>"S", "desc"=>"input_0"},
"2102" => { "type"=>"EV", "desc"=>"inputEvent_0"},
"2103" => { "type"=>"EVC", "desc"=>"inputEventCnt_0"},
"2201" => { "type"=>"S", "desc"=>"input_1"},
"2202" => { "type"=>"EV", "desc"=>"inputEvent_1"},
"2203" => { "type"=>"EVC", "desc"=>"inputEventCnt_1"},
# Used by Shelly i3 SHIX3-1:
"2301" => { "type"=>"S", "desc"=>"input_2"},
# Used by Shelly i3 SHIX3-1:
"2302" => { "type"=>"EV", "desc"=>"inputEvent_2"},
# Used by Shelly i3 SHIX3-1:
"2303" => { "type"=>"EVC", "desc"=>"inputEventCnt_2"},
"3101" => { "type"=>"T", "desc"=>"extTemp_0", "unit"=>"C"},
"3102" => { "type"=>"T", "desc"=>"extTemp_0f", "unit"=>"F"},
"3103" => { "type"=>"H", "desc"=>"humidity"},
"3104" => { "type"=>"T", "desc"=>"deviceTemp", "unit"=>"C"},
"3105" => { "type"=>"T", "desc"=>"deviceTempF", "unit"=>"F"},
# Used by Shelly Sense SHSEN-1, Shelly Door Window SHDW-1, Shelly Door Window 2 SHDW-2:
"3106" => { "type"=>"L", "desc"=>"luminosity", "unit"=>"lux"},
# Used by Shelly Gas SHGS-1:
"3107" => { "type"=>"C", "desc"=>"concentration", "unit"=>"ppm"},
# Used by Shelly Door Window SHDW-1, Shelly Door Window 2 SHDW-2:
"3108" => { "type"=>"S", "desc"=>"dwIsOpened"},
# Used by Shelly Door Window SHDW-1, Shelly Door Window 2 SHDW-2:
"3109" => { "type"=>"S", "desc"=>"tilt", "unit"=>"deg"},
# Used by Shelly Door Window SHDW-1, Shelly Door Window 2 SHDW-2:
"3110" => { "type"=>"S", "desc"=>"luminosityLevel"},
"3111" => { "type"=>"B", "desc"=>"battery"},
# Used by Shelly Sense SHSEN-1, Shelly Button SHBTN-1:
"3112" => { "type"=>"S", "desc"=>"charger"},
# Used by Shelly Gas SHGS-1:
"3113" => { "type"=>"S", "desc"=>"sensorOp"},
# Used by Shelly Gas SHGS-1:
"3114" => { "type"=>"S", "desc"=>"selfTest"},
"3115" => { "type"=>"S", "desc"=>"sensorError"},
# Used by Shelly Spot SHSPOT-1, Shelly Spot 2 SHSPOT-2:
"3116" => { "type"=>"S", "desc"=>"dayLight"},
"3117" => { "type"=>"S", "desc"=>"extInput"},
"3119" => { "type"=>"S", "desc"=>"timestamp"},
"3120" => { "type"=>"S", "desc"=>"active"},
"3201" => { "type"=>"T", "desc"=>"extTemp_1", "unit"=>"C"},
"3202" => { "type"=>"T", "desc"=>"extTemp_1f", "unit"=>"F"},
"3301" => { "type"=>"T", "desc"=>"extTemp_2", "unit"=>"C"},
"3302" => { "type"=>"T", "desc"=>"extTemp_2f", "unit"=>"F"},
"4101" => { "type"=>"P", "desc"=>"power_0", "unit"=>"W"},
# Used by Shelly 2 SHSW-21 roller-mode, Shelly 2.5 SHSW-25 roller-mode:
"4102" => { "type"=>"P", "desc"=>"rollerPower", "unit"=>"W"},
"4103" => { "type"=>"E", "desc"=>"energy_0", "unit"=>"Wmin"},
# Used by Shelly 2 SHSW-21 roller-mode, Shelly 2.5 SHSW-25 roller-mode:
"4104" => { "type"=>"E", "desc"=>"rollerEnergy", "unit"=>"Wmin"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4105" => { "type"=>"P", "desc"=>"power_0", "unit"=>"W"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4106" => { "type"=>"E", "desc"=>"energy_0", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4107" => { "type"=>"E", "desc"=>"energyReturned_0", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4108" => { "type"=>"V", "desc"=>"voltage_0", "unit"=>"V"},
# Used by Shelly 3EM SHEM-3:
"4109" => { "type"=>"I", "desc"=>"current_0", "unit"=>"A"},
# Used by Shelly 3EM SHEM-3:
"4110" => { "type"=>"S", "desc"=>"powerFactor_0"},
# Used by Shelly 2.5 SHSW-25 relay-mode, Shelly RGBW2-white SHRGBW2-white:
"4201" => { "type"=>"P", "desc"=>"power_1", "unit"=>"W"},
# Used by Shelly 2.5 SHSW-25 relay-mode, Shelly RGBW2-white SHRGBW2-white:
"4203" => { "type"=>"E", "desc"=>"energy_1", "unit"=>"Wmin"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4205" => { "type"=>"P", "desc"=>"power_1", "unit"=>"W"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4206" => { "type"=>"E", "desc"=>"energy_1", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4207" => { "type"=>"E", "desc"=>"energyReturned_1", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3, Shelly EM SHEM:
"4208" => { "type"=>"V", "desc"=>"voltage_1", "unit"=>"V"},
# Used by Shelly 3EM SHEM-3:
"4209" => { "type"=>"I", "desc"=>"current_1", "unit"=>"A"},
# Used by Shelly 3EM SHEM-3:
"4210" => { "type"=>"S", "desc"=>"powerFactor_1"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"4301" => { "type"=>"P", "desc"=>"power_2", "unit"=>"W"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"4303" => { "type"=>"E", "desc"=>"energy_2", "unit"=>"Wmin"},
# Used by Shelly 3EM SHEM-3:
"4305" => { "type"=>"P", "desc"=>"power_2", "unit"=>"W"},
# Used by Shelly 3EM SHEM-3:
"4306" => { "type"=>"E", "desc"=>"energy_2", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3:
"4307" => { "type"=>"E", "desc"=>"energyReturned_2", "unit"=>"Wh"},
# Used by Shelly 3EM SHEM-3:
"4308" => { "type"=>"V", "desc"=>"voltage_2", "unit"=>"V"},
# Used by Shelly 3EM SHEM-3:
"4309" => { "type"=>"I", "desc"=>"current_2", "unit"=>"A"},
# Used by Shelly 3EM SHEM-3:
"4310" => { "type"=>"S", "desc"=>"powerFactor_2"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"4401" => { "type"=>"P", "desc"=>"power_3", "unit"=>"W"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"4403" => { "type"=>"E", "desc"=>"energy_3", "unit"=>"Wmin"},
"5101" => { "type"=>"S", "desc"=>"brightness_0"},
"5102" => { "type"=>"S", "desc"=>"gain"},
"5103" => { "type"=>"S", "desc"=>"colorTemp", "unit"=>"K"},
# Used by Shelly Duo SHBDUO-1:
"5104" => { "type"=>"S", "desc"=>"whiteLevel"},
"5105" => { "type"=>"S", "desc"=>"L-red"},
"5106" => { "type"=>"S", "desc"=>"L-green"},
"5107" => { "type"=>"S", "desc"=>"L-blue"},
"5108" => { "type"=>"S", "desc"=>"L-white"},
# Used by Shelly RGBW2-white SHRGBW2-white, Shelly 2LED SH2LED-1:
"5201" => { "type"=>"S", "desc"=>"brightness_1"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"5301" => { "type"=>"S", "desc"=>"brightness_2"},
# Used by Shelly RGBW2-white SHRGBW2-white:
"5401" => { "type"=>"S", "desc"=>"brightness_3"},
"6101" => { "type"=>"A", "desc"=>"overtemp"},
"6102" => { "type"=>"A", "desc"=>"overpower_0"},
# Used by Shelly 2 SHSW-21 roller-mode, Shelly 2.5 SHSW-25 roller-mode:
"6103" => { "type"=>"A", "desc"=>"rollerStopReason"},
# Used by Shelly Dimmer SHDM-1, Shelly Dimmer W1 SHDIMW-1:
"6104" => { "type"=>"A", "desc"=>"loadError"},
# Used by Shelly Smoke 2 SHSM-02, Shelly Smoke SHSM-01:
"6105" => { "type"=>"A", "desc"=>"smoke"},
# Used by Shelly Flood SHWT-1:
"6106" => { "type"=>"A", "desc"=>"flood"},
# Used by Shelly Sense SHSEN-1, Shelly Spot SHSPOT-1, Shelly Spot 2 SHSPOT-2:
"6107" => { "type"=>"A", "desc"=>"motion"},
# Used by Shelly Gas SHGS-1:
"6108" => { "type"=>"A", "desc"=>"gas"},
"6109" => { "type"=>"P", "desc"=>"overpowerValue", "unit"=>"W"},
# Used by Shelly Door Window SHDW-1, Shelly Door Window 2 SHDW-2:
"6110" => { "type"=>"A", "desc"=>"vibration"},
# Used by Shelly 2.5 SHSW-25 relay-mode, Shelly 2 SHSW-21 relay-mode:
"6202" => { "type"=>"A", "desc"=>"overpower_1"},
"9101" => { "type"=>"S", "desc"=>"mode"},
"9102" => { "type"=>"EV", "desc"=>"wakeupEvent"},
"9103" => { "type"=>"EVC", "desc"=>"cfgChanged"}
};
# Copied from 36_Shelly, keep up to date..:
my %shelly_models = (
#(relays,rollers,dimmers,meters)
"generic" => [4,4,4,4],
"shellygeneric" => [4,4,4,4],
"shelly1" => [1,0,0,0],
"shelly1pm" => [1,0,0,1],
"shelly2" => [2,1,0,1],
"shelly2.5" => [2,1,0,2],
"shellyplug" => [1,0,0,1],
"shelly4" => [4,0,0,4],
"shellyrgbw" => [0,0,4,1],
"shellydimmer" => [0,0,1,1],
"shellyem" => [1,0,0,2],
"shellybulb" => [0,0,1,1],
);
my %shelly_models_by_mod_shelly = ();
# Mapping of DeviceId in Multicast to Shelly model attr
my %DEVID_MODEL = (
"SHPLG-S" => "shellyplug",
"SHSW-PM" => "shelly1pm",
"SHSW-L" => "shelly1pm",
"SHSW-1" => "shelly1",
"SHSW-21" => "shelly2",
"SHSW-25" => "shelly2.5",
"SHDM-2" => "shellydimmer",
"SHSW-44" => "shelly4",
"SHRGBW2" => "shellyrgbw",
"SHCB-1" => "shellybulb",
"SHBLB-1" => "shellybulb",
"SHBDUO-1" => "shellybulb",
"SHMOS-01" => "generic",
"SHWT-1" => "generic",
"SHHT-1" => "generic",
"SHGS-1" => "generic",
"SHBTN-2" => "generic",
"SHIX3-1" => "generic"
);
# Mapping of DeviceId in Multicast to suggested generic name
my %DEVID_PREFIX = (
"SHPLG-S" => "shelly_plug_s",
"SHSW-PM" => "shelly_1pm",
"SHSW-L" => "shelly_1l",
"SHSW-1" => "shelly_1",
"SHSW-21" => "shelly_2",
"SHSW-25" => "shelly_25",
"SHDM-2" => "shelly_dimmer",
"SHSW-44" => "shelly_4",
"SHRGBW2" => "shelly_rgbw",
"SHBLB-1" => "shelly_bulb",
"SHCB-1" => "shelly_duo",
"SHBDUO-1" => "shelly_duo",
"SHWT-1" => "shelly_flood",
"SHHT-1" => "shelly_ht",
"SHGS-1" => "shelly_gas",
"SHBTN-2" => "shelly_button",
"SHIX3-1" => "shelly_i3",
"SHMOS-01" => "shelly_motion"
);
# Mapping of DeviceId in Multicast to additional attributes on creation
my $devicon_lametta_pm0 = 'devStateIcon {my $lderr = ReadingsVal($name,"network","-")' .
' !~ /^.*>connected.*/?"10px-kreis-rot":"10px-kreis-gruen"; ' .
'my $light = ReadingsVal($name,"relay_0","off"); ' .
'my $cons = ReadingsVal($name,"power","unknown");' .
'my $kwh = sprintf("%.2f kWh", ReadingsVal($name,"energy",0)/1000.0);' .
'FW_makeImage($lderr)."<a href=\"/fhem?cmd.dummy=set $name toggle&XHR=1\">' .
'".FW_makeImage($light)."</a><div>$cons W / $kwh</div>"}';
my $devicon_lametta_pm = $devicon_lametta_pm0;
$devicon_lametta_pm =~ s/_0//;
my %DEVID_ATTRS = (
"SHDM-2" => [ ( "webCmd pct:on:off", "widgetOverride pct:slider,0,1,100", $devicon_lametta_pm0 ) ],
"SHBDUO-1" => [ ( "widgetOverride ct:colorpicker,CT,2700,10,6500", $devicon_lametta_pm0 ) ],
"SHCB-1" => [ ( $devicon_lametta_pm0 ) ],
"SHSW-PM" => [ ( $devicon_lametta_pm ) ],
);
my %DEVID_TTL_OVERRIDE = (
"SHWT-1" => 90000,
"SHHT-1" => 90000,
"SHGS-1" => 90000,
"SHBTN-2" => 90000
);
# SHWT-1 = Shelly Flood, should go to generic
# SHHT-1 = Shelly H&T, should go to generic
# SHGS-1 = Shelly Gas, should go to generic
# SHBTN-2 = Shelly Button, should go to generic
my %ROLLER_STATUS_MAP = (
"open" => "moving_up",
"close" => "moving_down",
"stop" => "stopped"
);
#####################################
sub ShellyMonitor_Initialize
{
my $hash = shift;
$hash->{Match} = "^\/(?s:.*)\!\$";
$hash->{ReadFn} = "ShellyMonitor_Read";
$hash->{ReadyFn} = "ShellyMonitor_Ready";
$hash->{DefFn} = "ShellyMonitor_Define";
$hash->{UndefFn} = "ShellyMonitor_Undef";
$hash->{AttrFn} = "ShellyMonitor_Attr";
$hash->{NotifyFn}= "ShellyMonitor_Notify";
$hash->{SetFn}= "ShellyMonitor_Set";
$hash->{AttrList}= "ignoreDevices ". $readingFnAttributes;
$hash->{FW_detailFn} = "ShellyMonitor_detailFn";
# Check which models are available in Mod_Shelly
LoadModule "Shelly";
my $fn = $modules{"Shelly"}{"AttrList"};
if($fn && $fn=~/ model:([^ ]+)( |$)/) {
map { $shelly_models_by_mod_shelly{$_} = 1 } split (/,/, $1);
Log3 $hash->{NAME}, 2, "Shelly-Module loaded supports models: " . join(',', keys %shelly_models_by_mod_shelly);
}
}
sub MCast_Open {
my ($hash, $interface) = @_;
my $name = $hash->{NAME};
my $dev = $hash->{".McastInterface"};
my $reopen = 0;
my $conn;
my $err;
eval {
$err = "Perl-Module IO::Socket::Multicast not found. Either execute \"sudo apt-get install libio-socket-multicast-perl\" (for Raspbian Buster), or \"sudo cpan install IO::Socket::Multicast\"";
require IO::Socket::Multicast;
$err = "Perl-Module JSON. Either execute \"sudo apt-get install libjson-perl\" (for Raspbian Buster), or \"sudo cpan install JSON\"";
require JSON;
$conn = IO::Socket::Multicast->new(LocalPort=>5683, ReuseAddr=>1) or $err = "Cannot open Multicast socket: $^E";
if (defined $interface) {
$err = "Error adding mcast interface $interface";
$conn->mcast_add('224.0.1.187', $interface);
} else {
$err = "Error adding mcast interface";
$conn->mcast_add('224.0.1.187') or $err = "Cannot open Multicast socket: $^E";
}
$conn->sockopt(IO::Socket::SO_RCVBUF, 65535);
$err = undef;
$hash->{".JSON"} = JSON->new->utf8;
1;
};
if($@) {
Log3 $name, 1, $err;
return $err;
# return &$doCb($@);
}
if(!$conn) {
Log3 $name, 1, "$name: Can't connect to $dev: $^E" if(!$reopen);
$readyfnlist{"$name.$dev"} = $hash;
# DevIo_setStates($hash, "disconnected");
return $err;
# return &$doCb("");
}
$hash->{MCastDev} = $conn;
$hash->{FD} = $conn->fileno();
$dev = "" if (! defined $dev);
delete($readyfnlist{"$name.$dev"});
$selectlist{"$name.$dev"} = $hash;
return;
}
sub MCast_Close {
my $hash = shift;
my $name = $hash->{NAME};
my $dev = $hash->{".McastInterface"};
my $conn = $hash->{MCastDev};
$conn->close() if ($conn);
return unless defined $dev;
delete($selectlist{"$name.$dev"});
delete($readyfnlist{"$name.$dev"});
delete($hash->{FD});
delete($hash->{EXCEPT_FD});
delete($hash->{PARTIAL});
delete($hash->{NEXT_OPEN});
}
#####################################
sub ShellyMonitor_Define {
my ($hash, $def) = @_;
my ($a, $h) = parseParams($def);
return 'wrong syntax: define <name> ShellyMonitor [interface]' if(@{$a} < 2);
MCast_Close($hash);
RemoveInternalTimer($hash);
my $name = ${$a}[0];
my $dev;
my $wantAuto;
if (@{$a}>2) {
$wantAuto = (${$a}[2]=~/^auto/);
$dev = $wantAuto ? (@{$a}>3 ? ${$a}[3] : undef ) : ${$a}[2];
}
$hash->{NAME} = $name;
$hash->{".McastInterface"} = $dev;
$hash->{".Ignored"}=0;
$hash->{".Received"}=0;
$hash->{".ReceivedBroken"}=0;
$hash->{".ReceivedByIp"}=();
$hash->{".ip2device"}=();
$hash->{NOTIFYDEV} = "global";
# if ($wantAuto && !AttrVal( $name, 'alexaMapping', undef ) ) {
# CommandAttr(undef,"$name autoCreate Shelly");
# }
my $device_name = "ShellyMonitor_".$name;
$modules{ShellyMonitor}{defptr}{$device_name} = $hash;
Log3 $hash,5,"ShellyMonitor ($name) - Opening device...";
return MCast_Open($hash, $dev);
}
sub ShellyMonitor_Init
{
Log 3,"Init done";
return;
}
#####################################
sub ShellyMonitor_Undef
{
my ($hash, $arg) = @_;
MCast_Close($hash);
return;
}
sub ShellyMonitor_DoRead
{
my $hash = shift;
my $name = $hash->{NAME};
my $conn = $hash->{MCastDev};
my $data;
my $pinfo = $conn->recv($data,1400);
my ($port, $ip_address) = unpack_sockaddr_in $pinfo;
my $sending_ip = inet_ntoa ($ip_address);
Log3 ($name, 4, "Received data from $sending_ip");
$hash->{".Received"}++;
$hash->{".ReceivedByIp"}->{$sending_ip}++;
my $ip2devicesDirty = 0;
my @devrefs = getDevicesForIp($hash, $sending_ip);
if (! @devrefs ) {
Log3 ($name, 4, "$sending_ip not found in cache");
# Search for defined devices by IP:
@devrefs = ();
my @devNames = devspec2array("TYPE=Shelly:FILTER=DEF=$sending_ip");
foreach ( @devNames ) {
my $model = AttrVal($_, "model", "generic");
my %d = (
name => $_ ,
isDefined => 1,
model => $model,
mode => AttrVal($_, "mode", undef)
);
push @devrefs, \%d;
Log3 $name, 2, "Defined real device $_ for $sending_ip as model $model";
}
$hash->{".ip2device"}->{$sending_ip} = [ @devrefs ];
$ip2devicesDirty = 1;
} else {
Log3 $name, 4, "$sending_ip: in cache, devices=" . join (' ', map { scalar $_->{name} } @devrefs) . " (size=" . scalar @devrefs . ")";
}
my $autoCreate = (defined $hash->{".autoCreate"}) ? 1 : 0;
# Now lets unpack the raw packet data...
my $opt1byte;
my ($b1,$b2,$msgid,$remain) = unpack('CCSA*', $data);
if ($b1 < 0x50 || $b1 > 0x5f) {
Log3 $name, 3, "Unexpected byte at pos 0: " . sprintf("0x%X", $b1) . ", expecting Non-Confirm. w/o token";
$hash->{".ReceivedBroken"}++;
return;
}
if ($b2 != 30) {
$hash->{".ReceivedBroken"}++;
Log3 $name, 3, "Unexpected byte at pos 1: " . sprintf("0x%X", $b2) . ", expecting Code 30";
return;
}
my $tokenlen = $b1 & 0xf;
my $token;
if ($tokenlen>0) {
my $unpackstr = "A${tokenlen}B8A*";
($token,$opt1byte,$remain) = unpack($unpackstr, $remain);
} else {
($opt1byte,$remain) = unpack('B8A*', $remain);
}
my $option = 0;
my $uri = "";
my $global_devid;
my $validity;
my $serial;
# Parsing the options in COAP format...
while ($opt1byte ne "11111111") {
my $optiondelta = oct('0b' . substr($opt1byte, 0, 4));
my $optionlen = oct('0b' . substr($opt1byte, 4));
if ($optiondelta == 13) {
($optiondelta,$remain) = unpack('CA*', $remain);
$optiondelta += 13;
} elsif ($optiondelta == 14) {
($optiondelta,$remain) = unpack('nA*', $remain);
$optiondelta += 269;
}
if ($optionlen == 13) {
($optionlen,$remain) = unpack('CA*', $remain);
$optionlen += 13;
} elsif ($optionlen == 14) {
($optionlen,$remain) = unpack('nA*', $remain);
$optionlen += 269;
}
$option += $optiondelta;
if ($option == 11) {
my $str;
($str,$opt1byte,$remain) = unpack ('A' . $optionlen . 'B8A*', $remain);
$uri .= '/' . $str;
} elsif ($option == $COIOT_OPTION_GLOBAL_DEVID) {
($global_devid,$opt1byte,$remain) = unpack ('A' . $optionlen . 'B8A*', $remain);
} elsif ($option == $COIOT_OPTION_STATUS_VALIDITY) {
($validity,$opt1byte,$remain) = unpack ('nB8A*', $remain);
if ($validity & 1) {
$validity *= 4;
} else {
$validity /= 10;
}
} elsif ($option == $COIOT_OPTION_STATUS_SERIAL) {
($serial,$opt1byte,$remain) = unpack ('vB8A*', $remain);
} else {
$hash->{".ReceivedBroken"}++;
Log3 $name, 3, "Unexpected option $option, only CoIoT V2-options supported";
return;
}
}
# Header parsed, processing data...
my ($devtype, $devid, $devversion) = split (/#/, $global_devid);
foreach ( @devrefs ) {
$_->{expires} = time() + ( $DEVID_TTL_OVERRIDE{$devtype} // $validity );
}
# Handle ignoring of devices
my $ignoreRegexp = $hash->{".ignoreDevices"};
if ($ignoreRegexp) {
@devrefs = grep { $_->{name} !~ qr/$ignoreRegexp/ } @devrefs;
Log3 ($name, 4, "Applied RegExp $ignoreRegexp");
if (! @devrefs || scalar @devrefs == 0) {
Log3 ($name, 4, "Shelly-devices found by IP match ignoreRule");
$hash->{".Ignored"}++;
return;
}
}
Log3 $name, 5, "URI: $uri, global_devid = $global_devid, validity=$validity, serial=$serial";
my $json = $hash->{".JSON"};
return unless ($json);
$data = $json->decode($remain);
my $shellyCoIoTModel;
my $shellyId;
if ($global_devid=~ /(SH[^#]+)#([A-F0-9]{6,12})#/) {
$shellyCoIoTModel = $1;
$shellyId = $2;
}
# Iterate over Shellys found by IP and x-check ID
foreach ( @devrefs ) {
$_->{expires} = time()+$validity;
next unless $_->{isDefined};
my $device = $defs{$_->{name}};
next unless ($device);
if (defined $device->{SHELLYID}) {
if ($device->{SHELLYID} !~ qr/.*$shellyId$/) {
Log3 $name, 1, "Device $_ has ID " . $device->{SHELLYID} . " which does not match $shellyId";
my $dName = $_;
@devrefs = grep { $_->{name} ne $dName } @devrefs;
$hash->{".ip2device"}->{$sending_ip} = [ @devrefs ];
}
} else {
$device->{SHELLYID} = $shellyId;
Log3 $name, 1, "Assigning device $_->{name} SHELLYID $shellyId";
}
}
my %devModel = ();
my $haveAutoCreated = 0;
# Hopefully, all Shellys have an ID with 6-12 Chars...
if (scalar @devrefs==0 && $global_devid=~ /(SH[^#]+)#([A-F0-9]{6,12})#/) {
my $shellyCoIoTModel = $1;
my $shellyId = $2;
my @devsByShellyId = devspec2array("TYPE=Shelly:FILTER=SHELLYID=.*".$shellyId);
if (scalar @devsByShellyId == 1 ) {
# The Shelly-device is already existing, but has changed IP, so lets change the IP
my $oname = $devsByShellyId[0];
delete $hash->{".ip2device"}->{$sending_ip};
my $oldip = $defs{$oname}->{DEF};
CommandDefMod ( undef , $oname . " Shelly $sending_ip");
if (defined $oldip && $oldip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
delete $hash->{".ip2device"}->{$oldip};
Log3 $name, 2, "Removing old ip $oldip for device $oname";
}
my $model = AttrVal($oname, "model", "generic");
my %d = (
name => $oname,
isDefined => 1,
model => $model,
mode => AttrVal($oname, "mode", undef)
);
push @devrefs, \%d;
$hash->{".ip2device"}->{$sending_ip} = [ @devrefs ];
$ip2devicesDirty = 1;
Log3 $name, 2, "Changed IP for device '" . $oname . "' to $sending_ip";
} else {
# No Shelly known by IP nor ID, so lets create a dummy
my $model = $DEVID_MODEL{$shellyCoIoTModel};
my $dname;
if (defined $model) {
$dname = $DEVID_PREFIX{$shellyCoIoTModel};
} else {
$dname = "shelly_generic_" . lc($shellyCoIoTModel);
$dname =~ s/-/_/g;
$dname =~ s/[^a-zA-Z0-9_]//g;
$model = "generic";
}
$dname .= '_' . lc($shellyId);
# Search if a shadow device is already existing with a different IP
my @ips = ( keys %{$hash->{".ip2device"}} );
foreach my $ip ( @ips ) {
my @devrefs2 = getDevicesForIp($hash, $ip);
foreach my $dev ( @devrefs2 ) {
if ($dev->{name} eq $dname && $dev->{isDefined}==0) {
Log3 $name, 2, "shadow device $dname found on old ip $ip, removing";
delete $hash->{".ip2device"}->{$ip};
}
}
}
Log3 $name, 2, "Defined shadow device $dname for $sending_ip as model $model";
my %d = (
name => $dname,
isDefined => 0,
model => $model,
expires => time()+$validity,
attrs => $DEVID_ATTRS{$shellyCoIoTModel},
mcastName => $shellyCoIoTModel
);
push @devrefs, \%d;
$hash->{".ip2device"}->{$sending_ip} = [ @devrefs ];
$ip2devicesDirty = 1;
}
}
foreach ( @devrefs ) {
my $device = $defs{$_->{name}};
next unless ($device);
if ($_->{isDefined}) {
readingsBeginUpdate($device);
$_->{model} = AttrVal($_->{name}, "model", undef);
Log3 $name, 5, "Found device $_->{name}, model $_->{model}";
}
}
my %rgb = ();
my %rgbdevices = ();
foreach my $i ( keys %{$data}) {
if ($i ne "G") {
Log3 $name, 4, "Unexpected JSON array '$i' in data";
next;
}
my $arr = $data->{"G"};
foreach my $j ( @{$arr}) {
my $channel = @{$j}[0];
my $sensorid = @{$j}[1];
my $svalue = @{$j}[2];
my $defarr = $SHELLY_DEF_SEN->{$sensorid};
if (defined $defarr) {
my $rname = $defarr->{"desc"};
if ($rname =~ /^(power|output|energy|brightness|extTemp)_(.).*/ || $rname =~ /^(roller.*|mode|L-.*|colorTemp)$/) {
my $rtype = $1;
my $rno = $2;
foreach ( @devrefs ) {
# We want to set the mode also for shadow devices
my $model = $_->{model};
if ($rtype eq "mode") {
$_->{mode} = $svalue unless $_->{isDefined}==1;
}
next unless $_->{isDefined}==1;
# Only real devices from here on..
my $device = $defs{$_->{name}};
next unless ($device);
if ($rtype eq "power") {
my $subs = ($shelly_models{$model}[3] ==1) ? "" : "_".$rno;
readingsBulkUpdateIfChanged($device, "power" . $subs, $svalue);
} elsif ($rtype eq "energy") {
my $subs = ($shelly_models{$model}[3] ==1) ? "" : "_".$rno;
readingsBulkUpdateIfChanged($device, "energy" . $subs, int($svalue/6)/10);
} elsif ($rtype eq "output") {
my $subs = ($shelly_models{$model}[0] ==1) ? "" : "_".$rno;
my $state = ( $svalue == 0 ? "off" : ( $svalue == 1 ? "on" : undef ));
if ($state) {
readingsBulkUpdateIfChanged($device, "relay" . $subs, $state);
}
} elsif ($rtype eq "brightness") {
my $subs = ($shelly_models{$model}[3] ==1) ? "" : "_".$rno;
readingsBulkUpdateIfChanged($device, "pct" . $subs, $svalue);
} elsif ($rtype eq "rollerStopReason") {
readingsBulkUpdateIfChanged($device, "stop_reason", $svalue);
} elsif ($rtype eq "rollerEnergy") {
readingsBulkUpdateIfChanged($device, "energy", int($svalue/6)/10);
} elsif ($rtype eq "rollerPower") {
readingsBulkUpdateIfChanged($device, "power", $svalue);
} elsif ($rtype eq "rollerPos") {
readingsBulkUpdateIfChanged($device, "pct", $svalue);
my $v = $svalue;
$v = "open" if ($svalue == 100);
$v = "closed" if ($svalue == 0);
readingsBulkUpdateIfChanged($device, "position", $v);
} elsif ($rtype eq "roller") {
my $v = $ROLLER_STATUS_MAP{$svalue};
$v = $svalue unless defined $v;
readingsBulkUpdateIfChanged($device, "state", $v);
readingsBulkUpdateIfChanged($device, "last_dir", "down") if ($svalue eq "close") ;
readingsBulkUpdateIfChanged($device, "last_dir", "up") if ($svalue eq "open") ;
} elsif ($rtype eq "mode" && $haveAutoCreated==1) {
CommandAttr ( undef, $_->{name} . ' mode ' . $svalue);
} elsif ($rtype eq "colorTemp") {
readingsBulkUpdateIfChanged($device, "ct", $svalue);
} elsif ($rtype eq "extTemp") {
readingsBulkUpdateIfChanged($device, $rname, $svalue);
} elsif ($rtype =~ /L-(red|green|blue|white)/) {
$rgb{$1} = $svalue;
$rgbdevices{$_->{name}} = 1;
readingsBulkUpdateIfChanged($device, $rtype, $svalue);
}
}
} else {
# Generic Shelly Device gets any reading in native form
foreach ( @devrefs ) {
# We want to set the mode also for shadow devices
my $model = $_->{model};
my $device = $defs{$_->{name}};
next unless ($device);
$svalue = join (' ', @$svalue) if (ref $svalue eq "ARRAY");
# In case of inputEventCnt_x, we have to determine the old device reading
# to check for changes:
if ($rname =~ /^inputEventCnt_(\d)$/) {
if (ReadingsVal($_->{name}, $rname, "-1") ne $svalue && $svalue>0) {
$_->{inputEventCnt} = {} unless ($_->{inputEventCnt});
$_->{inputEventCnt}->{$1} = 1;
}
}
if ($rname eq "wakeupEvent" && $svalue eq "button") {
# inputEventCnt_0 will be always one, so we force an update:
$_->{inputEventCnt} = { "0" => "1" };
}
readingsBulkUpdateIfChanged($device, $rname, $svalue)
if (defined $device && (( ! defined $model ) || ($model eq "generic")));
}
}
Log3 $name, 5, "$rname = $svalue";
} else {
Log3 $name, 4, "Unknown: c=$channel, sensorid=$sensorid, value=$svalue";
}
}
}
foreach ( @devrefs ) {
if ($_->{isDefined}) {
my $device = $defs{$_->{name}};
if ($rgbdevices{$_->{name}} &&
defined $rgb{"red"} && defined $rgb{"green"} && defined $rgb{"blue"}) {
readingsBulkUpdateIfChanged($device, "rgb",
sprintf("%02X%02X%02X", $rgb{"red"},$rgb{"green"},$rgb{"blue"}));
}
if ($_->{inputEventCnt}) {
foreach my $i ( keys %{$_->{inputEventCnt}}) {
my $rname = "inputEvent_$i";
# Old news is recent news :-)
readingsBulkUpdate ($device, $rname, ReadingsVal($device->{NAME}, $rname, undef));
}
delete $_->{inputEventCnt};
}
readingsEndUpdate($device, 1) if ($device);
}
}
my $nstate = "Statistics: " . $hash->{".Received"} . " msg received, " . $hash->{".ReceivedBroken"} .
" broken, " . $hash->{".Ignored"} . " ignored, " . (0 + (keys %{$hash->{".ReceivedByIp"}})) . " devices";
readingsSingleUpdate ($hash, "state", $nstate, 0);
FW_directNotify("FILTER=$name", "#FHEMWEB:WEB", "location.reload('true')", "") if ($ip2devicesDirty>0);
return(undef);
}
sub ShellyMonitor_detailFn {
my ($FW_wname, $deviceName, $FW_room) = @_;
my $hash = $defs{$deviceName};
my $haveUnsupported = 0;
my $nstate = "<script type=\"text/javascript\">\n" . 'function checkInput(id) { var n=document.getElementById("dn" + id).value; var re=/^[a-z0-9._]+$/i; if (n.match(re)) { document.getElementById("ds" + id).value = document.getElementById("ds" + id).value.concat(n); return true; } else { alert("DeviceName " + n + " is invalid"); return false }};' . "\n</script>\n";
$nstate .= "<div class='makeTable wide'><span class='mkTitle'>Identified Devices</span><table class='block wide'><tr class='odd'><th>IP</th><th>Name</th><th>Model</th><th></th></tr>";
my $cnt = 0;
my @ips = ( keys %{$hash->{".ip2device"}} );
@ips = map substr($_, 4), sort map pack('C4a*', split(/\./), $_), @ips;
my $now = time();
my $formNo = 1;
foreach my $ip ( @ips ) {
my @devrefs = getDevicesForIp($hash, $ip);
foreach my $dev ( @devrefs ) {
if ($dev->{expires} < $now) {
Log3 $hash->{NAME}, 1, "Device " . $dev->{name} . " has expired, no messages seen";
if (scalar @devrefs == 1) {
delete $hash->{".ip2device"}->{$ip};
} else {
@devrefs = grep { $_->{expires} > $now } @devrefs;
$hash->{".ip2device"}->{$ip} = \ @devrefs;
}
next;
}
$nstate .= "<form action=\"$FW_ME\"><input type=\"hidden\" name=\"cmd\" value=\"set $deviceName create $ip \" id=\"ds$formNo\">" unless ($dev->{isDefined});
if ($FW_CSRF =~ /^[&?]([^=]+)=(.*)$/) {
$nstate .= "<input type=\"hidden\" name=\"$1\" value=\"$2\" />";
}
$nstate .= "<tr class='" . (($cnt++)%2==0 ? "even" : "odd") .
"'><td>$ip</td><td>";
if ($dev->{isDefined}) {
$nstate .= "<b><a href=\"fhem?detail=" . $dev->{name} . "\"></b>" . $dev->{name} . "</a></b>";
} else {
$nstate .= "<input type=\"text\" value=\"$dev->{name}\" id=\"dn$formNo\"/>";
}
$nstate .= "</td><td>$dev->{model}";
if (! defined $shelly_models_by_mod_shelly{$dev->{model}} && $dev->{model} ne "generic") {
$nstate .= ", n.s.";
$haveUnsupported = 1;
} else {
$nstate .= " $dev->{mode}" if (defined $dev->{mode});
}
if ($dev->{model} eq "generic" && defined $dev->{mcastName}) {
$nstate .= " (" . $dev->{mcastName} . ")";
}
if ($dev->{isDefined}) {
$nstate .= "</td><td></td></tr>";
} else {
$nstate .= "</td><td><input type=\"submit\" value=\"Define\" onClick=\"return checkInput($formNo);\"></td></tr></form>";
$formNo++;
}
# ($dev->{isDefined} ? "" : "<a href=\"$FW_ME?cmd=set $deviceName autocreate $ip".$FW_CSRF."\">Create</a>" ) .
}
}
$nstate .= "</table>";
$nstate .= "<i>n.s. = not supported by Mod_Shelly</i><br/>" if ($haveUnsupported);
$nstate .= "</div>";
return $nstate;
}
#####################################
sub ShellyMonitor_Read
{
my $hash = shift;
if( $init_done ) {
ShellyMonitor_DoRead($hash);
# my $new_state = "Statistics: " . $hash->{".Received"} . " msg received, " . $hash->{".ReceivedBroken"} . " broken, " . $hash->{".Ignored"} . " ignored, " . (0 + (keys %{$hash->{".ReceivedByIp"}})) . " devices";
# readingsSingleUpdate($hash,"state",$new_state,1);
}
return(undef);
}
#####################################
sub ShellyMonitor_Notify
{
my ($hash, $dev_hash) = @_;
my $ownName = $hash->{NAME}; # own name / hash
return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled
my $devName = $dev_hash->{NAME}; # Device that created the events
my $events = deviceEvents($dev_hash,1);
return if( !$events );
my $ip2devicesDirty = 0;
foreach my $event (@{$events}) {
$event = "" if(!defined($event));
next unless ($event =~ /^(RENAMED|DELETED|DEFINED|MODIFIED).*/);
my ($evType, $evDev1, $evDev2) = split (/ /, $event);
my @ips = ( keys %{$hash->{".ip2device"}} );
$ip2devicesDirty = 0;
foreach my $ip ( @ips ) {
my @devrefs = getDevicesForIp($hash, $ip);
foreach my $dev ( @devrefs ) {
next unless ($dev->{name} eq $evDev1);
$ip2devicesDirty = 1;
delete $hash->{".ip2device"}->{$ip} if ($evType eq "DELETED");
$dev->{name} = $evDev2 if ($evType eq "RENAMED");
$dev->{isDefined} = 1 if ($evType eq "DEFINED");
}
}
if ($ip2devicesDirty==0) {
Log3 $hash->{NAME}, 4, "Did not find device $evDev1 in cache...";
my $ohash = $defs{$evDev1};
if (defined $ohash && $ohash->{TYPE} eq "Shelly") {
# We did not find it, and it was something about Shellys:
# Be paranoic, clear cache...
Log3 $hash->{NAME}, 4, "... but its a shelly, so clear cache";
$hash->{".ip2device"} = ();
$ip2devicesDirty = 1
}
}
Log3 $hash->{NAME}, 4, "Modified ip2device-cache on event: $event";
}
FW_directNotify("#FHEMWEB:WEB", "location.reload('true')", "") if ($ip2devicesDirty>0);
}
#####################################
sub ShellyMonitor_Ready
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $dev=$hash->{".McastInterface"};
# This is relevant for windows/USB only
my $po = $hash->{USBDev};
my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags);
return if (!$po);
($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
return ($InBytes>0);
}
sub ShellyMonitor_Set
{
my ($hash, $name, $sName, $sValue, $devName) = @_;
return "autocreate create" if ($sName eq "?");
return "only autocreate and create vailable" if ($sName !~ /(auto|)create/);
if ($sName eq "autocreate") {
$sValue = ".*" unless (defined $sValue);
return "autocreate only takes an IP address pattern as optional value" if (defined $devName);
} else {
$sValue = '^' . $sValue . '$';
$sValue =~ s/\./\\./g;
}
my $created = 0;
my @ips = grep { $_ =~ qr/^$sValue$/ } ( keys %{$hash->{".ip2device"}} );
return "Provided IP $sValue did not match any IPs" unless (scalar @ips>=1);
foreach my $ip ( @ips ) {
my @devrefs = getDevicesForIp($hash, $ip);
next unless (@devrefs);
my $device = $devrefs[0];
next if ($device->{isDefined});
Log3 $name, 1, "AutoCreate called for IP $ip, #devices=" . scalar @devrefs;
my $dname = defined $devName ? $devName : $device->{name};
$device->{name} = $dname;
my $model = $device->{model};
my $mode = $device->{mode} if ($model =~ /(shelly2|shelly2.5|shellyrgbw|shellybulb)/);
my $r;
if ($sName eq "autocreate") {
$r = DoTrigger("global", "UNDEFINED $dname Shelly $ip");
} else {
$r = CommandDefine(undef, "$dname Shelly $ip");
}
Log3 $name, 1, "AutoCreating $dname returned $r" if ($r);
if (defined $shelly_models_by_mod_shelly{$model}) {
CommandAttr ( undef, $dname . ' model ' . $model);
} elsif ($shelly_models_by_mod_shelly{"shellygeneric"}) {
CommandAttr ( undef, $dname . ' model shellygeneric');
}
return "Creation of device '$dname' failed" unless ($defs{$dname});
my $attrs = $device->{attrs};
if ($attrs) {
foreach (@{$attrs}) {
CommandAttr ( undef, $dname . ' ' . $_ );
}
}
if (defined $mode) {
CommandAttr ( undef, $dname . ' mode ' . $mode) if ($model ne "generic");;
if ($model =~ /shellybulb/) {
CommandAttr ( undef, $dname . ' webCmd ' . ($mode eq "white" ? 'pct:ct:on:off' : 'rgb:on:off' ) );
}
if ($model =~ /shellyrgb.*/ && ($mode eq "color") ) {
CommandAttr ( undef, $dname . ' webCmd rgb:on:off' );
}
}
if ($model =~ /shellydimmer.*/) {
CommandAttr ( undef, $dname . ' webCmd pct:on:off' );
}
CommandAttr ( undef, $dname . ( $model eq 'generic' ? ' interval 0' : ' interval 600' ) );
$created++;
}
FW_directNotify("#FHEMWEB:WEB", "location.reload('true')", "") if ($created>0);
return;
}
sub ShellyMonitor_Attr
{
my ($cmd,$name,$aName,$aVal) = @_;
# $cmd can be "del" or "set"
# $name is device name
# aName and aVal are Attribute name and value
my $hash = $defs{$name};
my $dev=$hash->{".McastInterface"};
if ($aName eq "ignoreDevices") {
$hash->{".ignoreDevices"} = ($cmd eq "set" ) ? $aVal : undef;
# } elsif ($aName eq "autoCreate") {
# $hash->{".autoCreate"} = ($cmd eq "set" && $aVal eq "Shelly") ? $aVal : undef;
}
return;
}
sub getDevicesForIp {
my ($hash, $ip) = @_;
my $ip2devices = $hash->{".ip2device"}->{$ip};
return unless ($ip2devices);
my @devrefs = @{$ip2devices};
foreach ( @devrefs ) {
if (ref $_ ne "HASH") {
delete $hash->{".ip2device"}->{$ip};
@devrefs = ();
Log3 $hash->{NAME}, 1, "Panic, it happened: Cache for $ip did contain a none-hash, $_ instead";
}
}
return @devrefs;
}
sub completecheck {
my ($hash) = @_;
my @is = ( keys %{$hash->{".ip2device"}} );
foreach my $ip ( @is ) {
my @devrefs = getDevicesForIp($hash, $ip);
Log3 $hash->{NAME}, 4, "IP: $ip " . scalar @devrefs . " entries";
}
}
1;
=pod
=item device
=item summary Listens to CoIoT-Messages sent by Shellys and updates readings
=item summary_DE Wertet CoIoT-Pakete von Shelly-Geräten aus und aktualisiert die Readings
=begin html
<a name="ShellyMonitor"></a>
<h3>ShellyMonitor</h3>
<ul>
This module is for Shelly-devices, that report their data in the CoIoT-"standard" (based on COAP).
Defined devices are updated in their readings, non-defined devices found are displayed in FHEMWEB
in a table, where they might be created with a click.
<br><br>
<h4>Requirements</h4>
ShellyMonitor needs two additional Perl modules:
<ul>
<li><i>JSON</i><br>
For Raspian Buster by <code>sudo apt-get install libjson-perl</code> installable,
or by <code>sudo cpan install JSON</code>
</li>
<li><i>IO::Socket::Multicast</i><br>
For Raspian Buster by <code>sudo apt-get install libio-socket-multicast-perl</code> installable,
or with <code>sudo cpan install IO::Socket::Multicast</code>
</li>
</ul>
<h4>Define</h4>
<ul>
<code>define &lt;name&gt; ShellyMonitor [interface]</code><br>
<br>
&lt;interface&gt; is necessary if the computers primary interface is not the one
with the multicast messages. E.g., it might be "wlan0".
<br>
</ul>
<h4>Set</h4>
<ul>
<li><code>set &lt;name&gt; autocreate [&lt;ip regexp&gt;]</code><br>
Setting this command triggers the creation of all discovered, but not yet defined
devices that have an IP address matching to pattern. Without a pattern,
all devices are created.<br>
Creation implies:
<ul>
<li>a <b>define</b> via the autocreate module mechanism with the systematic name displayed in the WEB table</li>
<li>setting the <b>model</b> attribute for the device</li>
<li>setting the <b>mode</b> attribute if applicable</li>
<li>setting the <b>webCmd</b> attribute if applicable</li>
</ul>
</li>
<li><code>set &lt;name&gt; create &lt;ip address&gt; [&lt;device name&gt;]</code><br>
The device specified by its IP address is created with the optionally given
device name. Unlike <code>autocreate</code>, a direct <code>define</code> is
executed, and the features of the autocreate module (FileLog-device, room-attribute)
are not assigned.<br/>
The other attributes described for the <code>autocreate</code>-set command are assigned.
</li>
</ul>
<h4>Attributes</h4>
<ul><li>
<code>ignoreDevices</code><br>
Regular expression for Shelly device (names) that shall be ignored.
E.g., setting this to "<code>.*</code>" will not update any devices
</li>
<br>
</ul></ul>
=end html
=begin html_DE
<a name="ShellyMonitor"></a>
<h3>ShellyMonitor</h3>
<ul>
Dieses Modul aktualisiert die Readings von Shelly-Geräten, die ihre Daten im CoIoT-"Standard" (Abwandlung von COAP) im Netzwerk versenden. Die gefundenen Ger&auml;te werden in FHEMWEB in einer Tabelle angezeigt, wo sie sich
mit einem Klick erzeugen und anschlie&szlig;end ggf. umbenennen lassen.
<br><br>
<h4>Anforderungen</h4>
ShellyMonitor ben&ouml;tigt zwei zus&auml;tzliche Perl-Pakete:
<ul>
<li><i>JSON</i><br>
Unter Raspian Buster per <code>sudo apt-get install libjson-perl</code> installierbar,
oder per <code>sudo cpan install JSON</code>
</li>
<li><i>IO::Socket::Multicast</i><br>
Unter Raspian Buster per <code>sudo apt-get install libio-socket-multicast-perl</code> installierbar,
oder per <code>sudo cpan install IO::Socket::Multicast</code>
</li>
</ul>
<h4>Define</h4>
<ul>
<code>define &lt;name&gt; ShellyMonitor [interface]</code><br>
<br>
&lt;interface&gt; ist nötig, falls das primäre Netzwerk-Interface nicht das
Netz ist, in dem die Multicast-Pakete versendet werden.
Beispielsweise "wlan0" oder "eth0"
<br>
</ul>
<h4>Set</h4>
<ul>
<li><code>set &lt;name&gt; autocreate [&lt;ip regexp&gt;]</code><br>
Mit diesem Kommando werden alle gefundenen Shelly-Ger&auml;te, die noch nicht angelegt
wurden, erzeugt, sofern ihre aktuelle IP-Adresse dem regul&auml;ren Ausdruck entspricht.
Ohne diesen Parameter werden alle gefundenen Ger&auml;te erzeugt.
<br>
Die Erzeugung umfasst:
<ul>
<li>a <b>define</b> &uuml;ber das autocreate-Modul mit dem systematischen Namen, der in
der Tabelle angezeigt wird</li>
<li>Setzen des <b>model</b>-Attributs f&uuml;r das Ger&auml;t</li>
<li>Setzen des <b>mode</b>-Attributs, sofern beim Ger&auml;t vorhanden</li>
<li>Setzen eines <b>webCmd</b>-Attributs, falls sinnvoll</li>
</ul>
</li>
<li><code>set &lt;name&gt; create &lt;ip address&gt; [deviceName]</code><br>
Mit diesem Kommando wird das durch die angegebene IP-Adresse spezifizierte Ger&auml;t
unter dem als <i>deviceName</i> optional angegebenen Namen erzeugt.
Anders als bei <code>autocreate</code> wird das Ger&auml;t nicht &uuml;ber das
autocreate-Modul erzeugt. Es wird daher kein Raum zugewiesen und kein FileLog-Device
angelegt. Die Attribute werden hingegen wie bei <code>autocreate</code> beschrieben zugewiesen.
</li>
</ul>
<h4>Attribute</h4>
<ul><li>
<code>ignoreDevices</code><br>
Regulärer Ausdruck, welche Shelly-Geräte nicht aktualisiert werden sollen.
Beispielsweise werden mit "<code>.*</code>" alle Geräte ignoriert.
Der Ausdruck bezieht sich auf den Ger&auml;tenamen.
</li>
<br>
</ul></ul>
=end html_DE
=cut