31_LightScene.pm 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129
  1. # $Id: 31_LightScene.pm 17044 2018-07-28 18:34:46Z justme1968 $
  2. package main;
  3. use strict;
  4. use warnings;
  5. use POSIX;
  6. #use JSON;
  7. #use Data::Dumper;
  8. use vars qw($FW_ME);
  9. use vars qw($FW_subdir);
  10. use vars qw($FW_wname);
  11. use vars qw($FW_cname);
  12. use vars qw(%FW_webArgs); # all arguments specified in the GET
  13. my $LightScene_hasJSON = 1;
  14. my $LightScene_hasDataDumper = 1;
  15. my $scn = ''; # scene used for edit-table
  16. sub LightScene_Initialize($)
  17. {
  18. my ($hash) = @_;
  19. $hash->{DefFn} = "LightScene_Define";
  20. $hash->{NotifyFn} = "LightScene_Notify";
  21. $hash->{UndefFn} = "LightScene_Undefine";
  22. $hash->{SetFn} = "LightScene_Set";
  23. $hash->{GetFn} = "LightScene_Get";
  24. $hash->{AttrFn} = "LightScene_Attr";
  25. $hash->{AttrList} = "async_delay followDevices:1,2 lightSceneRestoreOnlyIfChanged:1,0 showDeviceCurrentState:1,0 switchingOrder traversalOrder ". $readingFnAttributes;
  26. $hash->{FW_detailFn} = "LightScene_detailFn";
  27. $data{FWEXT}{"/LightScene"}{FUNC} = "LightScene_CGI"; #mod
  28. eval "use JSON";
  29. $LightScene_hasJSON = 0 if($@);
  30. eval "use Data::Dumper";
  31. $LightScene_hasDataDumper = 0 if($@);
  32. }
  33. sub LightScene_Define($$)
  34. {
  35. my ($hash, $def) = @_;
  36. return "install JSON (or Data::Dumper) to use LightScene" if( !$LightScene_hasJSON && !$LightScene_hasDataDumper );
  37. my @args = split("[ \t]+", $def);
  38. return "Usage: define <name> LightScene <device>+" if(@args < 3);
  39. my $name = shift(@args);
  40. my $type = shift(@args);
  41. $hash->{HAS_JSON} = $LightScene_hasJSON;
  42. $hash->{HAS_DataDumper} = $LightScene_hasDataDumper;
  43. my %list;
  44. foreach my $a (@args) {
  45. foreach my $d (devspec2array($a)) {
  46. $list{$d} = 1;
  47. addToDevAttrList( $d, "lightSceneParamsToSave" );
  48. addToDevAttrList( $d, "lightSceneRestoreOnlyIfChanged:1,0" );
  49. }
  50. }
  51. $hash->{CONTENT} = \%list;
  52. if( !defined($hash->{SCENES}) ) {
  53. my %scenes;
  54. $hash->{SCENES} = \%scenes;
  55. LightScene_Load($hash);
  56. }
  57. LightScene_updateHelper( $hash, AttrVal($name,"switchingOrder",undef) );
  58. my @arr = ();
  59. $hash->{".asyncQueue"} = \@arr;
  60. $hash->{STATE} = 'Initialized';
  61. return undef;
  62. }
  63. sub LightScene_Undefine($$)
  64. {
  65. my ($hash,$arg) = @_;
  66. delete $hash->{SCENES};
  67. return undef;
  68. }
  69. sub
  70. LightScene_2html($)
  71. {
  72. my($hash) = @_;
  73. $hash = $defs{$hash} if( ref($hash) ne 'HASH' );
  74. return undef if( !$hash );
  75. my $name = $hash->{NAME};
  76. my $room = $FW_webArgs{room};
  77. my $show_heading = 1;
  78. my $row = 1;
  79. my $ret = "";
  80. $ret .= "<table>";
  81. $ret .= "<tr><td><div class=\"devType\"><a href=\"$FW_ME?detail=$name\">".AttrVal($name, "alias", $name)."</a></div></td></tr>" if( $show_heading );
  82. $ret .= "<tr><td><table class=\"block wide\">";
  83. if( defined($FW_webArgs{detail}) || AttrVal($name,"showDeviceCurrentState",undef) ) {
  84. $room = "&detail=$FW_webArgs{detail}" if( defined($FW_webArgs{detail}) );
  85. $ret .= sprintf("<tr class=\"%s\">", ($row&1)?"odd":"even");
  86. #$row++;
  87. $ret .= "<td><div></div></td>";
  88. foreach my $d (sort keys %{ $hash->{CONTENT} }) {
  89. my %extPage = ();
  90. my ($allSets, $cmdlist, $txt) = FW_devState($d, $room, \%extPage);
  91. $ret .= "<td style=\"cursor:pointer\" informId=\"$name-$d.state\">$txt</td>";
  92. }
  93. }
  94. $ret .= sprintf("<tr class=\"%s\">", ($row&1)?"odd":"even");
  95. $row++;
  96. $ret .= "<td><div></div></td>";
  97. foreach my $d (sort keys %{ $hash->{CONTENT} }) {
  98. $ret .= "<td><div class=\"col2\"><a href=\"$FW_ME?detail=$d\">". AttrVal($d, "alias", $d) ."</a></div></td>";
  99. }
  100. foreach my $scene (sort keys %{ $hash->{SCENES} }) {
  101. $ret .= sprintf("<tr class=\"%s\">", ($row&1)?"odd":"even");
  102. $row++;
  103. my $srf = $room ? "&room=$room" : "";
  104. $srf = $room if( $room && $room =~ m/^&/ );
  105. my $link = "cmd=set $name scene $scene";
  106. my $txt = $scene;
  107. if( 1 ) {
  108. my ($icon, $link, $isHtml) = FW_dev2image($name, $scene);
  109. $txt = ($isHtml ? $icon : FW_makeImage($icon, $scene)) if( $icon );
  110. }
  111. if( AttrVal($FW_wname, "longpoll", 1)) {
  112. $txt = "<a style=\"cursor:pointer\" onClick=\"FW_cmd('$FW_ME$FW_subdir?XHR=1&$link')\">$txt</a>";
  113. } else {
  114. $txt = "<a href=\"$FW_ME$FW_subdir?$link$srf\">$txt</a>";
  115. }
  116. $ret .= "<td><div>$txt</div></td>";
  117. foreach my $d (sort keys %{ $hash->{CONTENT} }) {
  118. if( !defined($hash->{SCENES}{$scene}{$d} ) ) {
  119. $ret .= "<td><div></div></td>";
  120. next;
  121. }
  122. my $icon;
  123. my $state = $hash->{SCENES}{$scene}{$d};
  124. $icon = $state->{icon} if( ref($state) eq 'HASH' );
  125. $state = $state->{state} if( ref($state) eq 'HASH' );
  126. my ($isHtml);
  127. $isHtml = 0;
  128. if( !$icon ) {
  129. my ($link);
  130. ($icon, $link, $isHtml) = FW_dev2image($d, $state);
  131. }
  132. $icon = FW_iconName($state) if( !$icon );
  133. if( $icon ) {
  134. $ret .= "<td><div class=\"col2\">". ($isHtml ? $icon : FW_makeImage($icon, $state)) ."</div></td>";
  135. } else {
  136. $ret .= "<td><div>". $state ."</div></td>";
  137. }
  138. }
  139. }
  140. $ret .= "</table></td></tr>";
  141. $ret .= "</table>";
  142. $ret .= "<br>";
  143. return $ret;
  144. }
  145. sub
  146. LightScene_detailFn()
  147. {
  148. my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  149. my $hash = $defs{$d};
  150. $hash->{mayBeVisible} = 1;
  151. my $html = LightScene_2html($d); #mod
  152. $html .= LightScene_editTable($hash); #mod
  153. return $html;
  154. }
  155. sub
  156. LightScene_Notify($$)
  157. {
  158. my ($hash,$dev) = @_;
  159. my $name = $hash->{NAME};
  160. my $type = $hash->{TYPE};
  161. if( grep(m/^INITIALIZED$/, @{$dev->{CHANGED}}) ) {
  162. } elsif( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) {
  163. LightScene_Save();
  164. }
  165. return if($dev->{TYPE} eq $hash->{TYPE});
  166. my $max = int(@{$dev->{CHANGED}});
  167. for (my $i = 0; $i < $max; $i++) {
  168. my $s = $dev->{CHANGED}[$i];
  169. $s = "" if(!defined($s));
  170. if($s =~ m/^RENAMED ([^ ]*) ([^ ]*)$/) {
  171. my ($old, $new) = ($1, $2);
  172. if( defined($hash->{CONTENT}{$old}) ) {
  173. $hash->{DEF} =~ s/(^|\s+)$old(\s+|$)/$1$new$2/;
  174. foreach my $scene (keys %{ $hash->{SCENES} }) {
  175. $hash->{SCENES}{$scene}{$new} = $hash->{SCENES}{$scene}{$old} if( defined($hash->{SCENES}{$scene}{$old}) );
  176. delete( $hash->{SCENES}{$scene}{$old} );
  177. }
  178. delete( $hash->{CONTENT}{$old} );
  179. $hash->{CONTENT}{$new} = 1;
  180. }
  181. } elsif($s =~ m/^DELETED ([^ ]*)$/) {
  182. my ($name) = ($1);
  183. if( defined($hash->{CONTENT}{$name}) ) {
  184. $hash->{DEF} =~ s/(^|\s+)$name(\s+|$)/ /;
  185. $hash->{DEF} =~ s/^ //;
  186. $hash->{DEF} =~ s/ $//;
  187. foreach my $scene (keys %{ $hash->{SCENES} }) {
  188. delete( $hash->{SCENES}{$scene}{$name} );
  189. }
  190. delete( $hash->{CONTENT}{$name} );
  191. }
  192. } else {
  193. next if (!$hash->{CONTENT}->{$dev->{NAME}});
  194. if( !defined($hash->{mayBeVisible}) ) {
  195. Log3 $name, 5, "$name: not on any display, ignoring notify";
  196. return undef if( !$hash->{followDevices} );
  197. } else {
  198. if( defined($FW_visibleDeviceHash{$name}) ) {
  199. } else {
  200. Log3 $name, 5, "$name: no longer visible, ignoring notify";
  201. delete( $hash->{mayBeVisible} );
  202. return undef if( !$hash->{followDevices} );
  203. }
  204. }
  205. return undef if ( !$hash->{mayBeVisible} && !$hash->{followDevices} );
  206. my @parts = split(/: /,$s);
  207. my $reading = shift @parts;
  208. my $value = join(": ", @parts);
  209. next if( $value ne "" );
  210. $reading = "state";
  211. $value = $s;
  212. if( $hash->{mayBeVisible} || $hash->{followDevices} ) {
  213. my $room = AttrVal($name, "room", "");
  214. my %extPage = ();
  215. (undef, undef, $value) = FW_devState($dev->{NAME}, $room, \%extPage);
  216. DoTrigger( $name, "$dev->{NAME}.$reading: <html>$value</html>" );
  217. }
  218. if( $hash->{followDevices} ) {
  219. my %s = ();
  220. foreach my $d (@{$hash->{devices}}) {
  221. next if(!$defs{$d});
  222. my($state,undef,undef) = LightScene_SaveDevice($hash,$d);
  223. $s{$d} = $state;
  224. }
  225. my $matched = 0;
  226. foreach my $scene (sort keys %{ $hash->{SCENES} }) {
  227. $matched = (scalar keys %{ $hash->{SCENES}{$scene} } > 0)?1:0;
  228. foreach my $d (sort keys %{ $hash->{SCENES}{$scene} }) {
  229. next if( !defined($hash->{SCENES}{$scene}{$d}));
  230. next if(!$defs{$d});
  231. my $state = $hash->{SCENES}{$scene}{$d};
  232. $state = $state->{state} if( ref($state) eq 'HASH' );
  233. if( ref($state) eq 'ARRAY' ) {
  234. $matched = 0;
  235. } elsif( !defined($s{$d}) || $state ne $s{$d} ) {
  236. $matched = 0;
  237. }
  238. last if( !$matched );
  239. }
  240. readingsSingleUpdate($hash, "state", $scene, 1 ) if( $matched );
  241. last if( $matched );
  242. }
  243. if( !$matched ) {
  244. if( $hash->{followDevices} == 2 ) {
  245. readingsSingleUpdate($hash, "state", "unknown", 1 );
  246. } else {
  247. DoTrigger( $name, "nomatch" )
  248. }
  249. }
  250. }
  251. }
  252. }
  253. return undef;
  254. }
  255. sub
  256. myStatefileName()
  257. {
  258. my $statefile = $attr{global}{statefile};
  259. my @t = localtime(gettimeofday());
  260. $statefile = ResolveDateWildcards($statefile, @t);
  261. $statefile = substr $statefile,0,rindex($statefile,'/')+1;
  262. return $statefile ."LightScenes.save" if( $LightScene_hasJSON );
  263. return $statefile ."LightScenes.dd.save" if( $LightScene_hasDataDumper );
  264. }
  265. my $LightScene_LastSaveTime="";
  266. sub
  267. LightScene_Save()
  268. {
  269. my $time_now = TimeNow();
  270. return if( $time_now eq $LightScene_LastSaveTime);
  271. $LightScene_LastSaveTime = $time_now;
  272. return "No statefile specified" if(!$attr{global}{statefile});
  273. my $statefile = myStatefileName();
  274. my $hash;
  275. for my $d (keys %defs) {
  276. next if( !$defs{$d}{TYPE} );
  277. next if( $defs{$d}{TYPE} ne "LightScene" );
  278. next if( !defined($defs{$d}{SCENES}) );
  279. $hash->{$d} = $defs{$d}{SCENES} if( keys(%{$defs{$d}{SCENES}}) );
  280. }
  281. if(open(FH, ">$statefile")) {
  282. my $t = localtime;
  283. print FH "#$t\n";
  284. if( $LightScene_hasJSON ) {
  285. print FH encode_json($hash) if( defined($hash) );
  286. } elsif( $LightScene_hasDataDumper ) {
  287. my $dumper = Data::Dumper->new([]);
  288. $dumper->Terse(1);
  289. $dumper->Values([$hash]);
  290. print FH $dumper->Dump;
  291. }
  292. close(FH);
  293. } else {
  294. my $msg = "LightScene_Save: Cannot open $statefile: $!";
  295. Log3 undef, 1, $msg;
  296. }
  297. return undef;
  298. }
  299. sub
  300. LightScene_Load($)
  301. {
  302. my ($hash) = @_;
  303. return "No statefile specified" if(!$attr{global}{statefile});
  304. my $statefile = myStatefileName();
  305. if(open(FH, "<$statefile")) {
  306. my $encoded;
  307. while (my $line = <FH>) {
  308. chomp $line;
  309. next if($line =~ m/^#.*$/);
  310. $encoded .= $line;
  311. }
  312. close(FH);
  313. return if( !defined($encoded) );
  314. my $decoded;
  315. if( $LightScene_hasJSON ) {
  316. $decoded = decode_json( $encoded );
  317. } elsif( $LightScene_hasDataDumper ) {
  318. $decoded = eval $encoded;
  319. }
  320. $hash->{SCENES} = $decoded->{$hash->{NAME}} if( defined($decoded->{$hash->{NAME}}) );
  321. } else {
  322. my $msg = "LightScene_Load: Cannot open $statefile: $!";
  323. Log3 undef, 1, $msg;
  324. }
  325. return undef;
  326. }
  327. sub
  328. LightScene_SaveDevice($$;$$)
  329. {
  330. my($hash,$d,$scene,$desc) = @_;
  331. my $state = "";
  332. my $icon = undef;
  333. my $id = undef;
  334. my $type = $defs{$d}->{TYPE};
  335. $type = "" if( !defined($type) );
  336. if( my $toSave = AttrVal($d,"lightSceneParamsToSave","") ) {
  337. $icon = Value($d);
  338. if( $toSave =~ m/^{.*}$/) {
  339. my $DEVICE = $d;
  340. $toSave = eval $toSave;
  341. $toSave = "state" if( $@ );
  342. }
  343. my @sets = split(',', $toSave);
  344. foreach my $set (@sets) {
  345. my $saved = "";
  346. my @params = split(':', $set);
  347. foreach my $param (@params) {
  348. $saved .= " : " if( $saved );
  349. my $use_get = 0;
  350. my $get = $param;
  351. my $regex;
  352. my $set = $param;
  353. if( $param =~ /(get\s+)?(\S*)(\s*->\s*(set\s+)?)?(\S*)?/ ) {
  354. $use_get = 1 if( $1 );
  355. $get = $2 if( $2 );
  356. $set = $5 if( $5 );
  357. }
  358. ($get,$regex) = split('@', $get, 2);
  359. $set = $get if( $regex && $set eq $param );
  360. $set = "state" if( $set eq "STATE" );
  361. $saved .= "$set " if( $set ne "state" );
  362. my $value;
  363. if( $use_get ) {
  364. $value = CommandGet( "", "$d $get" );
  365. } elsif( $get eq "STATE" ) {
  366. $value = Value($d);
  367. } else {
  368. $value = ReadingsVal($d,$get,undef);
  369. }
  370. $value = eval $regex if( $regex );
  371. Log3 $hash, 2, "$hash->{NAME}: $@" if($@);
  372. $saved .= $value;
  373. }
  374. if( !$state ) {
  375. $state = $saved;
  376. } else {
  377. $state = [$state] if( ref($state) ne 'ARRAY' );
  378. push( @{$state}, $saved );
  379. }
  380. }
  381. } elsif( $type eq 'CUL_HM' ) {
  382. #my $subtype = AttrVal($d,"subType","");
  383. my $subtype = CUL_HM_Get($defs{$d},$d,"param","subType");
  384. if( $subtype eq "switch" ) {
  385. $state = Value($d);
  386. } elsif( $subtype eq "dimmer" ) {
  387. $state = Value($d);
  388. if ( $state =~ m/^(\d+)/ ) {
  389. $icon = $state;
  390. $state = $1 if ( $state =~ m/^(\d+)/ );
  391. }
  392. } else {
  393. $state = Value($d);
  394. }
  395. } elsif( $type eq 'FS20' ) {
  396. $state = Value($d);
  397. } elsif( $type eq 'SWAP_0000002200000003' ) {
  398. $state = Value($d);
  399. $state = "rgb ". $state if( $state ne "off" );
  400. } elsif( $type eq 'HUEDevice' ) {
  401. my $subtype = AttrVal($d,"subType","");
  402. if( $defs{$d}->{helper}->{devtype} eq "G" ) {
  403. if( $scene ) {
  404. if( ref($desc) eq 'HASH' ) {
  405. $id = $desc->{id} if( $desc->{id} );
  406. fhem( "set $d deletescene $id" );
  407. }
  408. my $name = "FHEM-$hash->{NAME}-$scene";
  409. my $ret = fhem( "set $d savescene $name" );
  410. if( $ret =~ m/^created (.*)/ ) {
  411. $id = $1;
  412. }
  413. $state = "scene $id";
  414. } else {
  415. $state = "<unknown>";
  416. }
  417. } elsif( $subtype eq "switch" || Value($d) eq "off" ) {
  418. $state = Value($d);
  419. } elsif( $subtype eq "dimmer" ) {
  420. $state = "bri ". ReadingsVal($d,'bri',"0");
  421. } elsif( $subtype =~ m/color|ct/ ) {
  422. my $cm = ReadingsVal($d,"colormode","");
  423. if( $cm eq "ct" ) {
  424. ReadingsVal($d,"ct","") =~ m/(\d+) .*/;
  425. $state = "bri ". ReadingsVal($d,'bri',"0") ." : ct ". $1;
  426. } elsif( $cm eq "hs" ) {
  427. $state = "bri ". ReadingsVal($d,'bri',"0") ." : hue ". ReadingsVal($d,'hue',"") ." : sat ". ReadingsVal($d,'sat',"");
  428. } else {
  429. $state = "bri ". ReadingsVal($d,'bri',"0") ." : xy ". ReadingsVal($d,'xy',"");
  430. }
  431. }
  432. } elsif( $type eq 'IT' ) {
  433. my $subtype = AttrVal($d,"model","");
  434. if( $subtype eq "itswitch" ) {
  435. $state = Value($d);
  436. } elsif( $subtype eq "itdimmer" ) {
  437. $state = Value($d);
  438. } else {
  439. $state = Value($d);
  440. }
  441. } elsif( $type eq 'TRX_LIGHT' ) {
  442. $state = Value($d);
  443. } else {
  444. $state = Value($d);
  445. }
  446. return($state,$icon,$type,$id);
  447. }
  448. sub
  449. LightScene_RestoreDevice($$$)
  450. {
  451. my($hash,$d,$cmd) = @_;
  452. if( AttrVal($d,"lightSceneRestoreOnlyIfChanged", AttrVal($hash->{NAME},"lightSceneRestoreOnlyIfChanged",0) ) > 0 )
  453. {
  454. my($state,undef,undef) = LightScene_SaveDevice($hash,$d);
  455. return ("",0) if( $state eq $cmd );
  456. }
  457. my $async_delay = AttrVal($hash->{NAME}, "async_delay", undef);
  458. my $ret;
  459. if( $cmd =~m/^;/ ) {
  460. if(defined($async_delay)) {
  461. push @{$hash->{".asyncQueue"}}, $cmd;
  462. } else {
  463. $ret = AnalyzeCommandChain(undef,$cmd);
  464. }
  465. } else {
  466. if(defined($async_delay)) {
  467. push @{$hash->{".asyncQueue"}}, "$d $cmd";
  468. } else {
  469. $ret = CommandSet(undef,"$d $cmd");
  470. }
  471. }
  472. return ($ret,1);
  473. }
  474. sub
  475. LightScene_Set($@)
  476. {
  477. my ($hash, $name, $cmd, $scene, @a) = @_;
  478. my $ret = "";
  479. if( !defined($cmd) ){ return "$name: set needs at least one parameter" };
  480. my @sorted = sort keys %{$hash->{SCENES}};
  481. if( $cmd eq "?" ){ return "Unknown argument ?, choose one of remove:".join(",", @sorted) ." rename save set setcmd scene:".join(",", @sorted) ." all nextScene:noArg previousScene:noArg"};
  482. if( $cmd eq "all" && !defined( $scene ) ) { return "Usage: set $name all <command>" };
  483. if( $cmd eq "save" && !defined( $scene ) ) { return "Usage: set $name save <scene_name>" };
  484. if( $cmd eq "scene" && !defined( $scene ) ) { return "Usage: set $name scene <scene_name>" };
  485. if( $cmd eq "remove" && !defined( $scene ) ) { return "Usage: set $name remove <scene_name>" };
  486. if( $cmd eq "rename" && !defined( $scene ) ) { return "Usage: set $name rename <scene_alt> <scene_neu>" };
  487. if( $cmd eq "remove" ) {
  488. return "no such scene: $scene" if( !defined $hash->{SCENES}{$scene} );
  489. delete( $hash->{SCENES}{$scene} );
  490. return undef;
  491. } elsif( $cmd eq "rename" ) {
  492. return "no such scene: $scene" if( !defined $hash->{SCENES}{$scene} );
  493. my ($new) = @a;
  494. if( !( $new ) ) { return "Usage: set $name rename <scene_alt> <scene_neu>" };
  495. $hash->{SCENES}{$new} = $hash->{SCENES}{$scene};
  496. delete( $hash->{SCENES}{$scene} );
  497. return undef;
  498. } elsif( $cmd eq "scene" ) {
  499. return "no such scene: $scene" if( !defined $hash->{SCENES}{$scene} );
  500. } elsif( $cmd eq "set" || $cmd eq "setcmd" ) {
  501. my ($d, @args) = @a;
  502. if( !defined( $scene ) || !defined( $d ) ) { return "Usage: set $name set <scene_name> <device> [<cmd>]" };
  503. return "no such scene: $scene" if( !defined $hash->{SCENES}{$scene} );
  504. #return "device >$d< is not a member of scene >$scene<" if( !defined($hash->{CONTENT}{$d} ) );
  505. if( !@args ) {
  506. delete $hash->{SCENES}{$scene}{$d};
  507. } else {
  508. $hash->{SCENES}{$scene}{$d} = (($cmd eq "setcmd")?';':''). join(" ", @args);
  509. }
  510. LightScene_updateHelper( $hash, AttrVal($name,"switchingOrder",undef) );
  511. return undef;
  512. } elsif( $cmd eq "updateToJson" && $LightScene_hasDataDumper && $LightScene_hasJSON ) {
  513. $LightScene_hasJSON = 0;
  514. LightScene_Load($hash);
  515. LightScene_updateHelper( $hash, AttrVal($name,"switchingOrder",undef) );
  516. $LightScene_hasJSON = 1;
  517. LightScene_Save();
  518. return undef;
  519. } elsif( $cmd eq 'nextScene' || $cmd eq 'previousScene' ) {
  520. my $sorted = \@sorted;
  521. if( my $list = AttrVal($name, 'traversalOrder', undef ) ) {
  522. my @parts = split( /[ ,\n]/, $list );
  523. $sorted = \@parts;
  524. }
  525. my $max = scalar @{$sorted}-1;
  526. return "no scenes defined" if( $max < 0 );
  527. my $current = ReadingsVal( $name, 'state', '' );
  528. my( $index )= grep { $sorted->[$_] eq $current } 0..$max;
  529. $index = -1 if( !defined($index) );
  530. ++$index if( $cmd eq 'nextScene' );
  531. --$index if( $cmd eq 'previousScene' );
  532. return if( $scene && $scene eq 'nowrap' && $index > $max );
  533. return if( $scene && $scene eq 'nowrap' && $index < 0 );
  534. $index = 0 if( $index > $max );
  535. $index = $max if( $index < 0 );
  536. $cmd = 'scene';
  537. $scene = $sorted->[$index];
  538. return "no such scene: $scene" if( !defined $hash->{SCENES}{$scene} );
  539. }
  540. $hash->{INSET} = 1;
  541. my @devices;
  542. if( ( $cmd eq "scene" || $cmd eq "all" )
  543. && defined($hash->{switchingOrder}) && defined($hash->{switchingOrder}{$scene}) ) {
  544. @devices = @{$hash->{switchingOrder}{$scene}};
  545. } else {
  546. @devices = @{$hash->{devices}};
  547. }
  548. my $count = 0;
  549. my $async_delay = AttrVal($hash->{NAME}, "async_delay", undef);
  550. my $asyncQueueLength = @{$hash->{".asyncQueue"}};
  551. foreach my $d (@devices) {
  552. next if(!$defs{$d});
  553. if($defs{$d}{INSET}) {
  554. Log3 $name, 1, "ERROR: endless loop detected for $d in " . $hash->{NAME};
  555. next;
  556. }
  557. if( $cmd eq "save" ) {
  558. my($state,$icon,$type,$id) = LightScene_SaveDevice($hash,$d,$scene,$hash->{SCENES}{$scene}{$d});
  559. if( $icon || ref($state) eq 'ARRAY' || $type eq "SWAP_0000002200000003" || $type eq "HUEDevice" ) {
  560. my %desc;
  561. $desc{state} = $state;
  562. my ($icon, $link, $isHtml) = FW_dev2image($d);
  563. $desc{icon} = $icon;
  564. $desc{id} = $id if( $id );
  565. $hash->{SCENES}{$scene}{$d} = \%desc;
  566. } else {
  567. $hash->{SCENES}{$scene}{$d} = $state;
  568. }
  569. $ret .= $d .": ". $state ."\n" if( defined($FW_webArgs{room}) && $FW_webArgs{room} eq "all" ); #only if telnet
  570. } elsif ( $cmd eq "scene" ) {
  571. next if( !defined($hash->{SCENES}{$scene}{$d}));
  572. my $state = $hash->{SCENES}{$scene}{$d};
  573. $state = $state->{state} if( ref($state) eq 'HASH' );
  574. if( ref($state) eq 'ARRAY' ) {
  575. my $r = "";
  576. foreach my $entry (@{$state}) {
  577. $r .= "," if( $ret );
  578. my($rr,$switched) = LightScene_RestoreDevice($hash,$d,$entry);
  579. $count += $switched;
  580. $r .= $rr // "";
  581. }
  582. $ret .= " " if( $ret );
  583. $ret .= $r;
  584. } else {
  585. $ret .= " " if( $ret );
  586. my($rr,$switched) = LightScene_RestoreDevice($hash,$d,$state);
  587. $count += $switched;
  588. $ret .= $rr // "";
  589. }
  590. } elsif ( $cmd eq "all" ) {
  591. $ret .= " " if( $ret );
  592. my($rr,$switched) = LightScene_RestoreDevice($hash,$d,"$scene ".join(" ", @a));
  593. $count += $switched;
  594. $ret .= $rr // "";
  595. } else {
  596. $ret = "Unknown argument $cmd, choose one of save scene";
  597. }
  598. }
  599. if( $cmd eq "scene" ) {
  600. readingsSingleUpdate($hash, "state", $scene, 1 ) if( !$hash->{followDevices} || $count == 0 );
  601. } elsif( $cmd eq "all" ) {
  602. readingsSingleUpdate($hash, "state", "all $scene ".join(" ", @a), 1 ) if( !$hash->{followDevices} || $count == 0 );
  603. }
  604. delete($hash->{INSET});
  605. Log3 $hash, 5, "SET: $ret" if($ret);
  606. LightScene_updateHelper( $hash, AttrVal($name,"switchingOrder",undef) );
  607. InternalTimer(gettimeofday()+0, "LightScene_asyncQueue", $hash, 0) if( @{$hash->{".asyncQueue"}} && !$asyncQueueLength );
  608. return $ret;
  609. return undef;
  610. }
  611. sub
  612. LightScene_asyncQueue(@)
  613. {
  614. my ($hash) = @_;
  615. my $cmd = shift @{$hash->{".asyncQueue"}};
  616. if(defined $cmd) {
  617. if( $cmd =~m/^;/ ) {
  618. AnalyzeCommandChain(undef,$cmd);
  619. } else {
  620. CommandSet(undef, $cmd);
  621. }
  622. my $async_delay = AttrVal($hash->{NAME}, "async_delay", 0);
  623. InternalTimer(gettimeofday()+$async_delay,"LightScene_asyncQueue",$hash,0) if( @{$hash->{".asyncQueue"}} );
  624. }
  625. return undef;
  626. }
  627. sub
  628. LightScene_Get($@)
  629. {
  630. my ($hash, @a) = @_;
  631. my $name = $a[0];
  632. return "$name: get needs at least one parameter" if(@a < 2);
  633. my $cmd= $a[1];
  634. if( $cmd eq "scene" && @a < 3 ) { return "Usage: get scene <scene_name>" };
  635. my $ret = "";
  636. if( $cmd eq "html" ) {
  637. return LightScene_2html($hash);
  638. } elsif( $cmd eq "scenes" ) {
  639. foreach my $scene (sort keys %{ $hash->{SCENES} }) {
  640. $ret .= $scene ."\n";
  641. }
  642. return $ret;
  643. } elsif( $cmd eq "scene" ) {
  644. my $ret = "";
  645. my $scene = $a[2];
  646. if( defined($hash->{SCENES}{$scene}) ) {
  647. foreach my $d (sort keys %{ $hash->{SCENES}{$scene} }) {
  648. next if( !defined($hash->{SCENES}{$scene}{$d}));
  649. my $state = $hash->{SCENES}{$scene}{$d};
  650. $state = $state->{state} if( ref($state) eq 'HASH' );
  651. if( ref($state) eq 'ARRAY' ) {
  652. my $r = "";
  653. foreach my $entry (@{$state}) {
  654. $r .= ',' if( $r );
  655. $r .= $entry;
  656. }
  657. $ret .= $d .": $r\n";
  658. } else {
  659. $ret .= $d .": $state\n";
  660. }
  661. }
  662. } else {
  663. $ret = "no scene <$scene> defined";
  664. }
  665. return $ret;
  666. }
  667. return "Unknown argument $cmd, choose one of html:noArg scenes:noArg scene:".join(",", sort keys %{$hash->{SCENES}});
  668. }
  669. sub
  670. LightScene_updateHelper($$)
  671. {
  672. my ($hash, $attrVal) = @_;
  673. my @devices = sort keys %{ $hash->{CONTENT} };
  674. $hash->{devices} = \@devices;
  675. if( !$attrVal ) {
  676. delete $hash->{switchingOrder};
  677. return;
  678. }
  679. my %switchingOrder = ();
  680. my @parts = split( ' ', $attrVal );
  681. foreach my $part (@parts) {
  682. my ($s,$devices) = split( ':', $part,2 );
  683. my $reverse = 0;
  684. if( $devices && $devices =~ m/^!(.*)/ ) {
  685. $reverse = 1;
  686. $devices = $1;
  687. }
  688. foreach my $scene (keys %{ $hash->{SCENES} }) {
  689. eval { $scene =~ m/$s/ };
  690. if( $@ ) {
  691. my $name = $hash->{NAME};
  692. Log3 $name, 3, $name .": ". $s .": ". $@;
  693. next;
  694. }
  695. next if( $scene !~ m/$s/ );
  696. next if( $switchingOrder{$scene} );
  697. my @devs = split( ',', $devices );
  698. my @devices = ();
  699. @devices = @{$hash->{devices}} if( $reverse );
  700. foreach my $d (@devs) {
  701. foreach my $device (@{$hash->{devices}}) {
  702. next if( !$reverse && grep { $_ eq $device } @devices );
  703. eval { $device =~ m/$d/ };
  704. if( $@ ) {
  705. my $name = $hash->{NAME};
  706. Log3 $name, 3, $name .": ". $d .": ". $@;
  707. next;
  708. }
  709. next if( $device !~ m/$d/ );
  710. @devices = grep { $_ ne $device } @devices if($reverse);
  711. push( @devices, $device );
  712. }
  713. }
  714. foreach my $device (@{$hash->{devices}}) {
  715. next if( grep { $_ eq $device } @devices );
  716. push( @devices, $device );
  717. }
  718. $switchingOrder{$scene} = \@devices;
  719. }
  720. }
  721. $hash->{switchingOrder} = \%switchingOrder;
  722. }
  723. sub
  724. LightScene_Attr($@)
  725. {
  726. my ($cmd, $name, $attrName, $attrVal) = @_;
  727. if( $attrName eq "followDevices" ) {
  728. my $hash = $defs{$name};
  729. if( $cmd eq "set" ) {
  730. $hash->{followDevices} = $attrVal;
  731. } else {
  732. delete $hash->{followDevices};
  733. }
  734. } elsif( $attrName eq "switchingOrder" ) {
  735. my $hash = $defs{$name};
  736. if( $cmd eq "set" ) {
  737. LightScene_updateHelper( $hash, $attrVal );
  738. } else {
  739. delete $hash->{switchingOrder};
  740. }
  741. }
  742. return;
  743. }
  744. sub
  745. LightScene_CGI {
  746. my ($cgi) = @_;
  747. my ($cmd,$c)=FW_digestCgi($cgi);
  748. $scn = $FW_webArgs{scn};
  749. $cmd =~ s/ set / setcmd / if( defined($FW_webArgs{cmd1}) && $FW_webArgs{cmd1} eq 'setcmd' );
  750. # Debug "LS758: cmd: $cmd";
  751. AnalyzeCommand(undef,$cmd);
  752. #redirect to return to detail screen
  753. my $tgt = "?detail=$FW_webArgs{detail}";
  754. $tgt = $FW_ME.$tgt;
  755. $c = $defs{$FW_cname}->{CD};
  756. print $c "HTTP/1.1 302 Found\r\n",
  757. "Content-Length: 0\r\n",
  758. "Location: $tgt\r\n",
  759. "\r\n";
  760. return;
  761. }
  762. sub
  763. LightScene_editTable($) {
  764. my ($hash) = @_;
  765. my $html="\n\n<!--Beginning Edit-Table-->\n";
  766. my $cmd='scn';
  767. #make dropdown
  768. my @tv = (sort keys %{ $hash->{SCENES} });
  769. unshift (@tv,'Choose scene');
  770. my $dd.="<form method=\"get\" action=\"" . $FW_ME . "/LightScene\">\n";
  771. $dd.=FW_select("$hash->{NAME}-$cmd","scn", \@tv, $scn,"dropdown","submit()")."\n";
  772. $dd.=FW_hidden("detail",$hash->{NAME}) . "\n";
  773. $dd.="</form>\n";
  774. # make table
  775. my @devices;
  776. if( $scn && defined($hash->{SCENES}{$scn}) ) {
  777. if( defined($hash->{switchingOrder}) && defined($hash->{switchingOrder}{$scn}) ) {
  778. @devices = @{$hash->{switchingOrder}{$scn}};
  779. } else {
  780. @devices = @{$hash->{devices}};
  781. }
  782. } else {
  783. $scn = '';
  784. }
  785. if ($scn eq "Choose scene" || $scn eq '') {
  786. $html.="<table><tr><td>Edit scene</td><td>$dd</td></tr>";
  787. } else {
  788. $html.="<table><tr><td>Edit scene</td><td>$dd</td></tr></table>";
  789. $html .= '<table class="block wide">';
  790. $html .= '<tr><th>Device</th><th>Command</th></tr>'."\n";
  791. my $row=0;
  792. #table rows
  793. my @cmds = qw(set setcmd);
  794. my $set = "set $hash->{NAME} set $scn";
  795. my $setcmd = '';
  796. foreach my $dev (@devices) {
  797. $row+=1;
  798. $html .= "<tr class=\"".(($row&1)?"odd":"even")."\">";
  799. $html .= "<td>$dev</td>";
  800. my $default = $hash->{SCENES}{$scn}{$dev};
  801. if ($hash->{SCENES}{$scn}{$dev} =~ m/^;/) {
  802. $default =~ s/^;//;
  803. $setcmd='setcmd';
  804. } else {
  805. $setcmd='set';
  806. }
  807. $default = $default->{state} if( ref($default) eq 'HASH' );
  808. $html.="<td><form method=\"get\" action=\"" . $FW_ME . "/LightScene\">\n";
  809. $html.=FW_select('',"cmd1", \@cmds, $setcmd, 'select')."\n";
  810. $html.=FW_textfieldv("val.$dev", 50, 'class',$default)."\n";
  811. $html.=FW_hidden("dev.$dev", $dev) . "\n";
  812. $html.=FW_hidden("cmd.$dev", $set) . "\n";
  813. $html.=FW_submit("lse", 'saveline');
  814. $html.=FW_hidden("scn", $scn) . "\n";
  815. $html.=FW_hidden("detail",$hash->{NAME}) . "\n";
  816. $html .= "</form></td>\n";
  817. }
  818. }
  819. #table end
  820. $html .= "</table><br>\n";
  821. $html .= "<!--End Edit-Table-->\n";
  822. return $html;
  823. }
  824. 1;
  825. =pod
  826. =item helper
  827. =item summary create scenes from multiple fhem devices
  828. =item summary_DE verwaltet Szenen aus mehreren FHEM Ger&auml;ten
  829. =begin html
  830. <a name="LightScene"></a>
  831. <h3>LightScene</h3>
  832. <ul>
  833. Allows to store the state of a group of lights and other devices and recall it later.
  834. Multiple states for one group can be stored.
  835. <br><br>
  836. <a name="LightScene_Define"></a>
  837. <b>Define</b>
  838. <ul>
  839. <code>define &lt;name&gt; LightScene [&lt;dev1&gt;] [&lt;dev2&gt;] [&lt;dev3&gt;] ... </code><br>
  840. <br>
  841. Examples:
  842. <ul>
  843. <code>define light_group LightScene Lampe1 Lampe2 Dimmer1</code><br>
  844. <code>define kino_group LightScene LampeDecke LampeFernseher Fernseher Verstaerker</code><br>
  845. <code>define Wohnzimmer LightScene Leinwand Beamer TV Leselampe Deckenlampe</code><br>
  846. </ul>
  847. </ul><br>
  848. The device detail view will show an html overview of the current state of all included devices and all
  849. configured scenes with the device states for each. The column heading with the device names is clickable
  850. to go to detail view of this device. The first row that displays the current device state is clickable
  851. and should react like a click on the device icon in a room overview would. this can be used to interactively
  852. configure a new scene and save it with the command menu of the detail view. The first column of the table with
  853. the scene names ic clickable to activate the scene.<br><br>
  854. A weblink with a scene overview that can be included in any room or a floorplan can be created with:
  855. <ul><code>define wlScene weblink htmlCode {LightScene_2html("LightSceneName")}</code></ul>
  856. <a name="LightScene_Set"></a>
  857. <b>Set</b>
  858. <ul>
  859. <li>all &lt;command&gt;<br>
  860. execute set &lt;command&gt; for alle devices in this LightScene</li>
  861. <li>save &lt;scene_name&gt;<br>
  862. save current state for alle devices in this LightScene to &lt;scene_name&gt;</li>
  863. <li>scene &lt;scene_name&gt;<br>
  864. shows scene &lt;scene_name&gt; - all devices are switched to the previously saved state</li>
  865. <li>nextScene [nowrap]<br>
  866. activates the next scene in alphabetical order after the current scene or the first if no current scene is set.</li>
  867. <li>previousScene [nowrap]<br>
  868. activates the previous scene in alphabetical order before the current scene or the last if no current scene is set.</li>
  869. <li>set &lt;scene_name&gt; &lt;device&gt; [&lt;cmd&gt;]<br>
  870. set the saved state of &lt;device&gt; in &lt;scene_name&gt; to &lt;cmd&gt;</li>
  871. <li>setcmd &lt;scene_name&gt; &lt;device&gt; [&lt;cmd&gt;]<br>
  872. set command to be executed for &lt;device&gt; in &lt;scene_name&gt; to &lt;cmd&gt;.
  873. &lt;cmd&gt; can be any commandline that fhem understands including multiple commands separated by ;;
  874. <ul>
  875. <li>set kino_group setcmd allOff LampeDecke sleep 30 ;; set LampeDecke off</li>
  876. <li>set light_group setcmd test Lampe1 sleep 10 ;; set Lampe1 on ;; sleep 5 ;; set Lampe1 off</li>
  877. </ul></li>
  878. <li>remove &lt;scene_name&gt;<br>
  879. remove &lt;scene_name&gt; from list of saved scenes</li>
  880. <li>rename &lt;scene_old_name&gt; &lt;scene_new_name&gt;<br>
  881. rename &lt;scene_old_name&gt; to &lt;scene_new_name&gt;</li>
  882. </ul><br>
  883. <a name="LightScene_Get"></a>
  884. <b>Get</b>
  885. <ul>
  886. <li>scenes</li>
  887. <li>scene &lt;scene_name&gt;</li>
  888. </ul><br>
  889. <a name="LightScene_Attr"></a>
  890. <b>Attributes</b>
  891. <ul>
  892. <a name="async_delay"></a>
  893. <li>async_delay<br>
  894. If this attribute is defined, unfiltered set commands will not be
  895. executed in the clients immediately. Instead, they are added to a queue
  896. to be executed later. The set command returns immediately, whereas the
  897. clients will be set timer-driven, one at a time. The delay between two
  898. timercalls is given by the value of async_delay (in seconds) and may be
  899. 0 for fastest possible execution.
  900. </li>
  901. <li>lightSceneParamsToSave<br>
  902. this attribute can be set on the devices to be included in a scene. it is set to a comma separated list of readings
  903. that will be saved. multiple readings separated by : are collated in to a single set command (this has to be supported
  904. by the device). each reading can have a perl expression appended with '@' that will be used to alter the $value used for
  905. the set command. this can for example be used to strip a trailing % from a dimmer state. this perl expression must not contain
  906. spaces,colons or commas.<br>
  907. in addition to reading names the list can also contain expressions of the form <code>abc -> xyz</code>
  908. or <code>get cba -> set uvw</code> to map reading abc to set xyz or get cba to set uvw. the list can be given as a
  909. string or as a perl expression enclosed in {} that returns this string.<br>
  910. <code>attr myReceiver lightSceneParamsToSave volume,channel</code><br>
  911. <code>attr myHueDevice lightSceneParamsToSave {(Value($DEVICE) eq "off")?"state":"bri : xy"}</code></li>
  912. <code>attr myDimmer lightSceneParamsToSave state@{if($value=~m/(\d+)/){$1}else{$value}}</code><br>
  913. <li>lightSceneRestoreOnlyIfChanged<br>
  914. this attribute can be set on the lightscene and/or on the individual devices included in a scene.
  915. the device settings have precedence over the scene setting.<br>
  916. 1 -> for each device do nothing if current device state is the same as the saved state<br>
  917. 0 -> always set the state even if the current state is the same as the saved state. this is the default</li>
  918. <li>followDevices<br>
  919. the LightScene tries to follow the switching state of the devices set its state to the name of the scene that matches.<br>
  920. 1 -> if no match is found state will be unchanged and a nomatch event will be triggered.<br>
  921. 2 -> if no match is found state will be set to unknown. depending on the scene and devices state can toggle multiple
  922. times. use a watchdog if you want to handle this.</li>
  923. <li>showDeviceCurrentState<br>
  924. show the current state of member devices in weblink</li>
  925. <li>switchingOrder<br>
  926. space separated list of &lt;scene&gt;:&lt;deviceList&gt; items that will give a per scene order
  927. in which the devices should be switched.<br>
  928. the devices from &lt;deviceList&gt; will come before all other devices of this LightScene;
  929. if the first character of the &lt;deviceList&gt; ist a ! the devices from the list will come after
  930. all other devices from this lightScene.<br>
  931. &lt;scene&gt; and each element of &lt;deviceList&gt; are treated as a regex.<br>
  932. Example: To switch a master power outlet before every other device at power on and after every device on power off:<br>
  933. <code>define media LightScene TV,DVD,Amplifier,masterPower<br>
  934. attr media switchingOrder .*On:masterPower,.* allOff:!.*,masterPower</code>
  935. </li>
  936. <li>traversalOrder<br>
  937. comma separated list of scene names that should be traversed by the prevoiusScene and nextScene commands.<br>
  938. default not set -> all scenes will be traversed in alphabetical order
  939. </li>
  940. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  941. </ul><br>
  942. </ul>
  943. =end html
  944. =cut