############################################## # 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"}, # 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)."' . '".FW_makeImage($light)."
$cons W / $kwh
"}'; 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 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 = "\n"; $nstate .= "
Identified Devices"; 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 .= "" unless ($dev->{isDefined}); if ($FW_CSRF =~ /^[&?]([^=]+)=(.*)$/) { $nstate .= ""; } $nstate .= ""; } else { $nstate .= ""; $formNo++; } # ($dev->{isDefined} ? "" : "Create" ) . } } $nstate .= "
IPNameModel
$ip"; if ($dev->{isDefined}) { $nstate .= "{name} . "\">" . $dev->{name} . ""; } else { $nstate .= "{name}\" id=\"dn$formNo\"/>"; } $nstate .= "$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 .= "
"; $nstate .= "n.s. = not supported by Mod_Shelly
" if ($haveUnsupported); $nstate .= "
"; 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

ShellyMonitor

=end html =begin html_DE

ShellyMonitor

=end html_DE =cut