2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-07 23:09:26 +00:00
fhem-mirror/fhem/FHEM/36_Shelly.pm
phenning 6baefcf1b9 36_Shelly.pm: Version 1.81 Bugfix
git-svn-id: https://svn.fhem.de/fhem/trunk@18479 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2019-02-01 15:40:06 +00:00

1098 lines
37 KiB
Perl

########################################################################################
#
# 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 &lt;registername&gt; [&lt;channel&gt;] &lt;value&gt;";
#-- 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 <integer>";
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
<a name="Shelly"></a>
<h3>Shelly</h3>
<ul>
<p> FHEM module to communicate with a Shelly switch/roller actuator</p>
<a name="Shellydefine"></a>
<h4>Define</h4>
<p>
<code>define &lt;name&gt; Shelly &lt;IP address&gt;</code>
<br />Defines the Shelly device. </p>
Notes: <ul>
<li>The attribute <code>model</code> <b>must</b> be set</li>
<li>This module needs the JSON package</li>
</ul>
<a name="Shellyset"></a>
<h4>Set</h4>
For Shelly all Shelly devices
<ul>
<li><a name="shelly_sconfig"></a>
<code>set &lt;name&gt; config &lt;registername&gt; [&lt;channel&gt;] &lt;value&gt;</code>
<br />set the value of a configuration register</li>
<li><a name="shelly_password">password &lt;password&gt;</a><br>This is the only way to set the password for the Shelly web interface</li>
</ul>
For Shelly switching devices (mode=relay for model=shelly2, standard for all other models)
<ul>
<li><a name="shelly_onoff"></a>
<code>set &lt;name&gt; on|off [&lt;channel&gt;] </code>
<br />switches channel &lt;channel&gt; on or off. Channel numbers are 0 and 1 for model=shelly2, 0..3 model=shelly4. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.</li>
<li><a name="shelly_onofftimer"></a>
<code>set &lt;name&gt; on-for-timer|off-for-timer &lt;time&gt; [&lt;channel&gt;] </code>
<br />switches &lt;channel&gt; on or off for &lt;time&gt; seconds. Channel numbers are 0 and 1 for model=shelly2, 0..3 model=shelly4. If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.</li>
</ul>
<br/>For Shelly roller blind devices (mode=roller for model=shelly2)
<ul>
<li><a name="shelly_updown"></a>
<code>set &lt;name&gt; open|closed|stop </code>
<br />drives the roller blind open, closed or to a stop.</li>
<li><a name="shelly_pct"></a>
<code>set &lt;name&gt; pct &lt;integer percent value&gt; </code>
<br />drives the roller blind to a partially closed position (100=open, 0=closed)</li>
<li><a name="shelly_zero"></a>
<code>set &lt;name&gt; zero </code>
<br />calibration of roller device (only for model=shelly2)</li>
</ul>
<a name="Shellyget"></a>
<h4>Get</h4>
<ul>
<li><a name="shelly_config"></a>
<code>get &lt;name&gt; config &lt;registername&gt; [$lt;channel&gt;]</code>
<br />get the value of a configuration register and writes it in reading config</li>
<li><a name="shelly_registers"></a>
<code>get &lt;name&gt; registers</code>
<br />displays the names of the configuration registers for this device</li>
<li><a name="shelly_status"></a>
<code>get &lt;name&gt; status</code>
<br />returns the current devices status.</li>
<li><a name="shelly_version"></a>
<code>get &lt;name&gt; version</code>
<br />display the version of the module</li>
</ul>
<a name="Shellyattr"></a>
<h4>Attributes</h4>
<ul>
<li><a name="shelly_shellyuser"><code>attr &lt;name&gt; shellyuser &lt;shellyuser&gt;</code><br>username for addressing the Shelly web interface</li>
<li><a name="shelly_model"><code>attr &lt;name&gt; model shelly1|shelly2|shelly4|shellyplug </code></a>
<br />type of the Shelly device</li>
<li><a name="shelly_mode"><code>attr &lt;name&gt; mode relay|roller (only for model=shelly2)</code></a>
<br />type of the Shelly device</li>
<li><a name="shelly_interval">
<code>&lt;interval&gt;</code>
<br />Update interval for reading in seconds. The default is 60 seconds, a value of 0 disables the automatic update. </li>
</ul>
<br/>For Shelly switching devices (mode=relay for model=shelly2, standard for all other models)
<ul>
<li><a name="shelly_defchannel"><code>attr &lt;name&gt; defchannel <integer> </code></a>
<br />only for model=shelly2|shelly4 or multi-channel switches: Which channel will be switched, if a command is received without channel number</li>
</ul>
<br/>For Shelly roller blind devices (mode=roller for model=shelly2)
<ul>
<li><a name="shelly_maxtime"><code>attr &lt;name&gt; maxtime &lt;float&gt; </code></a>
<br />time needed for a complete drive upward or downward</li>
<li><a name="shelly_pct100"><code>attr &lt;name&gt; pct100 open|closed (default:open) </code></a>
<br />is pct=100 open or closed ? </li>
</ul>
<br/>Standard attributes
<ul>
<li><a href="#alias">alias</a>, <a href="#comment">comment</a>, <a
href="#event-on-update-reading">event-on-update-reading</a>, <a
href="#event-on-change-reading">event-on-change-reading</a>, <a href="#room"
>room</a>, <a href="#eventMap">eventMap</a>, <a href="#loglevel">loglevel</a>,
<a href="#webCmd">webCmd</a></li>
</ul>
</ul>
=end html
=begin html_DE
<h3>Shelly</h3>
<ul>
Absichtlich keine deutsche Dokumentation vorhanden, die englische Version gibt es hier: <a href="commandref.html#Shelly">Shelly</a>
</ul>
=end html_DE
=cut