36_Shelly.pm 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. ########################################################################################
  2. #
  3. # Shelly.pm
  4. #
  5. # FHEM module to communicate with Shelly switch/roller actor devices
  6. # Prof. Dr. Peter A. Henning, 2018
  7. #
  8. # $Id: 36_Shelly.pm 17724 2018-11-10 17:17:54Z phenning $
  9. #
  10. ########################################################################################
  11. #
  12. # This programm is free software; you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation; either version 2 of the License, or
  15. # (at your option) any later version.
  16. #
  17. # The GNU General Public License can be found at
  18. # http://www.gnu.org/copyleft/gpl.html.
  19. # A copy is found in the textfile GPL.txt and important notices to the license
  20. # from the author is found in LICENSE.txt distributed with these scripts.
  21. #
  22. # This script is distributed in the hope that it will be useful,
  23. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. # GNU General Public License for more details.
  26. #
  27. ########################################################################################
  28. package main;
  29. use strict;
  30. use warnings;
  31. use JSON; # imports encode_json, decode_json, to_json and from_json.
  32. use vars qw{%attr %defs};
  33. sub Log($$);
  34. #-- globals on start
  35. my $version = "1.41";
  36. #-- these we may get on request
  37. my %gets = (
  38. "status:noArg" => "S",
  39. "registers:noArg" => "R",
  40. "config" => "C",
  41. "version:noArg" => "V"
  42. );
  43. #-- these we may set
  44. my %setssw = (
  45. "on" => "O",
  46. "off" => "F",
  47. "on-for-timer" => "T",
  48. "off-for-timer" => "E",
  49. "config" => "C",
  50. );
  51. my %setsrol = (
  52. "closed:noArg" => "C",
  53. "open:noArg" => "O",
  54. "stop:noArg" => "S",
  55. "pct:slider,0,1,100" => "P"
  56. );
  57. my %shelly_models = (
  58. #(relays,rollers,meters)
  59. "shelly1" => [1,0,0,],
  60. "shelly2" => [2,1,1],
  61. "shellyplug" => [1,0,1],
  62. "shelly4" => [4,0,4]
  63. );
  64. my %shelly_regs = (
  65. "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",
  66. "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".
  67. "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".
  68. "obstacle_power=<watt>\x{27f6}power threshold for detection\nobstacle_delay=<seconds>\x{27f6}delay after motor start to watch\n".
  69. "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".
  70. "safety_allowed_on_trigger=none|open|close|all\x{27f6}commands allowed in safety mode"
  71. );
  72. ########################################################################################
  73. #
  74. # Shelly_Initialize
  75. #
  76. # Parameter hash
  77. #
  78. ########################################################################################
  79. sub Shelly_Initialize ($) {
  80. my ($hash) = @_;
  81. $hash->{DefFn} = "Shelly_Define";
  82. $hash->{UndefFn} = "Shelly_Undef";
  83. $hash->{AttrFn} = "Shelly_Attr";
  84. $hash->{GetFn} = "Shelly_Get";
  85. $hash->{SetFn} = "Shelly_Set";
  86. #$hash->{NotifyFn} = "Shelly_Notify";
  87. #$hash->{InitFn} = "Shelly_Init";
  88. $hash->{AttrList}= "verbose model:".join(",",(keys %shelly_models))." mode:relay,roller defchannel maxtime maxpower interval pct100:open,closed ".
  89. $readingFnAttributes;
  90. }
  91. ########################################################################################
  92. #
  93. # Shelly_Define - Implements DefFn function
  94. #
  95. # Parameter hash, definition string
  96. #
  97. ########################################################################################
  98. sub Shelly_Define($$) {
  99. my ($hash, $def) = @_;
  100. my @a = split("[ \t][ \t]*", $def);
  101. return "[Shelly] Define the IP address of the Shelly device as a parameter"
  102. if(@a != 3);
  103. return "[Shelly] Invalid IP address ".$a[2]." of Shelly device"
  104. if( $a[2] !~ m|\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?(\:\d+)?| );
  105. my $dev = $a[2];
  106. #-- split into parts
  107. my @tcp = split(':',$dev);
  108. #-- when the specified ip address contains a port already, use it as supplied
  109. if ( $tcp[1] ){
  110. $hash->{TCPIP} = $dev;
  111. }else{
  112. $hash->{TCPIP} = $tcp[0].":80";
  113. };
  114. $hash->{DURATION} = 0;
  115. $hash->{MOVING} = 0;
  116. delete $hash->{BLOCKED};
  117. $hash->{INTERVAL} = 60;
  118. $modules{Shelly}{defptr}{$a[0]} = $hash;
  119. #-- InternalTimer blocks if init_done is not true
  120. my $oid = $init_done;
  121. $init_done = 1;
  122. readingsBeginUpdate($hash);
  123. my $err = Shelly_status($hash);
  124. if( !defined($err) ){
  125. readingsBulkUpdate($hash,"state","initialized");
  126. readingsBulkUpdate($hash,"network","connected");
  127. }else{
  128. readingsBulkUpdate($hash,"state",$err);
  129. readingsBulkUpdate($hash,"network","not connected");
  130. }
  131. readingsEndUpdate($hash,1);
  132. $init_done = $oid;
  133. return undef;
  134. }
  135. #######################################################################################
  136. #
  137. # Shelly_Undef - Implements UndefFn function
  138. #
  139. # Parameter hash = hash of device addressed
  140. #
  141. #######################################################################################
  142. sub Shelly_Undef ($) {
  143. my ($hash) = @_;
  144. delete($modules{Shelly}{defptr}{NAME});
  145. RemoveInternalTimer($hash);
  146. return undef;
  147. }
  148. #######################################################################################
  149. #
  150. # Shelly_Attr - Set one attribute value
  151. #
  152. ########################################################################################
  153. sub Shelly_Attr(@) {
  154. my ($cmd,$name,$attrName, $attrVal) = @_;
  155. my $hash = $main::defs{$name};
  156. my $ret;
  157. my $model = AttrVal($name,"model","shelly2");
  158. my $mode = AttrVal($name,"mode","relay");
  159. #-- temporary code
  160. delete $hash->{BLOCKED};
  161. #---------------------------------------
  162. if ( ($cmd eq "set") && ($attrName =~ /model/) ) {
  163. my $regex = "((".join(")|(",(keys %shelly_models))."))";
  164. if( $attrVal !~ /$regex/){
  165. Log3 $name,1,"[Shelly_Attr] wrong value of model attribute, see documentation for possible values";
  166. return
  167. }
  168. #-- only one channel
  169. if( $shelly_models{$model}[0] == 1){
  170. fhem("deletereading ".$name." relay_.*");
  171. fhem("deletereading ".$name." overpower_.*");
  172. }else{
  173. fhem("deletereading ".$name." relay");
  174. fhem("deletereading ".$name." overpower");
  175. }
  176. #-- no rollers
  177. if( $shelly_models{$model}[1] == 0){
  178. fhem("deletereading ".$name." position.*");
  179. fhem("deletereading ".$name." stop_reason.*");
  180. fhem("deletereading ".$name." last_dir.*");
  181. fhem("deletereading ".$name." pct.*");
  182. delete $hash->{MOVING};
  183. delete $hash->{DURATION};
  184. }
  185. #-- always clear readings for meters
  186. #if( $shelly_models{$model}[2] <= 1){
  187. fhem("deletereading ".$name." power.*");
  188. fhem("deletereading ".$name." overpower.*");
  189. #}
  190. #---------------------------------------
  191. }elsif ( ($cmd eq "set") && ($attrName =~ /mode/) ) {
  192. if( $model ne "shelly2" ){
  193. Log3 $name,1,"[Shelly_Attr] setting the mode attribute only works for model=shelly2";
  194. return
  195. }
  196. if( $attrVal !~ /((relay)|(roller))/){
  197. Log3 $name,1,"[Shelly_Attr] wrong mode value $attrVal";
  198. return;
  199. }elsif( $attrVal eq "relay"){
  200. fhem("deletereading ".$name." position.*");
  201. fhem("deletereading ".$name." stop_reason.*");
  202. fhem("deletereading ".$name." last_dir.*");
  203. fhem("deletereading ".$name." pct.*");
  204. }elsif( $attrVal eq "roller"){
  205. fhem("deletereading ".$name." relay.*");
  206. fhem("deletereading ".$name." overpower.*");
  207. }
  208. Shelly_configure($hash,"settings?mode=".$attrVal);
  209. #---------------------------------------
  210. }elsif ( ($cmd eq "set") && ($attrName eq "maxtime") ) {
  211. if( ($model ne "shelly2") || ($mode ne "roller" ) ){
  212. Log3 $name,1,"[Shelly_Attr] setting the maxtime attribute only works for model=shelly2 and mode=roller";
  213. return
  214. }
  215. Shelly_configure($hash,"settings?maxtime=".$attrVal);
  216. #---------------------------------------
  217. }elsif ( ($cmd eq "set") && ($attrName eq "pct100") ) {
  218. if( ($model ne "shelly2") || ($mode ne "roller" ) ){
  219. Log3 $name,1,"[Shelly_Attr] setting the pct100 attribute only works for model=shelly2 and mode=roller";
  220. return
  221. }
  222. #---------------------------------------
  223. }elsif ( ($cmd eq "set") && ($attrName eq "interval") ) {
  224. #-- update timer
  225. $hash->{INTERVAL} = int($attrVal);
  226. if ($init_done) {
  227. RemoveInternalTimer($hash);
  228. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 0);
  229. }
  230. }
  231. return
  232. }
  233. ########################################################################################
  234. #
  235. # Shelly_Get - Implements GetFn function
  236. #
  237. # Parameter hash, argument array
  238. #
  239. ########################################################################################
  240. sub Shelly_Get ($@) {
  241. my ($hash, @a) = @_;
  242. #-- check syntax
  243. my $name = $hash->{NAME};
  244. my $v;
  245. my $model = AttrVal($name,"model","");
  246. my $mode = AttrVal($name,"mode","");
  247. #-- get version
  248. if( $a[1] eq "version") {
  249. return "$name.version => $version";
  250. #-- current status
  251. }elsif($a[1] eq "status") {
  252. $v = Shelly_status($hash);
  253. #-- some help on registers
  254. }elsif($a[1] eq "registers") {
  255. my $txt = "relay";
  256. $txt = "roller"
  257. if( ($model eq "shelly2") && ($mode eq "roller") );
  258. return $shelly_regs{$txt}."\n\nSet/Get these registers by calling set/get $name config &lt;registername&gt; [&lt;channel&gt;] &lt;value&gt;";
  259. #-- configuration register
  260. }elsif($a[1] eq "config") {
  261. my $reg = $a[2];
  262. my ($chan,$val);
  263. if( int(@a) == 5 ){
  264. $chan = $a[3];
  265. $val = $a[4];
  266. }elsif( int(@a) == 4 ){
  267. $chan = 0;
  268. $val = $a[3];
  269. }else{
  270. my $msg = "Error: wrong number of parameters";
  271. Log3 $name,1,"[Shelly_Get] ".$msg;
  272. return $msg;
  273. }
  274. my $pre = "settings/";
  275. if( ($model eq "shelly2") && ($mode eq "roller") ){
  276. $pre .= "roller/$chan?";
  277. }else{
  278. $pre .= "relay/$chan?";
  279. $v = Shelly_configure($hash,$pre.$reg);
  280. }
  281. #-- else
  282. } else {
  283. my $newkeys = join(" ", sort keys %gets);
  284. $newkeys =~ s/:noArg//g
  285. if( $a[1] ne "?");
  286. return "[Shelly_Get] with unknown argument $a[1], choose one of ".$newkeys;
  287. }
  288. if(defined($v)) {
  289. return "$a[0] $a[1] => $v";
  290. }
  291. return "$a[0] $a[1] => ok";
  292. }
  293. ########################################################################################
  294. #
  295. # Shelly_Set - Implements SetFn function
  296. #
  297. # Parameter hash, a = argument array
  298. #
  299. ########################################################################################
  300. sub Shelly_Set ($@) {
  301. my ($hash, @a) = @_;
  302. my $name = shift @a;
  303. my ($newkeys,$cmd,$value,$v,$msg);
  304. $cmd = shift @a;
  305. $value = shift @a;
  306. my $model = AttrVal($name,"model","shelly2");
  307. my $mode = AttrVal($name,"mode","relay");
  308. my ($channel,$time);
  309. #-- we have a Shelly 1,4 or ShellyPlug switch type device
  310. #-- or we have a Shelly 2 switch type device
  311. if( ($model eq "shelly1") || ($model eq "shelly4") || ($model eq "shellyplug") || (($model eq "shelly2") && ($mode eq "relay")) ){
  312. #-- WEB asking for command list
  313. if( $cmd eq "?" ) {
  314. $newkeys = join(" ", sort keys %setssw);
  315. #$newkeys =~ s/on\s/on:0,1 /;
  316. #$newkeys =~ s/off\s/off:0,1 /;
  317. return "[Shelly_Set] Unknown argument " . $cmd . ", choose one of ".$newkeys;
  318. }
  319. if( $cmd =~ /^((on)|(off)).*/ ){
  320. $channel = $value;
  321. if( $cmd =~ /(.*)-for-timer/ ){
  322. $time = $value;
  323. $channel = shift @a;
  324. }
  325. if( $shelly_models{$model}[0] == 1){
  326. $channel = 0
  327. }else{
  328. if( !defined($channel) || ($channel !~ /[0123]/) || $channel >= $shelly_models{$model}[0] ){
  329. if( !defined($channel) ){
  330. $channel = AttrVal($name,"defchannel",undef);
  331. if( !defined($channel) ){
  332. $msg = "Error: wrong channel $channel given and defchannel attribute not set properly";
  333. Log3 $name, 1,"[Shelly_Set] ".$msg;
  334. return $msg;
  335. }else{
  336. Log3 $name, 4,"[Shelly_Set] switching default channel $channel";
  337. }
  338. }
  339. }
  340. }
  341. if( $cmd =~ /(.*)-for-timer/ ){
  342. $cmd = $1;
  343. if( $time !~ /\d+/ ){
  344. $msg = "Error: wrong time spec $time, must be <integer>";
  345. Log3 $name, 1,"[Shelly_Set] ".$msg;
  346. return $msg;
  347. }
  348. $cmd = $cmd."&timer=$time";
  349. }
  350. Shelly_onoff($hash,$channel,"?turn=".$cmd);
  351. }
  352. #-- we have a Shelly 2 roller type device
  353. }elsif( ($model eq "shelly2") && ($mode eq "roller") ){
  354. my $channel = $value;
  355. my $max=AttrVal($name,"maxtime",undef);
  356. #-- WEB asking for command list
  357. if( $cmd eq "?" ) {
  358. $newkeys = join(" ", sort keys %setsrol);
  359. return "[Shelly_Set] Unknown argument " . $cmd . ", choose one of ".$newkeys;
  360. }
  361. if( $hash->{MOVING} ){
  362. $msg = "Error: roller blind still moving, wait for some time";
  363. Log3 $name,1,"[Shelly_Set] ".$msg;
  364. return $msg
  365. }
  366. if( $cmd eq "closed" ){
  367. Shelly_updown($hash,"?go=close");
  368. $hash->{DURATION} = $max;
  369. }elsif( $cmd eq "open" ){
  370. Shelly_updown($hash,"?go=open");
  371. $hash->{DURATION} = $max;
  372. }elsif( $cmd eq "stop" ){
  373. Shelly_updown($hash,"?go=stop");
  374. $hash->{DURATION} = 0;
  375. }elsif( $cmd eq "pct" ){
  376. my $tpct = $value;
  377. my $pos = ReadingsVal($name,"position","");
  378. my $pct = ReadingsVal($name,"pct",undef);
  379. if( !$max ){
  380. $msg = "Error: pct value can be set only if maxtime attribute is set properly";
  381. Log3 $name,1,"[Shelly_Set] ".$msg;
  382. return $msg
  383. }
  384. my $normal = (AttrVal($name,"pct100","open") eq "open");
  385. if( $pos eq "open" ){
  386. #-- 100% = open
  387. if($normal){
  388. $time = int(($max*(100-$tpct))/10)/10;
  389. }else{
  390. $time = int(($max*$pct)/10)/10;
  391. }
  392. $cmd = "?go=close&duration=".$time;
  393. }elsif( $pos eq "closed" ){
  394. #-- 100% = open
  395. if($normal){
  396. $time = int(($max*$tpct)/10)/10;
  397. }else{
  398. $time = int(($max*(100-$tpct))/10)/10;
  399. }
  400. $cmd = "?go=open&duration=".$time;
  401. }else{
  402. if( !defined($pct) ){
  403. $msg = "Error: current pct value unknown. Open or close roller blind before";
  404. Log3 $name,1,"[Shelly_Set] ".$msg;
  405. return $msg;
  406. }
  407. if( $tpct > $pct ){
  408. $time = int(($max*($tpct-$pct))/10)/10;
  409. #-- 100% = open
  410. if($normal){
  411. $cmd = "?go=open&duration=".$time;
  412. }else{
  413. $cmd = "?go=close&duration=".$time;
  414. }
  415. }else{
  416. $time = int(($max*($pct-$tpct))/10)/10;
  417. #-- 100% = open
  418. if($normal){
  419. $cmd = "?go=close&duration=".$time;
  420. }else{
  421. $cmd = "?go=open&duration=".$time;
  422. }
  423. }
  424. }
  425. $hash->{MOVING} = 1;
  426. $hash->{DURATION} = $time;
  427. Shelly_updown($hash,$cmd);
  428. }
  429. }
  430. #-- configuration register
  431. if($cmd eq "config") {
  432. my $reg = $value;
  433. my ($chan,$val);
  434. if( int(@a) == 2 ){
  435. $chan = $a[0];
  436. $val = $a[1];
  437. }elsif( int(@a) == 1 ){
  438. $chan = 0;
  439. $val = $a[0];
  440. }else{
  441. my $msg = "Error: wrong number of parameters";
  442. Log3 $name,1,"[Shelly_Set] ".$msg;
  443. return $msg;
  444. }
  445. my $pre = "settings/";
  446. if( ($model eq "shelly2") && ($mode eq "roller") ){
  447. $pre .= "roller/$chan?";
  448. }else{
  449. $pre .= "relay/$chan?";
  450. $v = Shelly_configure($hash,$pre.$reg."=".$val);
  451. }
  452. }
  453. return undef;
  454. }
  455. ########################################################################################
  456. #
  457. # Shelly_configure - Configure Shelly device
  458. # acts as callable program Shelly_configure($hash,$channel,$cmd)
  459. # and as callback program Shelly_configure($hash,$channel,$cmd,$err,$data)
  460. #
  461. # Parameter hash, channel = 0,1 cmd = command
  462. #
  463. ########################################################################################
  464. sub Shelly_configure {
  465. my ($hash, $cmd, $err, $data) = @_;
  466. my $name = $hash->{NAME};
  467. my $url;
  468. my $state = $hash->{READINGS}{state}{VAL};
  469. my $net = $hash->{READINGS}{network}{VAL};
  470. return
  471. if( $net ne "connected" );
  472. my $model = AttrVal($name,"model","");
  473. if ( $hash && !$err && !$data ){
  474. $url = "http://".$hash->{TCPIP}."/".$cmd;
  475. Log3 $name, 5,"[Shelly_configure] called with only hash => Issue a non-blocking call to $url";
  476. HttpUtils_NonblockingGet({
  477. url => $url,
  478. callback=>sub($$$){ Shelly_configure($hash,$cmd,$_[1],$_[2]) }
  479. });
  480. return undef;
  481. }elsif ( $hash && $err ){
  482. #Log3 $name, 1,"[Shelly_configure] has error $err";
  483. readingsSingleUpdate($hash,"state","Error",1);
  484. return;
  485. }
  486. Log3 $name, 5,"[Shelly_configure] has obtained data $data";
  487. my $json = JSON->new->utf8;
  488. my $jhash = eval{ $json->decode( $data ) };
  489. if( !$jhash ){
  490. Log3 $name,1,"[Shelly_configure] has invalid JSON data";
  491. readingsSingleUpdate($hash,"state","Error",1);
  492. return;
  493. }
  494. #-- isolate register name
  495. my $reg = substr($cmd,index($cmd,"?")+1);
  496. my $val = $jhash->{$reg};
  497. readingsSingleUpdate($hash,"config",$reg."=".$val,1);
  498. return undef;
  499. }
  500. ########################################################################################
  501. #
  502. # Shelly_status - Retrieve data from device
  503. # acts as callable program Shelly_status($hash)
  504. # and as callback program Shelly_status($hash,$err,$data)
  505. #
  506. # Parameter hash
  507. #
  508. ########################################################################################
  509. sub Shelly_status {
  510. my ($hash, $err, $data) = @_;
  511. my $name = $hash->{NAME};
  512. my $url;
  513. my $state = $hash->{READINGS}{state}{VAL};
  514. if ( $hash && !$err && !$data ){
  515. $url = "http://".$hash->{TCPIP}."/status";
  516. Log3 $name, 5,"[Shelly_status] called with only hash => Issue a non-blocking call to $url";
  517. HttpUtils_NonblockingGet({
  518. url => $url,
  519. callback=>sub($$$){ Shelly_status($hash,$_[1],$_[2]) }
  520. });
  521. return undef;
  522. }elsif ( $hash && $err ){
  523. Log3 $name, 1,"[Shelly_status] has error $err";
  524. readingsSingleUpdate($hash,"state","Error",1);
  525. readingsSingleUpdate($hash,"network","not connected",1);
  526. #-- cyclic update nevertheless
  527. RemoveInternalTimer($hash);
  528. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1)
  529. if( $hash->{INTERVAL} ne "0" );
  530. return $err;
  531. }
  532. Log3 $name, 5,"[Shelly_status] has obtained data $data";
  533. my $json = JSON->new->utf8;
  534. my $jhash = eval{ $json->decode( $data ) };
  535. if( !$jhash ){
  536. Log3 $name,1,"[Shelly_status] invalid JSON data";
  537. readingsSingleUpdate($hash,"state","Error",1);
  538. return;
  539. }
  540. my $model = AttrVal($name,"model","shelly2");
  541. my $mode = AttrVal($name,"mode","relay");
  542. my $channels = $shelly_models{$model}[0];
  543. my $rollers = $shelly_models{$model}[1];
  544. my $meters = $shelly_models{$model}[2];
  545. my ($subs,$ison,$overpower,$power,$rstate,$rpower,$rstopreason,$rlastdir);
  546. readingsBeginUpdate($hash);
  547. readingsBulkUpdateIfChanged($hash,"state","OK");
  548. readingsBulkUpdateIfChanged($hash,"network","connected",1);
  549. #-- we have a Shelly 1, Shelly 4, Shelly 2 or ShellyPlug switch type device
  550. if( ($model eq "shelly1") || ($model eq "shellyplug") || ($model eq "shelly4") || (($model eq "shelly2") && ($mode eq "relay")) ){
  551. for( my $i=0;$i<$channels;$i++){
  552. $subs = (($channels == 1) ? "" : "_".$i);
  553. $ison = $jhash->{'relays'}[$i]{'ison'};
  554. $ison =~ s/0/off/;
  555. $ison =~ s/1/on/;
  556. $overpower = $jhash->{'relays'}[$i]{'overpower'};
  557. readingsBulkUpdateIfChanged($hash,"relay".$subs,$ison);
  558. readingsBulkUpdateIfChanged($hash,"overpower".$subs,$overpower);
  559. }
  560. for( my $i=0;$i<$meters;$i++){
  561. $subs = ($meters == 1) ? "" : "_".$i;
  562. $power = $jhash->{'meters'}[$i]{'power'};
  563. readingsBulkUpdateIfChanged($hash,"power".$subs,$power);
  564. }
  565. #-- we have a Shelly 2 roller type device
  566. }elsif( ($model eq "shelly2") && ($mode eq "roller") ){
  567. #-- reset blocking due to existing movement
  568. $hash->{MOVING} = 0;
  569. $hash->{DURATION} = 0;
  570. for( my $i=0;$i<$rollers;$i++){
  571. $subs = ($rollers == 1) ? "" : "_".$i;
  572. $rstate = $jhash->{'rollers'}[$i]{'state'};
  573. $rstate =~ s/close/closed/;
  574. $rpower = $jhash->{'rollers'}[$i]{'power'};
  575. $rstopreason = $jhash->{'rollers'}[$i]{'stop_reason'};
  576. $rlastdir = $jhash->{'rollers'}[$i]{'last_direction'};
  577. $rlastdir =~ s/close/down/;
  578. $rlastdir =~ s/open/up/;
  579. my $pct;
  580. #-- renormalize position
  581. my $normal = (AttrVal($name,"pct100","open") eq "open");
  582. if( $rstate eq "open" ){
  583. #-- 100% = open in case normal
  584. $pct = $normal?100:0;
  585. }elsif( $rstate eq "closed" ){
  586. #-- 100% = open in case normal
  587. $pct = $normal?0:100;
  588. }else{
  589. $pct = ReadingsVal($name,"pct",undef);
  590. $pct = "unknown"
  591. if( !defined($pct) );
  592. }
  593. #-- just in case we have leftover readings from relay devices
  594. #fhem("deletereading ".$name." channel.*");
  595. #fhem("deletereading ".$name." overpower.*");
  596. readingsBulkUpdateIfChanged($hash,"position".$subs,$rstate);
  597. readingsBulkUpdateIfChanged($hash,"power".$subs,$rpower);
  598. readingsBulkUpdateIfChanged($hash,"stop_reason".$subs,$rstopreason);
  599. readingsBulkUpdateIfChanged($hash,"last_dir".$subs,$rlastdir);
  600. readingsBulkUpdateIfChanged($hash,"pct".$subs,$pct);
  601. }
  602. }
  603. #-- common to all Shelly models
  604. my $hasupdate = $jhash->{'update'}{'has_update'};
  605. my $firmware = $jhash->{'update'}{'old_version'};
  606. $firmware =~ /.*\/(.*)\@.*/;
  607. $firmware = $1;
  608. if( $hasupdate ){
  609. my $newfw = $jhash->{'update'}{'new_version'};
  610. $newfw =~ /.*\/(.*)\@.*/;
  611. $newfw = $1;
  612. $firmware .= "(update needed to $newfw)";
  613. }
  614. readingsBulkUpdateIfChanged($hash,"firmware",$firmware);
  615. my $hascloud = $jhash->{'cloud'}{'enabled'};
  616. if( $hascloud ){
  617. my $hasconn = ($jhash->{'cloud'}{'connected'}) ? "connected" : "not connected";
  618. readingsBulkUpdateIfChanged($hash,"cloud","enabled($hasconn)");
  619. }else{
  620. readingsBulkUpdateIfChanged($hash,"cloud","disabled");
  621. }
  622. readingsEndUpdate($hash,1);
  623. #-- cyclic update
  624. RemoveInternalTimer($hash);
  625. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "Shelly_status", $hash, 1)
  626. if( $hash->{INTERVAL} ne "0" );
  627. return undef;
  628. }
  629. ########################################################################################
  630. #
  631. # Shelly_updown - Move rollere blind
  632. # acts as callable program Shelly_updown($hash,$cmd)
  633. # and as callback program Shelly_updown($hash,$cmd,$err,$data)
  634. #
  635. # Parameter hash, channel = 0,1 cmd = command
  636. #
  637. ########################################################################################
  638. sub Shelly_updown {
  639. my ($hash, $cmd, $err, $data) = @_;
  640. my $name = $hash->{NAME};
  641. my $url;
  642. my $state = $hash->{READINGS}{state}{VAL};
  643. my $net = $hash->{READINGS}{network}{VAL};
  644. return
  645. if( $net ne "connected" );
  646. my $model = AttrVal($name,"model","");
  647. #-- empty cmd parameter
  648. $cmd = ""
  649. if( !defined($cmd) );
  650. if ( $hash && !$err && !$data ){
  651. $url = "http://".$hash->{TCPIP}."/roller/0".$cmd;
  652. Log3 $name, 5,"[Shelly_updown] called with only hash => Issue a non-blocking call to $url";
  653. HttpUtils_NonblockingGet({
  654. url => $url,
  655. callback=>sub($$$){ Shelly_updown($hash,$cmd,$_[1],$_[2]) }
  656. });
  657. return undef;
  658. }elsif ( $hash && $err ){
  659. #Log3 $name, 1,"[Shelly_updown] has error $err";
  660. readingsSingleUpdate($hash,"state","Error",1);
  661. return;
  662. }
  663. Log3 $name, 5,"[Shelly_updown] has obtained data $data";
  664. my $json = JSON->new->utf8;
  665. my $jhash = eval{ $json->decode( $data ) };
  666. if( !$jhash ){
  667. if( ($model eq "shelly2") && ($data =~ /Device mode is not roller!/) ){
  668. Log3 $name,1,"[Shelly_updown] Device $name is not in roller mode";
  669. readingsSingleUpdate($hash,"state","Error",1);
  670. return
  671. }else{
  672. Log3 $name,1,"[Shelly_updown] has invalid JSON data";
  673. readingsSingleUpdate($hash,"state","Error",1);
  674. return;
  675. }
  676. }
  677. my ($rstate,$rpower,$rstopreason,$rlastdir,$pct,$normal,$pctopen,$pctclose);
  678. #-- immediately after moving blind
  679. if( $cmd ne ""){
  680. $rstate = "moving";
  681. $pct = ReadingsVal($name,"pct",undef);
  682. $normal = (AttrVal($name,"pct100","open") eq "open");
  683. $pctopen = ($normal && ($pct == 100)) || (!$normal && ($pct == 0));
  684. $pctclose= ($normal && ($pct == 0)) || (!$normal && ($pct == 100));
  685. #-- timer command
  686. if( index($cmd,"&") ne "-1"){
  687. my $max = AttrVal($name,"maxtime",undef);
  688. my $dir = substr($cmd,4,index($cmd,"&")-4);
  689. my $dur = substr($cmd,index($cmd,"&")+10);
  690. if( (!defined($pct) && ($dir eq "close")) || $pctopen ){
  691. #-- 100% = open
  692. if( $normal ){
  693. $pct = 100-int((100*$dur)/$max);
  694. }else{
  695. $pct = int((100*$dur)/$max);
  696. }
  697. }elsif( $dir eq "close" ){
  698. #-- 100% = open
  699. if( $normal ){
  700. $pct = $pct-int((100*$dur)/$max);
  701. }else{
  702. $pct = $pct+int((100*$dur)/$max);
  703. }
  704. }elsif( (!defined($pct) && ($dir eq "open")) || $pctclose ){
  705. #-- 100% = open
  706. if( $normal ){
  707. $pct = int((100*$dur)/$max);
  708. }else{
  709. $pct = 100-int((100*$dur)/$max);
  710. }
  711. }elsif( $dir eq "open" ){
  712. #-- 100% = open
  713. if( $normal ){
  714. $pct = $pct+int((100*$dur)/$max);
  715. }else{
  716. $pct = $pct-int((100*$dur)/$max);
  717. }
  718. }
  719. }
  720. $pct = 0
  721. if( $pct < 0);
  722. $pct = 100
  723. if( $pct > 100);
  724. readingsBeginUpdate($hash);
  725. readingsBulkUpdate($hash,"state","OK");
  726. readingsBulkUpdate($hash,"position",$rstate);
  727. readingsBulkUpdate($hash,"pct",$pct);
  728. readingsEndUpdate($hash,1);
  729. #-- Call us in 1 second again.
  730. InternalTimer(gettimeofday()+ 1, "Shelly_updown", $hash,0);
  731. #--after 1 second
  732. }else{
  733. $rstate = "moving";
  734. $rpower = $jhash->{'power'};
  735. $rstopreason = $jhash->{'stop_reason'};
  736. $rlastdir = $jhash->{'last_direction'};
  737. $rlastdir =~ s/close/down/;
  738. $rlastdir =~ s/open/up/;
  739. readingsBeginUpdate($hash);
  740. readingsBulkUpdate($hash,"state","OK");
  741. readingsBulkUpdate($hash,"position",$rstate);
  742. readingsBulkUpdate($hash,"power",$rpower);
  743. readingsBulkUpdate($hash,"stop_reason",$rstopreason);
  744. readingsBulkUpdate($hash,"last_dir",$rlastdir);
  745. readingsEndUpdate($hash,1);
  746. #-- Call status after movement.
  747. InternalTimer(gettimeofday()+int($hash->{DURATION}+0.5), "Shelly_status", $hash,0);
  748. }
  749. return undef;
  750. }
  751. ########################################################################################
  752. #
  753. # Shelly_onoff - Switch Shelly relay
  754. # acts as callable program Shelly_onoff($hash,$channel,$cmd)
  755. # and as callback program Shelly_onoff($hash,$channel,$cmd,$err,$data)
  756. #
  757. # Parameter hash, channel = 0,1 cmd = command
  758. #
  759. ########################################################################################
  760. sub Shelly_onoff {
  761. my ($hash, $channel, $cmd, $err, $data) = @_;
  762. my $name = $hash->{NAME};
  763. my $url;
  764. my $state = $hash->{READINGS}{state}{VAL};
  765. my $net = $hash->{READINGS}{network}{VAL};
  766. return
  767. if( $net ne "connected" );
  768. my $model = AttrVal($name,"model","");
  769. if ( $hash && !$err && !$data ){
  770. $url = "http://".$hash->{TCPIP}."/relay/".$channel.$cmd;
  771. Log3 $name, 1,"[Shelly_onoff] called with only hash => Issue a non-blocking call to $url";
  772. HttpUtils_NonblockingGet({
  773. url => $url,
  774. callback=>sub($$$){ Shelly_onoff($hash,$channel,$cmd,$_[1],$_[2]) }
  775. });
  776. return undef;
  777. }elsif ( $hash && $err ){
  778. #Log3 $name, 1,"[Shelly_onoff] has error $err";
  779. readingsSingleUpdate($hash,"state","Error",1);
  780. return;
  781. }
  782. Log3 $name, 5,"[Shelly_onoff] has obtained data $data";
  783. my $json = JSON->new->utf8;
  784. my $jhash = eval{ $json->decode( $data ) };
  785. if( !$jhash ){
  786. if( ($model eq "shelly2") && ($data =~ /Device mode is not relay!/) ){
  787. Log3 $name,1,"[Shelly_onoff] Device $name is not in relay mode";
  788. readingsSingleUpdate($hash,"state","Error",1);
  789. return
  790. }else{
  791. Log3 $name,1,"[Shelly_onoff] has invalid JSON data";
  792. readingsSingleUpdate($hash,"state","Error",1);
  793. return;
  794. }
  795. }
  796. my $ison = $jhash->{'ison'};
  797. my $hastimer = $jhash->{'has_timer'};
  798. my $overpower = $jhash->{'overpower'};
  799. $ison =~ s/0/off/;
  800. $ison =~ s/1/on/;
  801. $cmd =~ s/\?turn=//;
  802. #-- timer command
  803. if( index($cmd,"&") ne "-1"){
  804. $cmd = substr($cmd,0,index($cmd,"&"));
  805. if( $hastimer ne "1" ){
  806. Log3 $name,1,"[Shelly_onoff] returns with problem, timer not set";
  807. }
  808. }
  809. if( $ison ne $cmd ) {
  810. Log3 $name,1,"[Shelly_onoff] returns without success, cmd=$cmd but ison=$ison";
  811. }
  812. if( defined($overpower) && $overpower eq "1") {
  813. Log3 $name,1,"[Shelly_onoff] switched off automatically because of overpower signal";
  814. }
  815. #--
  816. my $subs = ($shelly_models{$model}[0] ==1) ? "" : "_".$channel;
  817. readingsBeginUpdate($hash);
  818. readingsBulkUpdate($hash,"state","OK");
  819. readingsBulkUpdate($hash,"relay".$subs,$ison);
  820. readingsBulkUpdate($hash,"overpower".$subs,$overpower)
  821. if( $shelly_models{$model}[2] > 0);
  822. readingsEndUpdate($hash,1);
  823. #InternalTimer(gettimeofday()+ 1, "Shelly_meter", $hash,0)
  824. # if( $shelly_models{$model}[2] > 0);
  825. #-- Call status after switch.
  826. InternalTimer(int(gettimeofday()+1.5), "Shelly_status", $hash,0);
  827. return undef;
  828. }
  829. ########################################################################################
  830. #
  831. # Shelly_meter - Retrieve data from meter
  832. # acts as callable program Shelly_meter($hash,$channel,cmd)
  833. # and as callback program Shelly_meter0($hash,$channel,$cmd,$err,$data)
  834. #
  835. # Parameter hash, channel, cmd = command
  836. #
  837. ########################################################################################
  838. sub Shelly_meter {
  839. my ($hash, $channel, $err, $data) = @_;
  840. my $name = $hash->{NAME};
  841. my $url;
  842. my $state = $hash->{READINGS}{state}{VAL};
  843. my $net = $hash->{READINGS}{network}{VAL};
  844. return
  845. if( $net ne "connected" );
  846. my $model = AttrVal($name,"model","");
  847. if ( $hash && !$err && !$data ){
  848. $url = "http://".$hash->{TCPIP}."/meter/".$channel;
  849. Log3 $name, 5,"[Shelly_meter] called with only hash => Issue a non-blocking call to $url";
  850. HttpUtils_NonblockingGet({
  851. url => $url,
  852. callback=>sub($$$){ Shelly_meter($hash,$channel,$_[1],$_[2]) }
  853. });
  854. return undef;
  855. }elsif ( $hash && $err ){
  856. Log3 $name, 1,"[Shelly_meter has error $err";
  857. readingsSingleUpdate($hash,"state","Error",1);
  858. return;
  859. }
  860. Log3 $name, 5,"[Shelly_meter] has obtained data $data";
  861. my $json = JSON->new->utf8;
  862. my $jhash = eval{ $json->decode( $data ) };
  863. if( !$jhash ){
  864. Log3 $name,1,"[Shelly_meter] invalid JSON data";
  865. readingsSingleUpdate($hash,"state","Error",1);
  866. return;
  867. }
  868. my $power = $jhash->{'power'};
  869. #--
  870. my $subs = ($shelly_models{$model}[2] ==1) ? "" : "_".$channel;
  871. readingsSingleUpdate($hash,"power".$subs,$power,1);
  872. return undef;
  873. }
  874. 1;
  875. =pod
  876. =item device
  877. =item summary to communicate with a Shelly switch/roller actuator
  878. =begin html
  879. <a name="Shelly"></a>
  880. <h3>Shelly</h3>
  881. <ul>
  882. <p> FHEM module to communicate with a Shelly switch/roller actuator</p>
  883. <a name="Shellydefine"></a>
  884. <h4>Define</h4>
  885. <p>
  886. <code>define &lt;name&gt; Shelly &lt;IP address&gt;</code>
  887. <br />Defines the Shelly device. </p>
  888. Notes: <ul>
  889. <li>The attribute <code>model</code> <b>must</b> be set</li>
  890. <li>This module needs the JSON package</li>
  891. </ul>
  892. <a name="Shellyset"></a>
  893. <h4>Set</h4>
  894. For Shelly all Shelly devices
  895. <ul>
  896. <li><a name="shelly_sconfig"></a>
  897. <code>set &lt;name&gt; config <registername> [&lt;channel&gt;] &lt;value&gt;</code>
  898. <br />set the value of a configuration register</li>
  899. </ul>
  900. For Shelly switching devices (mode=relay for model=shelly2, standard for all other models)
  901. <ul>
  902. <li><a name="shelly_onoff"></a>
  903. <code>set &lt;name&gt; on|off [&lt;channel&gt;] </code>
  904. <br />switches channel &lt;channel&gt; on or off. Only if model=shelly2/4: If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.</li>
  905. <li><a name="shelly_onofftimer"></a>
  906. <code>set &lt;name&gt; on-for-timer|off-for-timer &lt;time&gt; [&lt;channel&gt;] </code>
  907. <br />switches &lt;channel&gt; on or off for &lt;time&gt; seconds. Only if model=shelly2/4: If the channel parameter is omitted, the module will switch the channel defined in the defchannel attribute.</li>
  908. </ul>
  909. <br/>For Shelly roller blind devices (mode=roller for model=shelly2)
  910. <ul>
  911. <li><a name="shelly_updown"></a>
  912. <code>set &lt;name&gt; open|closed|stop </code>
  913. <br />drives the roller blind open, closed or to a stop.</li>
  914. <li><a name="shelly_pct"></a>
  915. <code>set &lt;name&gt; pct &lt;integer percent value&gt; </code>
  916. <br />drives the roller blind to a partially closed position (100=open, 0=closed)</li>
  917. </ul>
  918. <a name="Shellyget"></a>
  919. <h4>Get</h4>
  920. <ul>
  921. <li><a name="shelly_config"></a>
  922. <code>get &lt;name&gt; config &lt;registername&gt; [$lt;channel&gt;]</code>
  923. <br />get the value of a configuration register and writes it in reading config</li>
  924. <li><a name="shelly_registers"></a>
  925. <code>get &lt;name&gt; registers</code>
  926. <br />displays the names of the configuration registers for this device</li>
  927. <li><a name="shelly_status"></a>
  928. <code>get &lt;name&gt; status</code>
  929. <br />returns the current devices status.</li>
  930. <li><a name="shelly_version"></a>
  931. <code>get &lt;name&gt; version</code>
  932. <br />display the version of the module</li>
  933. </ul>
  934. <a name="Shellyattr"></a>
  935. <h4>Attributes</h4>
  936. <ul>
  937. <li><a name="shelly_model"><code>attr &lt;name&gt; model shelly1|shelly2|shelly4|shellyplug </code></a>
  938. <br />type of the Shelly device</li>
  939. <li><a name="shelly_mode"><code>attr &lt;name&gt; mode relay|roller (only for model=shelly2)</code></a>
  940. <br />type of the Shelly device</li>
  941. <li><a name="shelly_interval">
  942. <code>&lt;interval&gt;</code>
  943. <br />Update interval for reading in seconds. The default is 60 seconds, a value of 0 disables the automatic update. </li>
  944. </ul>
  945. <br/>For Shelly switching devices (mode=relay for model=shelly2, standard for all other models)
  946. <ul>
  947. <li><a name="shelly_defchannel"><code>attr &lt;name&gt; defchannel <integer> (only for model=shelly2|shelly4)</code></a>
  948. <br />for multi-channel switches: Which channel will be switched, if a command is received without channel number</li>
  949. </ul>
  950. <br/>For Shelly roller blind devices (mode=roller for model=shelly2)
  951. <ul>
  952. <li><a name="shelly_maxtime"><code>attr &lt;name&gt; maxtime &lt;float&gt; </code></a>
  953. <br />time needed for a complete drive upward or downward</li>
  954. <li><a name="shelly_pct100"><code>attr &lt;name&gt; pct100 open|closed (default:open) </code></a>
  955. <br />is pct=100 open or closed ? </li>
  956. </ul>
  957. <br/>Standard attributes
  958. <ul>
  959. <li><a href="#alias">alias</a>, <a href="#comment">comment</a>, <a
  960. href="#event-on-update-reading">event-on-update-reading</a>, <a
  961. href="#event-on-change-reading">event-on-change-reading</a>, <a href="#room"
  962. >room</a>, <a href="#eventMap">eventMap</a>, <a href="#loglevel">loglevel</a>,
  963. <a href="#webCmd">webCmd</a></li>
  964. </ul>
  965. </ul>
  966. =end html
  967. =begin html_DE
  968. <h3>Shelly</h3>
  969. <ul>
  970. Absichtlich keine deutsche Dokumentation vorhanden, die englische Version gibt es hier: <a href="/fhem/docs/commandref.html#Shelly">Shelly</a>
  971. </ul>
  972. =end html_DE
  973. =cut