";
Log3 $name, 1,"[Shelly_Set] ".$msg;
return $msg;
}
$cmd = $cmd."&timer=$time";
}
if( $cmd eq "toggle"){
$cmd = (ReadingsVal($name,"state","off") eq "on") ? "off" : "on";
}
Shelly_dim($hash,"color/0","?turn=".$cmd);
#Shelly_onoff($hash,"color/0","?turn=".$cmd);
}
if( $cmd eq "hsv" ){
my($hue,$saturation,$value)=split(',',$value);
#-- rescale
if( $hue>1 ){
$hue = $hue/360;
}
my ($red,$green,$blue)=Color::hsv2rgb($hue,$saturation,$value);
$cmd=sprintf("red=%d&green=%d&blue=%d",int($red*255+0.5),int($green*255+0.5),int($blue*255+0.5));
$cmd .= "&gain=100" if ($model eq "shellybulb");
Shelly_dim($hash,"color/0","?".$cmd);
}elsif( $cmd eq "rgb" ){
my $red =hex(substr($value,0,2));
my $green=hex(substr($value,2,2));
my $blue =hex(substr($value,4,2));
$cmd =sprintf("red=%d&green=%d&blue=%d",$red,$green,$blue);
$cmd .= "&gain=100" if ($model eq "shellybulb");
Shelly_dim($hash,"color/0","?".$cmd);
}elsif( $cmd eq "rgbw" ){
my $red =hex(substr($value,0,2));
my $green=hex(substr($value,2,2));
my $blue =hex(substr($value,4,2));
my $white=hex(substr($value,6,2));
$cmd =sprintf("red=%d&green=%d&blue=%d&white=%d",$red,$green,$blue,$white);
$cmd .= "&gain=100" if ($model eq "shellybulb");
Shelly_dim($hash,"color/0","?".$cmd);
}elsif( $cmd eq "white" ){
$cmd=sprintf("white=%d",$value);
Shelly_dim($hash,"color/0","?".$cmd);
}
}
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 or read general configuration
# 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 $state = $hash->{READINGS}{state}{VAL};
my $net = $hash->{READINGS}{network}{VAL};
return
if( $net !~ /connected/ );
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/".$cmd;
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_configure] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
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] device $name has returned data $data";
my $json = JSON->new->utf8;
my $jhash = eval{ $json->decode( $data ) };
if( !$jhash ){
Log3 $name,1,"[Shelly_configure] invalid JSON data for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
#-- if settings command, we obtain only basic settings
if( $cmd eq "settings/" ){
$hash->{SHELLYID} = $jhash->{'device'}{'hostname'};
return
}
#-- isolate register name
my $reg = substr($cmd,index($cmd,"?")+1);
my $chan= substr($cmd,index($cmd,"?")-1,1);
$reg = substr($reg,0,index($reg,"="))
if(index($reg,"=") > 0);
my $val = $jhash->{$reg};
$val = ""
if(!defined($val));
$chan = " [channel $chan]";
readingsSingleUpdate($hash,"config",$reg."=".$val.$chan,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) (only for 1G)
#
# Parameter hash
#
########################################################################################
sub Shelly_status {
my ($hash, $err, $data) = @_;
my $name = $hash->{NAME};
my $state = $hash->{READINGS}{state}{VAL};
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
#-- check if 2nd generation device
my $is2G = ($shelly_models{$model}[4] == 1);
#-- for 1G devices status is received in one single call
if( !$is2G ){
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/status";
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_status] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
callback => sub($$$){ Shelly_status($hash,$_[1],$_[2]) }
});
return undef;
}elsif ( $hash && $err ){
Log3 $name, 1,"[Shelly_status] device $name 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;
}else{
Shelly_proc1G($hash,$err,$data);
return undef;
}
#-- 2G devices need to submit the call several times
}else{
#-- get status of relays -> component is Switch
my $url = "http://$creds".$hash->{TCPIP}."/rpc/Switch.GetStatus?id=";
my $timeout = AttrVal($name,"timeout",4);
for( my $id=0; $id< $shelly_models{$model}[0]; $id++){
Log3 $name, 5,"[Shelly_status] issue a non-blocking call to ".$url.$id;
HttpUtils_NonblockingGet({
url => $url.$id,
timeout => $timeout,
callback => sub($$$$){ Shelly_proc2G($hash,$_[1],$_[2],"relay") }
});
}
return undef;
}
}
########################################################################################
#
# Shelly_proc1G - process data from device 1st generation
# In 1G devices status are all in one call
#
########################################################################################
sub Shelly_proc1G {
my ($hash, $err, $data) = @_;
my $name = $hash->{NAME};
my $state = $hash->{READINGS}{state}{VAL};
my $model = AttrVal($name,"model","generic");
Log3 $name, 5,"[Shelly_proc1G] device $name has returned data $data";
my $json = JSON->new->utf8;
my $jhash = eval{ $json->decode( $data ) };
if( !$jhash ){
Log3 $name,1,"[Shelly_proc1G] invalid JSON data for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
my $mode = AttrVal($name,"mode","");
my $channels = $shelly_models{$model}[0];
my $rollers = $shelly_models{$model}[1];
my $dimmers = $shelly_models{$model}[2];
my $meters = $shelly_models{$model}[3];
my ($subs,$ison,$overpower,$rpower,$rstate,$power,$energy,$rstopreason,$rcurrpos,$position,$rlastdir,$pct,$pctnormal);
readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged($hash,"network","connected to {TCPIP}."\">".$hash->{TCPIP}."",1);
#-- for all models set internal temperature reading
if ($jhash->{'temperature'}) {
readingsBulkUpdateIfChanged($hash,"inttemp",$jhash->{'temperature'});
}
#############################################################################################################################
#-- 1st generation: we have a shelly1, shelly1pm, shellypro4, shelly2, shelly25, shellyplug or shellyem switch type device
if( ($model =~ /shelly1.*/) || ($model eq "shellyuni") || ($model eq "shellyplug") || ($model =~ /shelly(pro)?4/) || ($model =~ /shelly.?em/) || (($model =~ /shelly2.*/) && ($mode eq "relay")) ){
for( my $i=0;$i<$channels;$i++){
$subs = (($channels == 1) ? "" : "_".$i);
$ison = $jhash->{'relays'}[$i]{'ison'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
$overpower = $jhash->{'relays'}[$i]{'overpower'};
readingsBulkUpdateIfChanged($hash,"relay".$subs,$ison);
readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower)
if(defined($overpower));
if($model =~ /shelly(1|(plug)).*/){
readingsBulkUpdateIfChanged($hash,"state",$ison)
}else{
readingsBulkUpdateIfChanged($hash,"state","OK");
}
}
if ($model eq "shelluni") {
my $voltage = $jhash->{'adcs'}[0]{'voltage'};
readingsBulkUpdateIfChanged($hash,"voltage",$voltage);
}
my $metern = ($model =~ /shelly.?em/)?"emeters":"meters";
for( my $i=0;$i<$meters;$i++){
$subs = ($meters == 1) ? "" : "_".$i;
$power = $jhash->{$metern}[$i]{'power'};
$energy = int($jhash->{$metern}[$i]{'total'}/6)/10;
readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
if ($model =~ /shelly.?em/) {
my $voltage = $jhash->{$metern}[$i]{'voltage'};
readingsBulkUpdateIfChanged($hash,'voltage'.$subs,$voltage);
my $reactivePower = $jhash->{$metern}[$i]{'reactive'};
#my $apparentPower = sqrt( ($power * $power) + ($reactivePower * $reactivePower) );
#my $powerFactor = ($apparentPower != 0)?(int($power / $apparentPower * 100) / 100):"0";
#readingsBulkUpdateIfChanged($hash,'powerFactor'.$subs,$powerFactor);
$energy = $jhash->{$metern}[$i]{'total'};
}else{
$energy = int($jhash->{$metern}[$i]{'total'}/6)/10;
}
readingsBulkUpdateIfChanged($hash,"energy".$subs,$energy);
}
#############################################################################################################################
#-- we have a shelly2 or shelly25 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_proc1G] device $name with model=$model returns no blind position, consider chosing a different model=shelly2/2.5"
if( $model !~ /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);
}
#############################################################################################################################
#-- we have a shellydimmer or shellyrgbw white device
}elsif( ($model eq "shellydimmer") || ($model eq "shellyrgbw" && $mode eq "white") ){
for( my $i=0;$i<$dimmers;$i++){
$subs = (($dimmers == 1) ? "" : "_".$i);
$ison = $jhash->{'lights'}[$i]{'ison'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
my $bri = $jhash->{'lights'}[$i]{'brightness'};
$overpower = $jhash->{'lights'}[$i]{'overpower'};
$power = $jhash->{'meters'}[$i]{'power'};
$energy = (defined($jhash->{'meters'}[$i]{'total'}))?int($jhash->{'meters'}[$i]{'total'}/6)/10:"undefined";
readingsBulkUpdateIfChanged($hash,"state".$subs,$ison);
readingsBulkUpdateIfChanged($hash,"pct".$subs,$bri);
readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower);
readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
readingsBulkUpdateIfChanged($hash,"energy".$subs,$energy);
}
readingsBulkUpdateIfChanged($hash,"state","OK")
if ($dimmers > 1);
#############################################################################################################################
#-- we have a shellybulb or shellyduo in white mode
}elsif( $model eq "shellybulb" && $mode eq "white" ){
for( my $i=0;$i<$dimmers;$i++){
$subs = (($dimmers == 1) ? "" : "_".$i);
$ison = $jhash->{'lights'}[$i]{'ison'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
my $bri = $jhash->{'lights'}[$i]{'brightness'};
my $ct = $jhash->{'lights'}[$i]{'temp'};
$overpower = $jhash->{'lights'}[$i]{'overpower'};
$power = $jhash->{'meters'}[$i]{'power'};
$energy = (defined($jhash->{'meters'}[$i]{'total'}))?int($jhash->{'meters'}[$i]{'total'}/6)/10:"undefined";
readingsBulkUpdateIfChanged($hash,"state".$subs,$ison);
readingsBulkUpdateIfChanged($hash,"pct".$subs,$bri);
readingsBulkUpdateIfChanged($hash,"ct".$subs,$ct);
readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower)
if($overpower);
readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
readingsBulkUpdateIfChanged($hash,"energy".$subs,$energy);
}
readingsBulkUpdateIfChanged($hash,"state","OK")
if ($dimmers > 1);
#############################################################################################################################
#-- we have a shellyrgbw color device
}elsif( $model =~ /shelly(rgbw|bulb)/ && $mode eq "color" ){
$ison = $jhash->{'lights'}[0]{'ison'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
$overpower = $jhash->{'lights'}[0]{'overpower'};
my $red = $jhash->{'lights'}[0]{'red'};
my $green = $jhash->{'lights'}[0]{'green'};
my $blue = $jhash->{'lights'}[0]{'blue'};
my $white = $jhash->{'lights'}[0]{'white'};
my $gain = $jhash->{'lights'}[0]{'gain'};
if ( defined $gain && $gain != 100 ) {
$red = int ($red*$gain/100.0);
$blue = int ($blue*$gain/100.0);
$green = int ($green*$gain/100.0);
$white = int ($white*$gain/100.0);
}
my $rgb = sprintf("%02X%02X%02X", $red,$green,$blue);
readingsBulkUpdateIfChanged($hash,"rgb",$rgb);
readingsBulkUpdateIfChanged($hash,"L-red",$red);
readingsBulkUpdateIfChanged($hash,"L-green",$green);
readingsBulkUpdateIfChanged($hash,"L-blue",$blue);
readingsBulkUpdateIfChanged($hash,"L-white",$white);
#-- taken out in newer firmwares
readingsBulkUpdateIfChanged($hash,"overpower",$overpower)
if( $overpower);
readingsBulkUpdateIfChanged($hash,"state",$ison);
for( my $i=0;$i<$meters;$i++){
$subs = ($meters == 1) ? "" : "_".$i;
$power = $jhash->{'meters'}[$i]{'power'};
$energy = (defined($jhash->{'meters'}[$i]{'total'}))?int($jhash->{'meters'}[$i]{'total'}/6)/10:"undefined";
readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
readingsBulkUpdateIfChanged($hash,"energy".$subs,$energy);
}
}
#############################################################################################################################
#-- common to all models
my $hasupdate = $jhash->{'update'}{'has_update'};
my $firmware = $jhash->{'update'}{'old_version'};
$firmware =~ /.*\/(v[0-9.]+(-rc\d|)).*/;
$firmware = $1;
if( $hasupdate ){
my $newfw = $jhash->{'update'}{'new_version'};
$newfw =~ /.*\/(v[0-9.]+(-rc\d|)).*/;
$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");
}
#-- look for external sensors
if ($jhash->{'ext_temperature'}) {
my %sensors = %{$jhash->{'ext_temperature'}};
foreach my $temp (keys %sensors){
readingsBulkUpdate($hash,"temperature_".$temp,$sensors{$temp}->{'tC'});
}
}
if ($jhash->{'ext_humidity'}) {
my %sensors = %{$jhash->{'ext_humidity'}};
foreach my $hum (keys %sensors){
readingsBulkUpdate($hash,"humidity_".$hum,$sensors{$hum}->{'hum'});
}
}
readingsEndUpdate($hash,1);
#-- cyclic update
RemoveInternalTimer($hash);
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1)
if( $hash->{INTERVAL} ne "0" );
return undef;
}
########################################################################################
#
# Shelly_proc2G - process data from device 2nd generation
# Necessary because in 2G devices status are per channel
#
########################################################################################
sub Shelly_proc2G {
my ($hash, $err, $data, $comp) = @_;
my $name = $hash->{NAME};
my $state = $hash->{READINGS}{state}{VAL};
my $model = AttrVal($name,"model","generic");
#Log 1,"=============> Status 2G $name model $model component $comp err $err data $data";
#-- error in non blocking call
if ( $hash && $err ){
Log3 $name, 1,"[Shelly_proc2G] device $name 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_proc2G] device $name has returned data $data";
my $json = JSON->new->utf8;
my $jhash = eval{ $json->decode( $data ) };
#-- error in data
if( !$jhash ){
Log3 $name,1,"[Shelly_proc2G] invalid JSON data for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
readingsSingleUpdate($hash,"state","OK",1);
my $mode = AttrVal($name,"mode","");
my $channel = $jhash->{'id'};
my $channels = $shelly_models{$model}[0];
my $rollers = $shelly_models{$model}[1];
my $dimmers = $shelly_models{$model}[2];
my $meters = $shelly_models{$model}[3];
my ($subs,$ison,$overpower,$voltage,$current,$power,$energy,$pfactor);
readingsBeginUpdate($hash);
if( $channel == 0){
readingsBulkUpdateIfChanged($hash,"network","connected to {TCPIP}."\">".$hash->{TCPIP}."",1);
#-- for all models set internal temperature reading - but only for first call
if ($jhash->{'temperature'}){
readingsBulkUpdateIfChanged($hash,"inttemp",$jhash->{'temperature'}{'tC'});
}
}
#############################################################################################################################
#-- 2nd generation: we have a shellyplus1(pm) or shellypro4pm switch type device
if( ($model =~ /shellyplus1(pm)?/) || ($model eq "shellypro4pm") ){
$subs = (($channels == 1) ? "" : "_".$channel);
$ison = $jhash->{'output'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
readingsBulkUpdateIfChanged($hash,"relay".$subs,$ison);
#$overpower = $jhash->{'relays'}[$i]{'overpower'};
#readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower)
# if(defined($overpower));
if( $meters > 0 ){
$voltage = $jhash->{'voltage'};
$current = $jhash->{'current'};
$power = $jhash->{'apower'};
$pfactor = $jhash->{'pf'};
$energy = $jhash->{'energy'}{'total'};
readingsBulkUpdateIfChanged($hash,"voltage".$subs,$voltage);
readingsBulkUpdateIfChanged($hash,"current".$subs,$current);
readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
readingsBulkUpdateIfChanged($hash,"pfactor".$subs,$pfactor);
readingsBulkUpdateIfChanged($hash,"energy".$subs,$energy);
}
}
#############################################################################################################################
#-- common to all models
readingsEndUpdate($hash,1);
#-- cyclic update
RemoveInternalTimer($hash);
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1)
if( $hash->{INTERVAL} ne "0" );
return undef;
}
########################################################################################
#
# Shelly_dim - Set Shelly dimmer state
# acts as callable program Shelly_dim($hash,$channel,$cmd)
# and as callback program Shelly_dim($hash,$channel,$cmd,$err,$data)
#
# Parameter hash, channel = 0,1 cmd = command
#
########################################################################################
sub Shelly_dim {
my ($hash, $channel, $cmd, $err, $data) = @_;
my $name = $hash->{NAME};
my $state = $hash->{READINGS}{state}{VAL};
my $net = $hash->{READINGS}{network}{VAL};
return
if( $net !~ /connected/ );
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/$channel$cmd";
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_dim] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
callback => sub($$$){ Shelly_dim($hash,$channel,$cmd,$_[1],$_[2]) }
});
return undef;
}elsif ( $hash && $err ){
#Log3 $name, 1,"[Shelly_dim] has error $err";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
Log3 $name, 5,"[Shelly_dim] device $name has returned data $data";
my $json = JSON->new->utf8;
my $jhash = eval{ $json->decode( $data ) };
if( !$jhash ){
if( ($model =~ /shellyrgbw.*/) && ($data =~ /Device mode is not dimmer!/) ){
Log3 $name,1,"[Shelly_dim] device $name is not a dimmer";
readingsSingleUpdate($hash,"state","Error",1);
return
}else{
Log3 $name,1,"[Shelly_dim] invalid JSON data for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
}
my $ison = $jhash->{'ison'};
my $bright = $jhash->{'brightness'};
my $hastimer = $jhash->{'has_timer'};
my $overpower = $jhash->{'overpower'};
if( $cmd =~ /\?turn=((on)|(off))/ ){
my $cmd2 = $1;
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/on/;
#-- timer command
if( index($cmd,"&") ne "-1"){
$cmd = substr($cmd,0,index($cmd,"&"));
if( $hastimer && $hastimer ne "1" ){
Log3 $name,1,"[Shelly_dim] returns with problem for device $name, timer not set";
}
}
if( $ison ne $cmd2 ) {
Log3 $name,1,"[Shelly_dim] returns without success for device $name, cmd=$cmd but ison=$ison";
}
}elsif( $cmd =~ /\?brightness=(.*)/){
my $cmd2 = $1;
if( $bright ne $cmd2 ) {
Log3 $name,1,"[Shelly_dim] returns without success for device $name, desired brightness $cmd, but device brightness=$bright";
}
}
if( defined($overpower) && $overpower eq "1") {
Log3 $name,1,"[Shelly_dim] device $name switched off automatically because of overpower signal";
}
readingsBeginUpdate($hash);
readingsBulkUpdate($hash,"overpower",$overpower)
if( $shelly_models{$model}[3] > 0);
readingsEndUpdate($hash,1);
#-- Call status after switch.
InternalTimer(int(gettimeofday()+1.5), "Shelly_status", $hash,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 $state = $hash->{READINGS}{state}{VAL};
my $net = $hash->{READINGS}{network}{VAL};
return
if( $net !~ /connected/ );
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
#-- empty cmd parameter
$cmd = ""
if( !defined($cmd) );
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/roller/0".$cmd;
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_updown] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
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 for device $name";
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 $state = $hash->{READINGS}{state}{VAL};
my $net = $hash->{READINGS}{network}{VAL};
return
if( $net !~ /connected/ );
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/relay/".$channel.$cmd;
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_onoff] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
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] device $name has returned 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 for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
}
my $ison = $jhash->{'ison'};
my $hastimer = $jhash->{'has_timer'};
my $overpower = $jhash->{'overpower'};
$ison =~ s/0|(false)/off/;
$ison =~ s/1|(true)/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 for device $name, timer not set";
}
}
if( $ison ne $cmd ) {
Log3 $name,1,"[Shelly_onoff] returns without success for device $name, cmd=$cmd but ison=$ison";
}
if( defined($overpower) && $overpower eq "1") {
Log3 $name,1,"[Shelly_onoff] device $name switched off automatically because of overpower signal";
}
#--
my $subs = ($shelly_models{$model}[0] ==1) ? "" : "_".$channel;
readingsBeginUpdate($hash);
if($model =~ /shelly(1|(plug)).*/){
readingsBulkUpdateIfChanged($hash,"state",$ison)
}else{
readingsBulkUpdate($hash,"state","OK");
}
readingsBulkUpdate($hash,"relay".$subs,$ison);
readingsBulkUpdate($hash,"overpower".$subs,$overpower)
if( $shelly_models{$model}[3] > 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 $state = $hash->{READINGS}{state}{VAL};
my $net = $hash->{READINGS}{network}{VAL};
return
if( $net !~ /connected/ );
my $model = AttrVal($name,"model","generic");
my $creds = Shelly_pwd($hash);
if ( $hash && !$err && !$data ){
my $url = "http://$creds".$hash->{TCPIP}."/meter/".$channel;
my $timeout = AttrVal($name,"timeout",4);
Log3 $name, 5,"[Shelly_meter] issue a non-blocking call to $url";
HttpUtils_NonblockingGet({
url => $url,
timeout => $timeout,
callback => sub($$$){ Shelly_meter($hash,$channel,$_[1],$_[2]) }
});
return undef;
}elsif ( $hash && $err ){
Log3 $name, 1,"[Shelly_meter] device $name has error $err";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
Log3 $name, 5,"[Shelly_meter] device $name has returned data $data";
my $json = JSON->new->utf8;
my $jhash = eval{ $json->decode( $data ) };
if( !$jhash ){
Log3 $name,1,"[Shelly_meter] invalid JSON data for device $name";
readingsSingleUpdate($hash,"state","Error",1);
return;
}
my $subs = ($shelly_models{$model}[3] ==1) ? "" : "_".$channel;
my $power = $jhash->{'power'};
my $energy = int($jhash->{'total'}/6)/10;
readingsSingleUpdate($hash,"power".$subs,$power,1);
readingsSingleUpdate($hash,"energy".$subs,$energy,1);
return undef;
}
1;
=pod
=item device
=item summary to communicate with a Shelly switch/roller actuator
=begin html
Shelly
FHEM module to communicate with a Shelly switch/roller actuator/dimmer/RGBW controller
Define
define <name> Shelly <IP address>
Defines the Shelly device.
Notes:
- The attribute
model
must be set
- This module needs the JSON package
- In Shelly button, switch, roller or dimmer devices one may set URL values that are "hit" when the input or output status changes.
This is useful to transmit status changes arising from locally pressed buttons directly to FHEM by setting
- Button switched ON url: http://<FHEM IP address>:<Port>/fhem?XHR=1&cmd=get%20<Devicename>%20status
If one wants to detach the button from the output, one may generate an additional reading button by setting in the Shelly
- For Button switched ON url: http://<FHEM IP address>:<Port>/fhem?XHR=1&cmd=set%20<Devicename>%20button_on%20[<channel>]
- For Button switched OFF url: http://<FHEM IP address>:<Port>/fhem?XHR=1&cmd=set%20<Devicename>%20button_off%20[<channel>]
Attention: Of course, a csrfToken must be included as well - or a proper allowed device declared.
- If the model attribute is set to generic, the device does not contain any actors, it is just a placeholder for arbitrary sensors
Set
For all Shelly devices
set <name> config <registername> <value> [<channel>]
set the value of a configuration register
- password <password>
This is the only way to set the password for the Shelly web interface
For Shelly switching devices (model=shelly1|shelly1pm|shellyuni|shelly4|shellypro4pm|shellyplug|shellyem|shelly3em or (model=shelly2/2.5 and mode=relay))
-
set <name> on|off|toggle [<channel>]
switches channel <channel> on or off. Channel numbers are 0 and 1 for model=shelly2/2.5, 0..3 for model=shelly4. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.
-
set <name> on-for-timer|off-for-timer <time> [<channel>]
switches <channel> on or off for <time> seconds. Channel numbers are 0 and 1 for model=shelly2/2.5 or model=shellyuni, and 0..3 model=shelly4. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.
-
set <name> xtrachannels
create readingsProxy devices for switching device with more than one channel
For Shelly roller blind devices (model=shelly2/2.5 and mode=roller)
-
set <name> open|closed|stop [<duration>]
drives the roller blind open, closed or to a stop. The commands open and closed take a optional parameter that determines the drive time in seconds
-
set <name> pct <integer percent value>
drives the roller blind to a partially closed position (normally 100=open, 0=closed, see attribute pct100). If the integer percent value
carries a sign + or - the following number will be added to the current value of the position to acquire the target value.
-
set <name> zero
calibration of roller device (only for model=shelly2/2.5)
For Shelly dimmer devices model=shellydimmer or (model=shellyrgbw and mode=white)
-
set <name> on|off [<channel>]
switches channel <channel> on or off. Channel numbers are 0..3 for model=shellyrgbw. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.
-
set <name> on-for-timer|off-for-timer <time> [<channel>]
switches <channel> on or off for <time> seconds. Channel numbers 0..3 for model=shellyrgbw. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.
-
set <name> pct <0..100> [<channel>]
percent value to set brightness value. Channel numbers 0..3 for model=shellyrgbw. If the channel parameter is omitted, the module will dim the channel defined in the defchannel attribute.
For Shelly RGBW devices (model=shellyrgbw and mode=color)
-
set <name> on|off
switches device <channel> on or off
-
set <name> on-for-timer|off-for-timer <time>
switches device on or off for <time> seconds.
-
set <name> hsv <hue value 0..360>,<saturation value 0..1>,<brightness value 0..1>
comma separated list of hue, saturation and value to set the color. Note, that 360° is the same hue as 0° = red.
Hue values smaller than 1 will be treated as fraction of the full circle, e.g. 0.5 will give the same hue as 180°.
-
set <name> rgb <rrggbb>
6-digit hex string to set the color
-
set <name> rgbw <rrggbbww>
8-digit hex string to set the color and white value
-
set <name> white <integer>
number 0..255 to set the white value
Get
-
get <name> config [<registername>] [<channel>]
get the value of a configuration register and writes it in reading config. If the register name is omitted, only general data like e.g. the SHELLYID are fetched.
-
get <name> registers
displays the names of the configuration registers for this device
-
get <name> status
returns the current devices status.
-
get <name> version
display the version of the module
Attributes
attr <name> shellyuser <shellyuser>
username for addressing the Shelly web interface
- <
attr <name> model generic|shelly1|shelly1pm|shelly2|shelly2.5|shellyuni|shelly4|shellyplug|shellydimmer|shellyrgbw
type of the Shelly device. >If the model attribute is set to generic, the device does not contain any actors,
it is just a placeholder for arbitrary sensors
attr <name> mode relay|roller (only for model=shelly2/2.5) mode white|color (only for model=shellyrgbw)
type of the Shelly device
-
<interval>
Update interval for reading in seconds. The default is 60 seconds, a value of 0 disables the automatic update.
-
<timeout>
Timeout in seconds for HttpUtils_NonblockingGet. The default is 4 seconds. Careful: Use this attribute only if you get timeout errors in your log.
For Shelly switching devices (mode=relay for model=shelly2/2.5, standard for all other switching models)
attr <name> defchannel
only for model=shelly2|shelly2.5|shelly4 or multi-channel switches: Which channel will be switched, if a command is received without channel number
For Shelly roller blind devices (mode=roller for model=shelly2/2.5)
attr <name> maxtime <float>
time needed for a complete drive upward or downward
attr <name> pct100 open|closed (default:open)
is pct=100 open or closed ?
Standard attributes
=end html
=begin html_DE
Shelly
Absichtlich keine deutsche Dokumentation vorhanden, die englische Version gibt es hier: Shelly
=end html_DE
=cut