##############################################
# 36_ShellyMonitor.pm
#
# Parses the MultiCast "COAP" messages of Shellys and updates
# devices accordingly
#
# $Id: 36_ShellyMonitor.pm 27981 2023-09-18 20:54:54Z gvzdus $
#

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"},
# Used by Shelly TRV1
"3103trv" => { "type"=>"T", "desc"=>"targetTemp", "unit"=>"C"},
"3104trv" => { "type"=>"T", "desc"=>"targetTempF", "unit"=>"F"},
# Used by Shelly HT
"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"},
# Used by Shelly TRV1
"3116trv" => { "type"=>"S", "desc"=>"valveError"},
"3117trv" => { "type"=>"S", "desc"=>"mode"},
"3118" => { "type"=>"S", "desc"=>"mode"},
"3119" => { "type"=>"S", "desc"=>"timestamp"},
"3120" => { "type"=>"S", "desc"=>"active"},
"3121" => { "type"=>"S", "desc"=>"valvePos", "unit"=>"%"},
"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,NG)
    "generic" => [4,4,4,4,0],
    "shellygeneric" => [4,4,4,4,0],
    "shelly1" => [1,0,0,0,0],
    "shelly1pm" => [1,0,0,1,0],
    "shelly2" => [2,1,0,1,0],
    "shelly2.5" => [2,1,0,2,0],
    "shellyplug" => [1,0,0,1,0],
    "shelly4" => [4,0,0,4,0],
    "shellypro4" => [4,0,0,4,0],
    "shellyrgbw" => [0,0,4,1,0],
    "shellydimmer" => [0,0,1,1,0],
    "shellyem" => [1,0,0,2,0],
    "shelly3em" => [1,0,0,3,0],
    "shellybulb" => [0,0,1,1,0],
    "shellyuni" => [2,0,0,1,0],
    #-- 2nd generation devices
    "shellyplus1" => [1,0,0,0,1],
    "shellyplus1pm" => [1,0,0,1,1],
    "shellypro4pm" => [4,0,0,4,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",
	"SHEM"     => "shellyem",
	"SHEM-3"   => "shelly3em",
    "SHMOS-01" => "generic",
    "SHWT-1"   => "generic",
    "SHHT-1"   => "generic",
    "SHGS-1"   => "generic",
    "SHBTN-2"  => "generic",
    "SHIX3-1"  => "generic",
	"SHTRV-01" => "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",
	"SHTRV-01" => "shelly_trv",
	"SHEM"     => "shelly_em",
	"SHEM-3"   => "shelly_em3"
);

# 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 (/,/, $2);
    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 ($shellyCoIoTModel=~/^SHTRV.*/ && defined $SHELLY_DEF_SEN->{$sensorid . "trv"}) {
		  $defarr = $SHELLY_DEF_SEN->{$sensorid . "trv"};
		}

        if (defined $defarr) {
          my $rname = $defarr->{"desc"};

          if ($rname =~ /^(power|output|energy|energyReturned|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 =~ /^energy/) {
                my $subs = ($shelly_models{$model}[3] ==1) ? "" : "_".$rno;
                readingsBulkUpdateIfChanged($device, $rtype . $subs,
                   $defarr->{"unit"} eq "Wmin" ? int($svalue/6)/10 : $svalue);
              } elsif ($rtype eq "output") {
                my $state = ( $svalue == 0 ? "off" : ( $svalue == 1 ? "on" : undef ));
                my $no_relais = $shelly_models{$model}[0];
                if ($no_relais == 0) {
                  readingsBulkUpdateIfChanged($device, "state", $state);
                } else {
                  if ($no_relais == 1) {
                    readingsBulkUpdateIfChanged($device, "relay", $state);
                  } else {
                    my $sname = "relay_" . $rno;
                    readingsBulkUpdateIfChanged($device, $sname, $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');
    } elsif ($shelly_models_by_mod_shelly{"generic"}) {
      CommandAttr ( undef, $dname . ' model generic');
    }
    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
