######################################################################################## # # Shelly.pm # # FHEM module to communicate with Shelly switch/roller actor devices # Prof. Dr. Peter A. Henning, 2018 # # $Id$ # ######################################################################################## # # This programm is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # ######################################################################################## package main; use strict; use warnings; use JSON; # imports encode_json, decode_json, to_json and from_json. use vars qw{%attr %defs}; sub Log($$); #-- globals on start my $version = "1.81"; #-- these we may get on request my %gets = ( "status:noArg" => "S", "registers:noArg" => "R", "config" => "C", "version:noArg" => "V" ); #-- these we may set my %setssw = ( "on" => "O", "off" => "F", "on-for-timer" => "T", "off-for-timer" => "E", "config" => "K", "password" => "W" ); my %setsrol = ( "closed:noArg" => "C", "open:noArg" => "O", "stop:noArg" => "S", "config" => "K", "password" => "W", "pct:slider,0,1,100" => "P", "zero:noArg" => "Z" ); my %shelly_models = ( #(relays,rollers,meters) "shelly1" => [1,0,0,], "shelly2" => [2,1,1], "shellyplug" => [1,0,1], "shelly4" => [4,0,4] ); my %shelly_regs = ( "relay" => "reset=1\x{27f6}factory reset\ndefault_state=off|on|last|switch\x{27f6}state after power on\nbtn_type=momentary|toggle|edge\x{27f6}type of local button\nauto_on=<seconds>\x{27f6}timed on\nauto_off=<seconds>\x{27f6}timed off", "roller" => "reset=1\x{27f6}factory reset\ndefault_state=stop|open|close|switch\x{27f6}state after power on\nswap=true|false\x{27f6}swap open and close\ninput_mode=openclose|onebutton\x{27f6}two or one local button\n". "btn_type=momentary|toggle\x{27f6}type of local button\nobstacle_mode=disabled|while_opening|while_closing|while_moving\x{27f6}when to watch\nobstacle_action=stop|reverse\x{27f6}what to do\n". "obstacle_power=<watt>\x{27f6}power threshold for detection\nobstacle_delay=<seconds>\x{27f6}delay after motor start to watch\n". "safety_mode=disabled|while_opening|while_closing|while_moving\x{27f6}safety mode=2nd button\nsafety_action=stop|pause|reverse\x{27f6}action when safety mode\n". "safety_allowed_on_trigger=none|open|close|all\x{27f6}commands allowed in safety mode" ); ######################################################################################## # # Shelly_Initialize # # Parameter hash # ######################################################################################## sub Shelly_Initialize ($) { my ($hash) = @_; $hash->{DefFn} = "Shelly_Define"; $hash->{UndefFn} = "Shelly_Undef"; $hash->{AttrFn} = "Shelly_Attr"; $hash->{GetFn} = "Shelly_Get"; $hash->{SetFn} = "Shelly_Set"; #$hash->{NotifyFn} = "Shelly_Notify"; #$hash->{InitFn} = "Shelly_Init"; $hash->{AttrList}= "verbose model:".join(",",(keys %shelly_models))." mode:relay,roller defchannel maxtime maxpower interval pct100:open,closed shellyuser ". $readingFnAttributes; } ######################################################################################## # # Shelly_Define - Implements DefFn function # # Parameter hash, definition string # ######################################################################################## sub Shelly_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "[Shelly] Define the IP address of the Shelly device as a parameter" if(@a != 3); return "[Shelly] Invalid IP address ".$a[2]." of Shelly device" if( $a[2] !~ m|\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?(\:\d+)?| ); my $dev = $a[2]; #-- split into parts my @tcp = split(':',$dev); #-- when the specified ip address contains a port already, use it as supplied if ( $tcp[1] ){ $hash->{TCPIP} = $dev; }else{ $hash->{TCPIP} = $tcp[0].":80"; }; $hash->{DURATION} = 0; $hash->{MOVING} = "stopped"; delete $hash->{BLOCKED}; $hash->{INTERVAL} = 60; $modules{Shelly}{defptr}{$a[0]} = $hash; #-- InternalTimer blocks if init_done is not true my $oid = $init_done; $init_done = 1; readingsBeginUpdate($hash); my $err = Shelly_status($hash); if( !defined($err) ){ readingsBulkUpdate($hash,"state","initialized"); readingsBulkUpdate($hash,"network","connected"); }else{ readingsBulkUpdate($hash,"state",$err); readingsBulkUpdate($hash,"network","not connected"); } readingsEndUpdate($hash,1); $init_done = $oid; return undef; } ####################################################################################### # # Shelly_Undef - Implements UndefFn function # # Parameter hash = hash of device addressed # ####################################################################################### sub Shelly_Undef ($) { my ($hash) = @_; my $name = $hash->{NAME}; delete($modules{Shelly}{defptr}{NAME}); RemoveInternalTimer($hash); my ($err, $sh_pw) = setKeyValue("SHELLY_PASSWORD_$name", undef); return undef; } ####################################################################################### # # Shelly_Attr - Set one attribute value # ######################################################################################## sub Shelly_Attr(@) { my ($cmd,$name,$attrName, $attrVal) = @_; my $hash = $main::defs{$name}; my $ret; my $model = AttrVal($name,"model","shelly2"); my $mode = AttrVal($name,"mode","relay"); #-- temporary code delete $hash->{BLOCKED}; #--------------------------------------- if ( ($cmd eq "set") && ($attrName =~ /model/) ) { Log 1,"[Shelly_Attr] =====> Attribute model=shelly2beta deprecated, please replace by model=shelly2" if( $model eq "shelly2beta" ); my $regex = "((".join(")|(",(keys %shelly_models))."))"; if( $attrVal !~ /$regex/){ Log3 $name,1,"[Shelly_Attr] wrong value of model attribute, see documentation for possible values"; return } #-- only one channel if( $shelly_models{$model}[0] == 1){ fhem("deletereading ".$name." relay_.*"); fhem("deletereading ".$name." overpower_.*"); }else{ fhem("deletereading ".$name." relay"); fhem("deletereading ".$name." overpower"); } #-- no rollers if( $shelly_models{$model}[1] == 0){ fhem("deletereading ".$name." position.*"); fhem("deletereading ".$name." stop_reason.*"); fhem("deletereading ".$name." last_dir.*"); fhem("deletereading ".$name." pct.*"); delete $hash->{MOVING}; delete $hash->{DURATION}; } #-- always clear readings for meters #if( $shelly_models{$model}[2] <= 1){ fhem("deletereading ".$name." power.*"); fhem("deletereading ".$name." overpower.*"); #} #--------------------------------------- }elsif ( ($cmd eq "set") && ($attrName =~ /mode/) ) { if( $model !~ /shelly2.*/ ){ Log3 $name,1,"[Shelly_Attr] setting the mode attribute only works for model=shelly2"; return } if( $attrVal !~ /((relay)|(roller))/){ Log3 $name,1,"[Shelly_Attr] wrong mode value $attrVal"; return; }elsif( $attrVal eq "relay"){ fhem("deletereading ".$name." position.*"); fhem("deletereading ".$name." stop_reason.*"); fhem("deletereading ".$name." last_dir.*"); fhem("deletereading ".$name." pct.*"); }elsif( $attrVal eq "roller"){ fhem("deletereading ".$name." relay.*"); fhem("deletereading ".$name." overpower.*"); } Shelly_configure($hash,"settings?mode=".$attrVal); #--------------------------------------- }elsif ( ($cmd eq "set") && ($attrName eq "maxtime") ) { if( ($model !~ /shelly2.*/) || ($mode ne "roller" ) ){ Log3 $name,1,"[Shelly_Attr] setting the maxtime attribute only works for model=shelly2 and mode=roller"; return } Shelly_configure($hash,"settings?maxtime=".$attrVal); #--------------------------------------- }elsif ( ($cmd eq "set") && ($attrName eq "pct100") ) { if( ($model !~ /shelly2.*/) || ($mode ne "roller" ) ){ Log3 $name,1,"[Shelly_Attr] setting the pct100 attribute only works for model=shelly2 and mode=roller"; return } #--------------------------------------- }elsif ( ($cmd eq "set") && ($attrName eq "interval") ) { #-- update timer $hash->{INTERVAL} = int($attrVal); if ($init_done) { RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 0); } } return } ######################################################################################## # # Shelly_Get - Implements GetFn function # # Parameter hash, argument array # ######################################################################################## sub Shelly_Get ($@) { my ($hash, @a) = @_; #-- check syntax my $name = $hash->{NAME}; my $v; my $model = AttrVal($name,"model",""); my $mode = AttrVal($name,"mode",""); #-- get version if( $a[1] eq "version") { return "$name.version => $version"; #-- current status }elsif($a[1] eq "status") { $v = Shelly_status($hash); #-- some help on registers }elsif($a[1] eq "registers") { my $txt = "relay"; $txt = "roller" if( ($model =~ /shelly2.*/) && ($mode eq "roller") ); return $shelly_regs{$txt}."\n\nSet/Get these registers by calling set/get $name config <registername> [<channel>] <value>"; #-- configuration register }elsif($a[1] eq "config") { my $reg = $a[2]; my ($chan,$val); if( int(@a) == 5 ){ $chan = $a[3]; $val = $a[4]; }elsif( int(@a) == 4 ){ $chan = 0; $val = $a[3]; }else{ my $msg = "Error: wrong number of parameters"; Log3 $name,1,"[Shelly_Get] ".$msg; return $msg; } my $pre = "settings/"; if( ($model =~ /shelly2.*/) && ($mode eq "roller") ){ $pre .= "roller/$chan?"; }else{ $pre .= "relay/$chan?"; $v = Shelly_configure($hash,$pre.$reg); } #-- else } else { my $newkeys = join(" ", sort keys %gets); $newkeys =~ s/:noArg//g if( $a[1] ne "?"); return "[Shelly_Get] with unknown argument $a[1], choose one of ".$newkeys; } if(defined($v)) { return "$a[0] $a[1] => $v"; } return "$a[0] $a[1] => ok"; } ######################################################################################## # # Shelly_Set - Implements SetFn function # # Parameter hash, a = argument array # ######################################################################################## sub Shelly_Set ($@) { my ($hash, @a) = @_; my $name = shift @a; my ($newkeys,$cmd,$value,$v,$msg); $cmd = shift @a; $value = shift @a; my $model = AttrVal($name,"model","shelly2"); my $mode = AttrVal($name,"mode","relay"); my ($channel,$time); #-- we have a Shelly 1,4 or ShellyPlug switch type device #-- or we have a Shelly 2 switch type device if( ($model eq "shelly1") || ($model eq "shelly4") || ($model eq "shellyplug") || (($model =~ /shelly2.*/) && ($mode eq "relay")) ){ #-- WEB asking for command list if( $cmd eq "?" ) { $newkeys = join(" ", sort keys %setssw); return "[Shelly_Set] Unknown argument " . $cmd . ", choose one of ".$newkeys; } if( $cmd =~ /^((on)|(off)).*/ ){ $channel = $value; if( $cmd =~ /(.*)-for-timer/ ){ $time = $value; $channel = shift @a; } if( $shelly_models{$model}[0] == 1){ $channel = 0 }else{ if( !defined($channel) || ($channel !~ /[0123]/) || $channel >= $shelly_models{$model}[0] ){ if( !defined($channel) ){ $channel = AttrVal($name,"defchannel",undef); if( !defined($channel) ){ $msg = "Error: wrong channel $channel given and defchannel attribute not set properly"; Log3 $name, 1,"[Shelly_Set] ".$msg; return $msg; }else{ Log3 $name, 4,"[Shelly_Set] switching default channel $channel"; } } } } if( $cmd =~ /(.*)-for-timer/ ){ $cmd = $1; if( $time !~ /\d+/ ){ $msg = "Error: wrong time spec $time, must be "; Log3 $name, 1,"[Shelly_Set] ".$msg; return $msg; } $cmd = $cmd."&timer=$time"; } Shelly_onoff($hash,$channel,"?turn=".$cmd); } #-- we have a Shelly 2 roller type device }elsif( ($model =~ /shelly2.*/) && ($mode eq "roller") ){ my $channel = $value; my $max=AttrVal($name,"maxtime",undef); #-- WEB asking for command list if( $cmd eq "?" ) { $newkeys = join(" ", sort keys %setsrol); return "[Shelly_Set] Unknown argument " . $cmd . ", choose one of ".$newkeys; } if( $cmd eq "zero" ) { Shelly_configure($hash,"rc"); } if( ($hash->{MOVING} ne "stopped") && ($cmd ne "stop") ){ $msg = "Error: roller blind still moving, wait for some time"; Log3 $name,1,"[Shelly_Set] ".$msg; return $msg } #-- open 100% or 0% ? my $pctnormal = (AttrVal($name,"pct100","open") eq "open"); if( $cmd eq "stop" ){ Shelly_updown($hash,"?go=stop"); # -- estimate pos here ??? $hash->{DURATION} = 0; }elsif( $cmd eq "closed" ){ $hash->{MOVING} = "moving_down"; $hash->{DURATION} = $max; $hash->{TARGETPCT} = $pctnormal ? 0 : 100; Shelly_updown($hash,"?go=close"); }elsif( $cmd eq "open" ){ $hash->{MOVING} = "moving_up"; $hash->{DURATION} = $max; $hash->{TARGETPCT} = $pctnormal ? 100 : 0; Shelly_updown($hash,"?go=open"); }elsif( $cmd eq "pct" ){ my $targetpct = $value; my $pos = ReadingsVal($name,"position",""); my $pct = ReadingsVal($name,"pct",undef); if( !$max ){ Log3 $name,1,"[Shelly_Set] please set the maxtime attribute for proper operation"; $max = 20; } $time = int(abs($targetpct-$pct)/100*$max); $cmd = "?go=to_pos&roller_pos=".$targetpct; $hash->{MOVING} = $pctnormal ? (($targetpct > $pct) ? "moving_up" : "moving_down") : (($targetpct > $pct) ? "moving_down" : "moving_up"); $hash->{DURATION} = $time; $hash->{TARGETPCT} = $targetpct; Shelly_updown($hash,$cmd); } } #-- configuration register if($cmd eq "config") { my $reg = $value; my ($chan,$val); if( int(@a) == 2 ){ $chan = $a[0]; $val = $a[1]; }elsif( int(@a) == 1 ){ $chan = 0; $val = $a[0]; }else{ my $msg = "Error: wrong number of parameters"; Log3 $name,1,"[Shelly_Set] ".$msg; return $msg; } my $pre = "settings/"; if( ($model eq "shelly2") && ($mode eq "roller") ){ $pre .= "roller/$chan?"; }else{ $pre .= "relay/$chan?"; $v = Shelly_configure($hash,$pre.$reg."=".$val); } } #-- password if($cmd eq "password") { my $user = AttrVal($name, "shellyuser", ''); if(!$user){ my $msg = "Error: password can be set only if attribute shellyuser is set"; Log3 $name,1,"[Shelly_Set] ".$msg; return $msg; } setKeyValue("SHELLY_PASSWORD_$name", $value); } return undef; } ######################################################################################## # # Shelly_pwd - retrieve the credentials if set # # Parameter hash # ######################################################################################## sub Shelly_pwd($){ my ($hash) = @_; my $name = $hash->{NAME}; my $user = AttrVal($name, "shellyuser", ''); return "" if(!$user); my ($err, $pw) = getKeyValue("SHELLY_PASSWORD_$name"); return $user.":".$pw."@"; } ######################################################################################## # # Shelly_configure - Configure Shelly device # acts as callable program Shelly_configure($hash,$channel,$cmd) # and as callback program Shelly_configure($hash,$channel,$cmd,$err,$data) # # Parameter hash, channel = 0,1 cmd = command # ######################################################################################## sub Shelly_configure { my ($hash, $cmd, $err, $data) = @_; my $name = $hash->{NAME}; my $url; my $state = $hash->{READINGS}{state}{VAL}; my $net = $hash->{READINGS}{network}{VAL}; return if( $net ne "connected" ); my $model = AttrVal($name,"model",""); my $creds = Shelly_pwd($hash); if ( $hash && !$err && !$data ){ $url = "http://$creds".$hash->{TCPIP}."/".$cmd; Log3 $name, 5,"[Shelly_configure] Issue a non-blocking call to $url"; HttpUtils_NonblockingGet({ url => $url, callback=>sub($$$){ Shelly_configure($hash,$cmd,$_[1],$_[2]) } }); return undef; }elsif ( $hash && $err ){ #Log3 $name, 1,"[Shelly_configure] has error $err"; readingsSingleUpdate($hash,"state","Error",1); return; } Log3 $name, 5,"[Shelly_configure] has obtained data $data"; my $json = JSON->new->utf8; my $jhash = eval{ $json->decode( $data ) }; if( !$jhash ){ Log3 $name,1,"[Shelly_configure] has invalid JSON data"; readingsSingleUpdate($hash,"state","Error",1); return; } #-- isolate register name my $reg = substr($cmd,index($cmd,"?")+1); my $val = $jhash->{$reg}; $val = "" if(!defined($val)); readingsSingleUpdate($hash,"config",$reg."=".$val,1); return undef; } ######################################################################################## # # Shelly_status - Retrieve data from device # acts as callable program Shelly_status($hash) # and as callback program Shelly_status($hash,$err,$data) # # Parameter hash # ######################################################################################## sub Shelly_status { my ($hash, $err, $data) = @_; my $name = $hash->{NAME}; my $url; my $state = $hash->{READINGS}{state}{VAL}; my $creds = Shelly_pwd($hash); if ( $hash && !$err && !$data ){ $url = "http://$creds".$hash->{TCPIP}."/status"; Log3 $name, 5,"[Shelly_status] Issue a non-blocking call to $url"; HttpUtils_NonblockingGet({ url => $url, callback=>sub($$$){ Shelly_status($hash,$_[1],$_[2]) } }); return undef; }elsif ( $hash && $err ){ Log3 $name, 1,"[Shelly_status] has error $err"; readingsSingleUpdate($hash,"state","Error",1); readingsSingleUpdate($hash,"network","not connected",1); #-- cyclic update nevertheless RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1) if( $hash->{INTERVAL} ne "0" ); return $err; } Log3 $name, 5,"[Shelly_status] has obtained data $data"; my $json = JSON->new->utf8; my $jhash = eval{ $json->decode( $data ) }; if( !$jhash ){ Log3 $name,1,"[Shelly_status] invalid JSON data"; readingsSingleUpdate($hash,"state","Error",1); return; } my $model = AttrVal($name,"model","shelly2"); my $mode = AttrVal($name,"mode","relay"); my $channels = $shelly_models{$model}[0]; my $rollers = $shelly_models{$model}[1]; my $meters = $shelly_models{$model}[2]; my ($subs,$ison,$overpower,$rpower,$rstate,$power,$rstopreason,$rcurrpos,$position,$rlastdir,$pct,$pctnormal); readingsBeginUpdate($hash); readingsBulkUpdateIfChanged($hash,"network","connected",1); #-- we have a Shelly 1, Shelly 4, Shelly 2 or ShellyPlug switch type device if( ($model eq "shelly1") || ($model eq "shellyplug") || ($model eq "shelly4") || (($model =~ /shelly2.*/) && ($mode eq "relay")) ){ for( my $i=0;$i<$channels;$i++){ $subs = (($channels == 1) ? "" : "_".$i); $ison = $jhash->{'relays'}[$i]{'ison'}; $ison =~ s/0/off/; $ison =~ s/1/on/; $overpower = $jhash->{'relays'}[$i]{'overpower'}; readingsBulkUpdateIfChanged($hash,"relay".$subs,$ison); readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower); if($model eq "shelly1"){ readingsBulkUpdateIfChanged($hash,"state",$ison) }else{ readingsBulkUpdateIfChanged($hash,"state","OK"); } } for( my $i=0;$i<$meters;$i++){ $subs = ($meters == 1) ? "" : "_".$i; $power = $jhash->{'meters'}[$i]{'power'}; readingsBulkUpdateIfChanged($hash,"power".$subs,$power); } #-- we have a Shelly 2 roller type device }elsif( ($model =~ /shelly2.*/) && ($mode eq "roller") ){ for( my $i=0;$i<$rollers;$i++){ $subs = ($rollers == 1) ? "" : "_".$i; #-- weird data: stop, close or open $rstate = $jhash->{'rollers'}[$i]{'state'}; $rstate =~ s/stop/stopped/; $rstate =~ s/close/moving_down/; $rstate =~ s/open/moving_up/; $hash->{MOVING} = $rstate; $hash->{DURATION} = 0; #-- weird data: close or open $rlastdir = $jhash->{'rollers'}[$i]{'last_direction'}; $rlastdir =~ s/close/down/; $rlastdir =~ s/open/up/; $rpower = $jhash->{'rollers'}[$i]{'power'}; $rstopreason = $jhash->{'rollers'}[$i]{'stop_reason'}; #-- open 100% or 0% ? $pctnormal = (AttrVal($name,"pct100","open") eq "open"); #-- possibly no data $rcurrpos = $jhash->{'rollers'}[$i]{'current_pos'}; #-- we have data from the device, take that one if( defined($rcurrpos) && ($rcurrpos =~ /\d\d?\d?/) ){ $pct = $pctnormal ? $rcurrpos : 100-$rcurrpos; $position = ($rcurrpos==100) ? "open" : ($rcurrpos==0 ? "closed" : $pct); #-- we have no data from the device }else{ Log3 $name,1,"[Shelly_status] device $name with model=$model returns no blind position, consider chosing a different model=shelly2" if( $model ne "shelly2" ); $pct = ReadingsVal($name,"pct",undef); #-- we have a reading if( defined($pct) && $pct =~ /\d\d?\d?/ ){ $rcurrpos = $pctnormal ? $pct : 100-$pct; $position = ($rcurrpos==100) ? "open" : ($rcurrpos==0 ? "closed" : $pct); #-- we have no reading }else{ if( $rstate eq "stopped" && $rstopreason eq "normal"){ if($rlastdir eq "up" ){ $rcurrpos = 100; $pct = $pctnormal?100:0; $position = "open" }else{ $rcurrpos = 0; $pct = $pctnormal?0:100; $position = "closed"; } } } } readingsBulkUpdateIfChanged($hash,"state".$subs,$rstate); readingsBulkUpdateIfChanged($hash,"pct".$subs,$pct); readingsBulkUpdateIfChanged($hash,"position".$subs,$position); readingsBulkUpdateIfChanged($hash,"power".$subs,$rpower); readingsBulkUpdateIfChanged($hash,"stop_reason".$subs,$rstopreason); readingsBulkUpdateIfChanged($hash,"last_dir".$subs,$rlastdir); } } #-- common to all Shelly models my $hasupdate = $jhash->{'update'}{'has_update'}; my $firmware = $jhash->{'update'}{'old_version'}; $firmware =~ /.*\/(.*)\@.*/; $firmware = $1; if( $hasupdate ){ my $newfw = $jhash->{'update'}{'new_version'}; $newfw =~ /.*\/(.*)\@.*/; $newfw = $1; $firmware .= "(update needed to $newfw)"; } readingsBulkUpdateIfChanged($hash,"firmware",$firmware); my $hascloud = $jhash->{'cloud'}{'enabled'}; if( $hascloud ){ my $hasconn = ($jhash->{'cloud'}{'connected'}) ? "connected" : "not connected"; readingsBulkUpdateIfChanged($hash,"cloud","enabled($hasconn)"); }else{ readingsBulkUpdateIfChanged($hash,"cloud","disabled"); } readingsEndUpdate($hash,1); #-- cyclic update RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1) if( $hash->{INTERVAL} ne "0" ); return undef; } ######################################################################################## # # Shelly_updown - Move roller blind # acts as callable program Shelly_updown($hash,$cmd) # and as callback program Shelly_updown($hash,$cmd,$err,$data) # # Parameter hash, channel = 0,1 cmd = command # ######################################################################################## sub Shelly_updown { my ($hash, $cmd, $err, $data) = @_; my $name = $hash->{NAME}; my $url; my $state = $hash->{READINGS}{state}{VAL}; my $net = $hash->{READINGS}{network}{VAL}; return if( $net ne "connected" ); my $model = AttrVal($name,"model",""); my $creds = Shelly_pwd($hash); #-- empty cmd parameter $cmd = "" if( !defined($cmd) ); if ( $hash && !$err && !$data ){ $url = "http://$creds".$hash->{TCPIP}."/roller/0".$cmd; Log3 $name, 1,"[Shelly_updown] Issue a non-blocking call to $url"; HttpUtils_NonblockingGet({ url => $url, callback=>sub($$$){ Shelly_updown($hash,$cmd,$_[1],$_[2]) } }); return undef; }elsif ( $hash && $err ){ #Log3 $name, 1,"[Shelly_updown] has error $err"; readingsSingleUpdate($hash,"state","Error",1); return; } Log3 $name, 5,"[Shelly_updown] has obtained data $data"; my $json = JSON->new->utf8; my $jhash = eval{ $json->decode( $data ) }; if( !$jhash ){ if( ($model =~ /shelly2.*/) && ($data =~ /Device mode is not roller!/) ){ Log3 $name,1,"[Shelly_updown] Device $name is not in roller mode"; readingsSingleUpdate($hash,"state","Error",1); return }else{ Log3 $name,1,"[Shelly_updown] has invalid JSON data"; readingsSingleUpdate($hash,"state","Error",1); return; } } #-- immediately after starting movement if( $cmd ne ""){ #-- open 100% or 0% ? my $pctnormal = (AttrVal($name,"pct100","open") eq "open"); my $targetpct = $hash->{TARGETPCT}; my $targetposition = $targetpct; if( $targetpct == 100 ){ $targetposition = $pctnormal ? "open" : "closed"; }elsif( $targetpct == 0 ){ $targetposition = $pctnormal ? "closed" : "open"; } readingsBeginUpdate($hash); readingsBulkUpdate($hash,"state",$hash->{MOVING}); readingsBulkUpdate($hash,"pct",$targetpct); readingsBulkUpdate($hash,"position",$targetposition); readingsEndUpdate($hash,1); #-- after 1 second call power measurement InternalTimer(gettimeofday()+1, "Shelly_updown2", $hash,1); } return undef; } sub Shelly_updown2($){ my ($hash) =@_; Shelly_meter($hash,0); InternalTimer(gettimeofday()+$hash->{DURATION}, "Shelly_status", $hash,1); } ######################################################################################## # # Shelly_onoff - Switch Shelly relay # acts as callable program Shelly_onoff($hash,$channel,$cmd) # and as callback program Shelly_onoff($hash,$channel,$cmd,$err,$data) # # Parameter hash, channel = 0,1 cmd = command # ######################################################################################## sub Shelly_onoff { my ($hash, $channel, $cmd, $err, $data) = @_; my $name = $hash->{NAME}; my $url; my $state = $hash->{READINGS}{state}{VAL}; my $net = $hash->{READINGS}{network}{VAL}; return if( $net ne "connected" ); my $model = AttrVal($name,"model",""); my $creds = Shelly_pwd($hash); if ( $hash && !$err && !$data ){ $url = "http://$creds".$hash->{TCPIP}."/relay/".$channel.$cmd; Log3 $name, 5,"[Shelly_onoff] Issue a non-blocking call to $url"; HttpUtils_NonblockingGet({ url => $url, callback=>sub($$$){ Shelly_onoff($hash,$channel,$cmd,$_[1],$_[2]) } }); return undef; }elsif ( $hash && $err ){ #Log3 $name, 1,"[Shelly_onoff] has error $err"; readingsSingleUpdate($hash,"state","Error",1); return; } Log3 $name, 5,"[Shelly_onoff] has obtained data $data"; my $json = JSON->new->utf8; my $jhash = eval{ $json->decode( $data ) }; if( !$jhash ){ if( ($model =~ /shelly2.*/) && ($data =~ /Device mode is not relay!/) ){ Log3 $name,1,"[Shelly_onoff] Device $name is not in relay mode"; readingsSingleUpdate($hash,"state","Error",1); return }else{ Log3 $name,1,"[Shelly_onoff] has invalid JSON data"; readingsSingleUpdate($hash,"state","Error",1); return; } } my $ison = $jhash->{'ison'}; my $hastimer = $jhash->{'has_timer'}; my $overpower = $jhash->{'overpower'}; $ison =~ s/0/off/; $ison =~ s/1/on/; $cmd =~ s/\?turn=//; #-- timer command if( index($cmd,"&") ne "-1"){ $cmd = substr($cmd,0,index($cmd,"&")); if( $hastimer ne "1" ){ Log3 $name,1,"[Shelly_onoff] returns with problem, timer not set"; } } if( $ison ne $cmd ) { Log3 $name,1,"[Shelly_onoff] returns without success, cmd=$cmd but ison=$ison"; } if( defined($overpower) && $overpower eq "1") { Log3 $name,1,"[Shelly_onoff] switched off automatically because of overpower signal"; } #-- my $subs = ($shelly_models{$model}[0] ==1) ? "" : "_".$channel; readingsBeginUpdate($hash); if($model eq "shelly1"){ readingsBulkUpdateIfChanged($hash,"state",$ison) }else{ readingsBulkUpdate($hash,"state","OK"); } readingsBulkUpdate($hash,"relay".$subs,$ison); readingsBulkUpdate($hash,"overpower".$subs,$overpower) if( $shelly_models{$model}[2] > 0); readingsEndUpdate($hash,1); #-- Call status after switch. InternalTimer(int(gettimeofday()+1.5), "Shelly_status", $hash,0); return undef; } ######################################################################################## # # Shelly_meter - Retrieve data from meter # acts as callable program Shelly_meter($hash,$channel,cmd) # and as callback program Shelly_meter0($hash,$channel,$cmd,$err,$data) # # Parameter hash, channel, cmd = command # ######################################################################################## sub Shelly_meter { my ($hash, $channel, $err, $data) = @_; my $name = $hash->{NAME}; my $url; my $state = $hash->{READINGS}{state}{VAL}; my $net = $hash->{READINGS}{network}{VAL}; return if( $net ne "connected" ); my $model = AttrVal($name,"model",""); my $creds = Shelly_pwd($hash); if ( $hash && !$err && !$data ){ $url = "http://$creds".$hash->{TCPIP}."/meter/".$channel; Log3 $name, 1,"[Shelly_meter] Issue a non-blocking call to $url"; HttpUtils_NonblockingGet({ url => $url, callback=>sub($$$){ Shelly_meter($hash,$channel,$_[1],$_[2]) } }); return undef; }elsif ( $hash && $err ){ Log3 $name, 1,"[Shelly_meter has error $err"; readingsSingleUpdate($hash,"state","Error",1); return; } Log3 $name, 1,"[Shelly_meter] has obtained data $data"; my $json = JSON->new->utf8; my $jhash = eval{ $json->decode( $data ) }; if( !$jhash ){ Log3 $name,1,"[Shelly_meter] invalid JSON data"; readingsSingleUpdate($hash,"state","Error",1); return; } my $power = $jhash->{'power'}; #-- my $subs = ($shelly_models{$model}[2] ==1) ? "" : "_".$channel; readingsSingleUpdate($hash,"power".$subs,$power,1); return undef; } 1; =pod =item device =item summary to communicate with a Shelly switch/roller actuator =begin html

Shelly

=end html =begin html_DE

Shelly

=end html_DE =cut