/FHEM/99_RuleDesigner.pm

https://github.com/HTWDD-SN/fhem_rule_designer · Perl · 583 lines · 378 code · 116 blank · 89 comment · 26 complexity · 885f812fe1bc4fa2d46f46a24921e28b MD5 · raw file

  1. ##############################################################################
  2. # The RuleDesigner Modul
  3. # - receives rule description,
  4. # - manages this rules within in a local database,
  5. # - and compiles the received rule description into FHEM rules.
  6. #
  7. # The source code is segmented into the following secitons
  8. # - FHEM MODULE FUNCTIONS
  9. # - CGI
  10. # - RULE REPOSITORY
  11. # - RULE COMPILER
  12. #
  13. # Documentation: http://www2.htw-dresden.de/~wiki_sn/index.php5/FHEM/Regelerstellung#FHEM_Modul
  14. #
  15. # __ __ ________ __ __ _______ _______
  16. # | \ | \| \| \ _ | \ | \ | \
  17. # | $$ | $$ \$$$$$$$$| $$ / \ | $$ | $$$$$$$\| $$$$$$$\
  18. # | $$__| $$ | $$ | $$/ $\| $$ | $$ | $$| $$ | $$
  19. # | $$ $$ | $$ | $$ $$$\ $$ | $$ | $$| $$ | $$
  20. # | $$$$$$$$ | $$ | $$ $$\$$\$$ | $$ | $$| $$ | $$
  21. # | $$ | $$ | $$ | $$$$ \$$$$ | $$__/ $$| $$__/ $$
  22. # | $$ | $$ | $$ | $$$ \$$$ | $$ $$| $$ $$
  23. # \$$ \$$ \$$ \$$ \$$ \$$$$$$$ \$$$$$$$
  24. #
  25. # HTW Dresden
  26. # Forschungsseminar Sensornetze 2014-2015
  27. # Felix Pistorius
  28. #
  29. ##############################################################################
  30. package main;
  31. use strict;
  32. use warnings;
  33. use DBI;
  34. use Switch 'Perl5', 'Perl6';
  35. use Net::Telnet;
  36. use JSON; # search.cpan.org/~makamaka/JSON-2.90/lib/JSON.pm
  37. use Data::Dumper;
  38. use vars qw(%data);
  39. # Variables
  40. my $MODULE_NAME = "wizard";#"ruledesigner";
  41. my $url = "/". $MODULE_NAME;
  42. my $contenttype = "application/json; charset=UTF-8";
  43. my $fhemhost = 'localhost';
  44. my $fhemport = '8083';
  45. ################################################################################
  46. # FHEM MODULE FUNCTIONS
  47. ################################################################################
  48. sub
  49. RuleDesigner_Initialize($)
  50. {
  51. my ($hash) = @_;
  52. $hash->{DefFn} = "RD_define";
  53. $data{FWEXT}{$url}{LINK} = $MODULE_NAME."/index.html";
  54. $data{FWEXT}{$url}{NAME} = $MODULE_NAME;
  55. $data{FWEXT}{$url}{FUNC} = "RD_CGI";
  56. RD_Repo_Init();
  57. }
  58. ################################################################################
  59. # CGI
  60. ################################################################################
  61. # handles all cgi requests for the rule designer modul
  62. sub
  63. RD_CGI()
  64. {
  65. my ($htmlargs) = @_;
  66. my ($json_ret) = "";
  67. my $query = "";
  68. my %rule;
  69. my $regexop = undef;
  70. if($htmlargs =~ /$MODULE_NAME\?/) {
  71. # URL contains HTTP GET parameters
  72. $regexop = '[^\?]+\?(.*)';
  73. } elsif($htmlargs =~ /$MODULE_NAME\&/) {
  74. # URL contains HTTP POST parameters
  75. $regexop = '[^\&]+\&(.*)';
  76. }
  77. if ($regexop ne undef and $htmlargs =~ m/$regexop/) {
  78. foreach my $e (split(/&/, $1)) {
  79. my ($k, $a) = split(/=/, $e);
  80. given ($k) {
  81. when 'q' { $query = $a; }
  82. when 'json' { $rule{'json'} = urlDecode($a); }
  83. when 'id' { $rule{'id'} = urlDecode($a); }
  84. }
  85. }
  86. given ($query) {
  87. when 'devicelist' { $json_ret = RD_CGI_Devicelist(); }
  88. when 'rulelist' { $json_ret = RD_Repo_Rulelist(); }
  89. when 'define' { $json_ret = RD_CGI_Definerule(%rule); }
  90. when 'delete' { RD_CGI_DeleteRule(%rule) }
  91. when 'deactivate' { RD_CGI_DeactivateRule(%rule) }
  92. when 'activate' { RD_CGI_ActivateRule(%rule) }
  93. default { $json_ret = '{"TYPE": "Error", "Message": "Unsupported query: '.$query.'"}'; }
  94. }
  95. } else {
  96. $json_ret = '{"TYPE": "Error", "Message": "Query was empty."}';
  97. }
  98. return ($contenttype, $json_ret);
  99. }
  100. # handles rule deletion requests
  101. sub RD_CGI_DeleteRule
  102. {
  103. my %attr = @_;
  104. RD_Repo_DeleteRule($attr{'id'});
  105. RD_Telnet("delete ".$attr{'id'});
  106. }
  107. # handles rule deactivation requests
  108. sub RD_CGI_DeactivateRule
  109. {
  110. my %attr = @_;
  111. # state = 0 -> rule deactivated
  112. my $state = 0;
  113. RD_Repo_SetState($attr{'id'}, $state);
  114. RD_Telnet("delete ".$attr{'id'});
  115. }
  116. # handels rule activation requests
  117. sub RD_CGI_ActivateRule
  118. {
  119. my %attr = @_;
  120. # state = 1 -> rule activated
  121. my $state = 1;
  122. RD_Repo_SetState($attr{'id'}, $state);
  123. my $json = decode_json( RD_Repo_GetRule($attr{'id'}) );
  124. my %rule = %{ parseRule($json) };
  125. RD_Telnet($rule{'RULE'});
  126. }
  127. # returns a URL for a json list with all FHEM devices as json object.
  128. sub
  129. RD_CGI_Devicelist()
  130. {
  131. my $json_ret = '{"TYPE": "URL"';
  132. $json_ret .= ', "msg":"'.$fhemhost.':'.$fhemport.'/fhem?cmd=jsonlist2&XHR=1"';
  133. $json_ret .= '}';
  134. return $json_ret;
  135. }
  136. # handles a transmitted rule description:
  137. # - call the rule parser
  138. # - insert the rule into the rule repository
  139. # - sned the generated rule to FHEM
  140. sub
  141. RD_CGI_Definerule
  142. {
  143. my %attr = @_;
  144. if ($attr{'json'}) {
  145. my $json_ref = decode_json($attr{'json'});
  146. my @json = @{$json_ref};
  147. for (values @json) {
  148. my %rule = %{ parseRule($_) };
  149. $rule{'JSON'} = encode_json($_);
  150. $rule{'STATE'} = 1;
  151. Log3($MODULE_NAME, 3, "generated rule: $rule{'RULE'}");
  152. RD_Repo_Insert(%rule);
  153. RD_Telnet($rule{'RULE'});
  154. }
  155. return '{"TYPE":"Info", "Message":"Success."}';
  156. }
  157. return '{"TYPE":"Error", "Message":"No rule."}';
  158. }
  159. ################################################################################
  160. # RULE REPOSITORY
  161. ################################################################################
  162. # connect to the database
  163. sub DBH
  164. {
  165. my $dns = "DBI:SQLite:dbname=./rulerepository.sqlite.db";
  166. my $dbh = DBI->connect($dns, "", "",
  167. { RaiseError => 1,
  168. PrintError => 1,
  169. AutoCommit => 1
  170. }) or Log3 $MODULE_NAME, 1, "RuleDesigner can't connect to the database." and return;
  171. return $dbh;
  172. }
  173. # initialised the database with all tables
  174. sub RD_Repo_Init()
  175. {
  176. my $dbh = DBH();
  177. my $create_table_rule = 'CREATE TABLE IF NOT EXISTS rule
  178. (ID INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(20), json VARCHAR(1000), description VARCHAR(500), state INTEGER)';
  179. $dbh->do($create_table_rule) or Log3($MODULE_NAME, 1, "CAN'T CREATE TABLE 'RULE': $dbh->errstr()");
  180. $dbh->disconnect();
  181. }
  182. # insert a rule into the database
  183. sub RD_Repo_Insert
  184. {
  185. my %rule = @_;
  186. my $dbh = DBH();
  187. my $sth = $dbh->prepare("INSERT INTO rule (name, json, description, state) VALUES (?,?,?,?)");
  188. $sth->execute($rule{'ID'}, $rule{'JSON'}, $rule{'PARAMS'}{'descr'},$rule{'STATE'});
  189. $dbh->disconnect();
  190. }
  191. # proof if rule id already exists
  192. sub RD_Repo_IfExists
  193. {
  194. my ($name) = @_;
  195. my $dbh = DBH();
  196. my $cnt = $dbh->selectrow_array("SELECT count(*) FROM rule WHERE name LIKE \"$name\"")
  197. or Log3($MODULE_NAME, 1, "RD_Repo_IfExists: $dbh->errstr()");
  198. $dbh->disconnect();
  199. return $cnt;
  200. }
  201. # generates a json object with all managed rule ID's
  202. sub RD_Repo_Rulelist
  203. {
  204. my $dbh = DBH();
  205. my @values;
  206. my $r;
  207. my $sth = $dbh->prepare("SELECT name, description, state FROM rule");
  208. if ($sth->execute()) {
  209. while(my @row = $sth->fetchrow_array()) {
  210. push(@values, '{"NAME":"'.$row[0].'","DESCRIPTION":"'.$row[1].'","STATE":"'.$row[2].'"}');
  211. }
  212. $r = "[". join(',', @values) ."]";
  213. } else {
  214. Log3($MODULE_NAME, 1, "RD_Repo_Rulelist: $dbh->errstr()");
  215. $r = '{"TYPE":"Error", "Message":"Database Error (Rulelist)"}';
  216. }
  217. $dbh->disconnect();
  218. return $r;
  219. }
  220. # update the state of a rule
  221. # possible states:
  222. # -1 : not define
  223. # 0 : deactivated
  224. # 1 : active
  225. sub RD_Repo_SetState
  226. {
  227. my ($id, $state) = @_;
  228. my $dbh = DBH();
  229. my $sth = $dbh->prepare("UPDATE rule SET state = ? WHERE name is ?");
  230. $sth->execute($state, $id) or Log3($MODULE_NAME, 1, "RD_Repo_SetState: $dbh->errstr()");
  231. $dbh->disconnect();
  232. }
  233. # delete a record of a rule by the rule name
  234. sub RD_Repo_DeleteRule
  235. {
  236. my ($id) = @_;
  237. my $dbh = DBH();
  238. my $sth = $dbh->prepare("DELETE FROM rule WHERE name IS ?");
  239. $sth->execute($id) or Log3($MODULE_NAME, 1, "RD_Repo_DeleteRule: $dbh->errstr()");
  240. $dbh->disconnect();
  241. }
  242. # get the json representation of a rule
  243. sub RD_Repo_GetRule
  244. {
  245. my ($id) = @_;
  246. my $dbh = DBH();
  247. my $r = "";
  248. my $sth = $dbh->prepare("SELECT json FROM rule WHERE name IS ?");
  249. if ($sth->execute($id)) {
  250. my @row = $sth->fetchrow_array();
  251. $r = $row[0];
  252. } else {
  253. Log3($MODULE_NAME, 1, "RD_Repo_DeleteRule: $dbh->errstr()");
  254. }
  255. $dbh->disconnect();
  256. return $r;
  257. }
  258. ################################################################################
  259. # RULE COMPILER
  260. ################################################################################
  261. # start function to parse the rule description and to build the FHEM rule
  262. # returns a hash value with all parsed elements (id, conditions, actions)
  263. # and the fhem rule itself.
  264. sub parseRule
  265. {
  266. my ($ref) = @_;
  267. my %rule = %$ref;
  268. my %res;
  269. for (keys %rule) {
  270. given ($_) {
  271. when 'ID' {
  272. $res{'ID'} = $rule{$_};
  273. }
  274. when 'PARAMS' {
  275. #$res{'PARAMS'} = parseParams($rule{$_});
  276. $res{'PARAMS'} = $rule{$_};
  277. }
  278. when 'COND' {
  279. $res{'COND'} = parseCond($rule{$_});
  280. }
  281. when 'ACTION' {
  282. $res{'ACTION'} = parseActorList($rule{$_});
  283. }
  284. when 'VDEV' {
  285. # TODO:
  286. }
  287. }
  288. }
  289. $res{'RULE'} = "define ".$res{"ID"}.$res{'COND'}{'RULE'}.$res{'ACTION'}."}";
  290. return \%res;
  291. }
  292. # Parse rule:
  293. # <conditions> -> <condition> | <gather>
  294. sub parseCond
  295. {
  296. my ($ref) = @_;
  297. my %cond = %$ref;
  298. my %res;
  299. if($cond{'SENSOR'} && $cond{'PARAMS'}) {
  300. %res = %{ parseCondition($ref) };
  301. $res{'RULE'} = ' notify ('.$res{'name'}.') { '. $res{'dec'} .' if ('.$res{'params'}.')';
  302. return \%res;
  303. }
  304. %res = %{parseGather($ref)};
  305. $res{'RULE'} = ' notify '.$res{'names'}.' { '. $res{'decs'} .' if ('.$res{'params'}.')';
  306. return \%res;
  307. }
  308. # Parese rule:
  309. # <conditions> -> <condition> | <gather>
  310. sub parseGatherCond
  311. {
  312. my ($ref) = @_;
  313. my %cond = %{$ref};
  314. my %res;
  315. if($cond{'SENSOR'} && $cond{'PARAMS'}) {
  316. %res = %{ parseCondition($ref) };
  317. } else {
  318. %res = parseGather($ref);
  319. return "\n\n". Dumper(%res) ."\n\n";
  320. }
  321. return \%res;
  322. }
  323. # Parse rule:
  324. # <condition> -> "SENSOR" : ID,
  325. # "REF_PARAMS" : {<ref_param>(, <ref_param>)?}
  326. sub parseCondition
  327. {
  328. my ($cond) = @_;
  329. my %condition;
  330. while ( my($k, $v) = each $cond) {
  331. given ($k) {
  332. when 'SENSOR' {
  333. $condition{'name'} = $v;
  334. $condition{'dec'} = 'my $'.$v.'_val = $value{"'.$v.'"};;';
  335. $condition{'var'} = '$'.$v.'_val';
  336. }
  337. when 'PARAMS' {
  338. $condition{'params'} = "";
  339. while ( my($k2, $v2) = each $v) {
  340. $condition{'params'} .= parseCondParams($v2);
  341. }
  342. }
  343. }
  344. }
  345. $condition{'params'} =~ s/%/$condition{'var'}/ge;
  346. return \%condition;
  347. }
  348. # Parse rule:
  349. # <LOG_GATHER> : [<conditions>(, <conditions>)?]
  350. sub parseGather
  351. {
  352. my ($gather) = @_;
  353. my $loggather = undef;
  354. my @conditions5;
  355. #return "\n\n".Dumper($gather)."\n\n";
  356. my @k = keys %{$gather};
  357. given($k[0]) {
  358. when 'AND' { $loggather = '&&'; }
  359. when 'OR' { $loggather = '||'; }
  360. }
  361. for(values $gather) {
  362. for (values $_) {
  363. push(@conditions5, parseGatherCond($_));
  364. }
  365. }
  366. my @names;
  367. my @decs;
  368. my @params;
  369. for (@conditions5) {
  370. my %c = %{$_};
  371. push(@names, $c{'name'});
  372. push(@decs, $c{'dec'});
  373. push(@params, $c{'params'});
  374. }
  375. my %res;
  376. $res{'names'} = "(".createSeperatedString("|", @names).")";
  377. $res{'decs'} = createSeperatedString(" ", @decs);
  378. $res{'params'} = createSeperatedString(' '.$loggather.' ', @params)." ";
  379. return \%res;
  380. }
  381. # Parse rule:
  382. # <ref_param> : [<log_func>, <value>]
  383. sub parseCondParams
  384. {
  385. my ($params) = @_;
  386. my $ret = "";
  387. while( my($k,$v) = each $params) {
  388. given($k) {
  389. when 0 {
  390. my $op = undef;
  391. given $v {
  392. when '==' {$op = 'eq'; }
  393. when '!=' {$op = 'ne'; }
  394. }
  395. $ret .= '% '. $op;
  396. }
  397. when 1 {
  398. $ret .= ' "'. $v .'"';
  399. }
  400. }
  401. }
  402. return $ret;
  403. }
  404. # Parse rule
  405. # <actors> -> <actor> (,<actor>)?
  406. sub parseActorList
  407. {
  408. my ($actions) = @_;
  409. my $act = "";
  410. while ( my($k, $v) = each $actions) {
  411. $act .= parseActor($v);
  412. }
  413. return "{ $act }";
  414. }
  415. # Parse rule of <actor>
  416. sub parseActor
  417. {
  418. my ($action) = @_;
  419. my $actor = undef;
  420. my $state = undef;
  421. while ( my($k, $v) = each $action) {
  422. given ($k) {
  423. when 'ACTOR' {
  424. $actor = $v;
  425. }
  426. when 'PARAMS' {
  427. $state = $v;
  428. $state =~ s/%/\\045/g;
  429. }
  430. }
  431. }
  432. my $rule = '{ fhem "set ';
  433. $rule .= $actor ." ". $state;
  434. $rule .= '" }';
  435. return $rule;
  436. }
  437. # Returns a seperated string by a given seperator and an array.
  438. sub createSeperatedString
  439. {
  440. my ($seperator, @list) = @_;
  441. my $lastIdx = $#list;
  442. my $res = "";
  443. if($lastIdx < 0) {
  444. return undef;
  445. }
  446. if($lastIdx == 0) {
  447. return $list[0];
  448. }
  449. for(my $i=0; $i<$lastIdx; $i++) {
  450. $res .= $list[$i].$seperator;
  451. }
  452. $res .= $list[$lastIdx];
  453. return $res;
  454. }
  455. # starts a telnet session to send the generated FHEM rule
  456. sub RD_Telnet
  457. {
  458. my ($rule) = @_;
  459. my $telnet = new Net::Telnet(Timeout=>10,
  460. Errmode=>'die',
  461. Host => 'localhost',
  462. Port => '7072');
  463. print $rule;
  464. $telnet->open();
  465. $telnet->print($rule);
  466. $telnet->close();
  467. }