10_MQTT2_DEVICE.pm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. ##############################################
  2. # $Id: 10_MQTT2_DEVICE.pm 17487 2018-10-08 07:38:31Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use SetExtensions;
  7. sub
  8. MQTT2_DEVICE_Initialize($)
  9. {
  10. my ($hash) = @_;
  11. $hash->{Match} = ".*";
  12. $hash->{SetFn} = "MQTT2_DEVICE_Set";
  13. $hash->{GetFn} = "MQTT2_DEVICE_Get";
  14. $hash->{DefFn} = "MQTT2_DEVICE_Define";
  15. $hash->{UndefFn} = "MQTT2_DEVICE_Undef";
  16. $hash->{AttrFn} = "MQTT2_DEVICE_Attr";
  17. $hash->{ParseFn} = "MQTT2_DEVICE_Parse";
  18. $hash->{RenameFn} = "MQTT2_DEVICE_Rename";
  19. no warnings 'qw';
  20. my @attrList = qw(
  21. IODev
  22. devicetopic
  23. disable:0,1
  24. disabledForIntervals
  25. model
  26. readingList:textField-long
  27. setList:textField-long
  28. getList:textField-long
  29. );
  30. use warnings 'qw';
  31. $hash->{AttrList} = join(" ", @attrList)." ".$readingFnAttributes;
  32. my %h = ( re=>{}, cid=>{});
  33. $modules{MQTT2_DEVICE}{defptr} = \%h;
  34. }
  35. #############################
  36. sub
  37. MQTT2_DEVICE_Define($$)
  38. {
  39. my ($hash, $def) = @_;
  40. my @a = split("[ \t][ \t]*", $def);
  41. my $name = shift @a;
  42. my $type = shift @a; # always MQTT2_DEVICE
  43. $hash->{CID} = shift(@a) if(@a);
  44. return "wrong syntax for $name: define <name> MQTT2_DEVICE [clientid]"
  45. if(int(@a));
  46. $hash->{DEVICETOPIC} = $name;
  47. $modules{MQTT2_DEVICE}{defptr}{cid}{$hash->{CID}} = $hash if($hash->{CID});
  48. AssignIoPort($hash);
  49. return undef;
  50. }
  51. #############################
  52. sub
  53. MQTT2_DEVICE_Parse($$)
  54. {
  55. my ($iodev, $msg) = @_;
  56. my $ioname = $iodev->{NAME};
  57. my %fnd;
  58. sub
  59. checkForGet($$$)
  60. {
  61. my ($hash, $key, $value) = @_;
  62. if($hash->{asyncGet} && $key eq $hash->{asyncGet}{reading}) {
  63. RemoveInternalTimer($hash->{asyncGet});
  64. asyncOutput($hash->{asyncGet}{CL}, "$key $value");
  65. delete($hash->{asyncGet});
  66. }
  67. }
  68. my $autocreate;
  69. if($msg =~ m/^autocreate:(.*)/) {
  70. $msg = $1;
  71. $autocreate = 1;
  72. }
  73. my ($cid, $topic, $value) = split(":", $msg, 3);
  74. my $dp = $modules{MQTT2_DEVICE}{defptr}{re};
  75. foreach my $re (keys %{$dp}) {
  76. my $reAll = $re;
  77. $reAll =~ s/\$DEVICETOPIC/\.\*/g;
  78. next if(!("$topic:$value" =~ m/^$reAll$/s ||
  79. "$cid:$topic:$value" =~ m/^$reAll$/s));
  80. foreach my $dev (keys %{$dp->{$re}}) {
  81. next if(IsDisabled($dev));
  82. my $hash = $defs{$dev};
  83. my $reRepl = $re;
  84. $reRepl =~ s/\$DEVICETOPIC/$hash->{DEVICETOPIC}/g;
  85. next if(!("$topic:$value" =~ m/^$reRepl$/s ||
  86. "$cid:$topic:$value" =~ m/^$reRepl$/s));
  87. my @retData;
  88. my $code = $dp->{$re}{$dev};
  89. Log3 $dev, 4, "MQTT2_DEVICE_Parse: $dev $topic => $code";
  90. if($code =~ m/^{.*}$/s) {
  91. $code = EvalSpecials($code, ("%TOPIC"=>$topic, "%EVENT"=>$value,
  92. "%DEVICETOPIC"=>$hash->{DEVICETOPIC}, "%NAME"=>$hash->{NAME}));
  93. my $ret = AnalyzePerlCommand(undef, $code);
  94. if($ret && ref $ret eq "HASH") {
  95. readingsBeginUpdate($hash);
  96. foreach my $k (keys %{$ret}) {
  97. readingsBulkUpdate($hash, $k, $ret->{$k});
  98. push(@retData, "$k $ret->{$k}");
  99. checkForGet($hash, $k, $ret->{$k});
  100. }
  101. readingsEndUpdate($hash, 1);
  102. }
  103. } else {
  104. readingsSingleUpdate($hash, $code, $value, 1);
  105. push(@retData, "$code $value");
  106. checkForGet($hash, $code, $value);
  107. }
  108. $fnd{$dev} = 1;
  109. }
  110. }
  111. # autocreate and expand readingList
  112. if($autocreate && !%fnd) {
  113. return "" if($cid && $cid =~ m/mosqpub.*/);
  114. my $cidHash = $modules{MQTT2_DEVICE}{defptr}{cid}{$cid};
  115. my $nn = $cidHash ? $cidHash->{NAME} : "MQTT2_$cid";
  116. PrioQueue_add(sub{
  117. return if(!$defs{$nn});
  118. my $add;
  119. if($value =~ m/^{.*}$/) {
  120. my $ret = json2nameValue($value);
  121. $add = "{ json2nameValue(\$EVENT) }" if(keys %{$ret});
  122. }
  123. if(!$add) {
  124. $topic =~ m,.*/([^/]+),;
  125. $add = ($1 ? $1 : $topic);
  126. }
  127. my $rl = AttrVal($nn, "readingList", "");
  128. $rl .= "\n" if($rl);
  129. CommandAttr(undef, "$nn readingList $rl$cid:$topic:.* $add");
  130. MQTT2_DEVICE_Parse($iodev, $msg);
  131. }, undef);
  132. return "UNDEFINED $nn MQTT2_DEVICE $cid" if(!$cidHash);
  133. return "";
  134. }
  135. return keys %fnd;
  136. }
  137. # compatibility: the first version was implemented as MQTT2_JSON and published.
  138. sub
  139. MQTT2_JSON($;$)
  140. {
  141. return json2nameValue($_[0], $_[1]);
  142. }
  143. sub
  144. MQTT2_getCmdHash($)
  145. {
  146. my ($list) = @_;
  147. my (%h, @cmd);
  148. map {
  149. my ($k,$v) = split(" ",$_,2);
  150. push @cmd, $k;
  151. $k =~ s/:.*//; # potential arguments
  152. $h{$k} = $v;
  153. }
  154. grep /./,
  155. split("\n", $list);
  156. return (\%h, join(" ",@cmd));
  157. }
  158. #############################
  159. # replace {} and $EVENT. Used both in set and get
  160. sub
  161. MQTT2_buildCmd($$$)
  162. {
  163. my ($hash, $a, $cmd) = @_;
  164. shift @{$a};
  165. if($cmd =~ m/^{.*}$/) {
  166. $cmd = EvalSpecials($cmd, ("%EVENT"=>join(" ",@{$a}), "%NAME"=>$hash->{NAME}));
  167. $cmd = AnalyzeCommandChain($hash->{CL}, $cmd);
  168. return if(!$cmd);
  169. } else {
  170. if($cmd =~ m/\$EV/) { # replace EVENT & $EVTPART
  171. my $event = join(" ",@{$a});
  172. $cmd =~ s/\$EVENT/$event/g;
  173. for(my $i=0; $i<@{$a}; $i++) {
  174. my $n = "\\\$EVTPART$i";
  175. $cmd =~ s/$n/$a->[$i]/ge;
  176. }
  177. } else {
  178. shift @{$a};
  179. $cmd .= " ".join(" ",@{$a}) if(@{$a});
  180. }
  181. }
  182. $cmd =~ s/\$DEVICETOPIC/$hash->{DEVICETOPIC}/g;
  183. return $cmd;
  184. }
  185. #############################
  186. sub
  187. MQTT2_DEVICE_Get($@)
  188. {
  189. my ($hash, @a) = @_;
  190. return "Not enough arguments for get" if(!defined($a[1]));
  191. my ($gets,$cmdList) = MQTT2_getCmdHash(AttrVal($hash->{NAME}, "getList", ""));
  192. return "Unknown argument $a[1], choose one of $cmdList" if(!$gets->{$a[1]});
  193. return undef if(IsDisabled($hash->{NAME}));
  194. my ($getReading, $cmd) = split(" ",$gets->{$a[1]},2);
  195. if($hash->{CL}) {
  196. my $tHash = { hash=>$hash, CL=>$hash->{CL}, reading=>$getReading };
  197. $hash->{asyncGet} = $tHash;
  198. InternalTimer(gettimeofday()+4, sub {
  199. asyncOutput($tHash->{CL}, "Timeout reading answer for $cmd");
  200. delete($hash->{asyncGet});
  201. }, $tHash, 0);
  202. }
  203. $cmd = MQTT2_buildCmd($hash, \@a, $cmd);
  204. return if(!$cmd);
  205. IOWrite($hash, split(" ",$cmd,2));
  206. return undef;
  207. }
  208. #############################
  209. sub
  210. MQTT2_DEVICE_Set($@)
  211. {
  212. my ($hash, @a) = @_;
  213. return "Not enough arguments for set" if(!defined($a[1]));
  214. my ($sets,$cmdList) = MQTT2_getCmdHash(AttrVal($hash->{NAME}, "setList", ""));
  215. my $cmdName = $a[1];
  216. my $cmd = $sets->{$cmdName};
  217. return SetExtensions($hash, $cmdList, @a) if(!$cmd);
  218. return undef if(IsDisabled($hash->{NAME}));
  219. $cmd = MQTT2_buildCmd($hash, \@a, $cmd);
  220. return if(!$cmd);
  221. IOWrite($hash, split(" ",$cmd,2));
  222. readingsSingleUpdate($hash, "state", $cmdName, 1);
  223. return undef;
  224. }
  225. sub
  226. MQTT2_DEVICE_Attr($$)
  227. {
  228. my ($type, $dev, $attrName, $param) = @_;
  229. my $hash = $defs{$dev};
  230. if($attrName eq "devicetopic") {
  231. $hash->{DEVICETOPIC} = ($type eq "del" ? $hash->{NAME} : $param);
  232. return undef;
  233. }
  234. if($attrName =~ m/(.*)List/) {
  235. my $atype = $1;
  236. if($type eq "del") {
  237. MQTT2_DEVICE_delReading($dev) if($atype eq "reading");
  238. return undef;
  239. }
  240. return "$dev attr $attrName: more parameters needed" if(!$param); #90145
  241. foreach my $el (split("\n", $param)) {
  242. my ($par1, $par2) = split(" ", $el, 2);
  243. next if(!$par1);
  244. (undef, $par2) = split(" ", $par2, 2) if($type eq "get");
  245. return "$dev attr $attrName: more parameters needed" if(!$par2);
  246. if($atype eq "reading") {
  247. if($par2 =~ m/^{.*}$/) {
  248. my $ret = perlSyntaxCheck($par2,
  249. ("%TOPIC"=>1, "%EVENT"=>"0 1 2 3 4 5 6 7 8 9",
  250. "%NAME"=>$dev, "%DEVICETOPIC"=>$hash->{DEVICETOPIC}));
  251. return $ret if($ret);
  252. } else {
  253. return "unsupported character in readingname $par2"
  254. if(!goodReadingName($par2));
  255. }
  256. } else {
  257. my $ret = perlSyntaxCheck($par2, ("%EVENT"=>"0 1 2 3 4 5 6 7 8 9"));
  258. return $ret if($ret);
  259. }
  260. }
  261. MQTT2_DEVICE_addReading($dev, $param) if($atype eq "reading");
  262. }
  263. return undef;
  264. }
  265. sub
  266. MQTT2_DEVICE_delReading($)
  267. {
  268. my ($name) = @_;
  269. my $dp = $modules{MQTT2_DEVICE}{defptr}{re};
  270. foreach my $re (keys %{$dp}) {
  271. if($dp->{$re}{$name}) {
  272. delete($dp->{$re}{$name});
  273. delete($dp->{$re}) if(!int(keys %{$dp->{$re}}));
  274. }
  275. }
  276. }
  277. sub
  278. MQTT2_DEVICE_addReading($$)
  279. {
  280. my ($name, $param) = @_;
  281. foreach my $line (split("\n", $param)) {
  282. my ($re,$code) = split(" ", $line,2);
  283. $modules{MQTT2_DEVICE}{defptr}{re}{$re}{$name} = $code if($re && $code);
  284. }
  285. }
  286. #####################################
  287. sub
  288. MQTT2_DEVICE_Rename($$)
  289. {
  290. my ($new, $old) = @_;
  291. MQTT2_DEVICE_delReading($old);
  292. MQTT2_DEVICE_addReading($new, AttrVal($new, "readingList", ""));
  293. return undef;
  294. }
  295. #####################################
  296. sub
  297. MQTT2_DEVICE_Undef($$)
  298. {
  299. my ($hash, $arg) = @_;
  300. MQTT2_DEVICE_delReading($arg);
  301. delete $modules{MQTT2_DEVICE}{defptr}{cid}{$hash->{CID}} if($hash->{CID});
  302. return undef;
  303. }
  304. 1;
  305. =pod
  306. =item summary devices communicating via the MQTT2_SERVER
  307. =item summary_DE &uuml;ber den MQTT2_SERVER kommunizierende Ger&auml;te
  308. =begin html
  309. <a name="MQTT2_DEVICE"></a>
  310. <h3>MQTT2_DEVICE</h3>
  311. <ul>
  312. MQTT2_DEVICE is used to represent single devices connected to the
  313. MQTT2_SERVER. MQTT2_SERVER and MQTT2_DEVICE is intended to simplify
  314. connecting MQTT devices to FHEM.
  315. <br> <br>
  316. <a name="MQTT2_DEVICEdefine"></a>
  317. <b>Define</b>
  318. <ul>
  319. <code>define &lt;name&gt; MQTT2_DEVICE</code>
  320. <br><br>
  321. To enable a meaningful function you will need to set at least one of the
  322. readingList, setList or getList attributes below.<br>
  323. </ul>
  324. <br>
  325. <a name="MQTT2_DEVICEset"></a>
  326. <b>Set</b>
  327. <ul>
  328. see the setList attribute documentation below.
  329. </ul>
  330. <br>
  331. <a name="MQTT2_DEVICEget"></a>
  332. <b>Get</b>
  333. <ul>
  334. see the getList attribute documentation below.
  335. </ul>
  336. <br>
  337. <a name="MQTT2_DEVICEattr"></a>
  338. <b>Attributes</b>
  339. <ul>
  340. <a name="devicetopic"></a>
  341. <li>devicetopic value<br>
  342. replace $DEVICETOPIC in the topic part of readingList, setList and
  343. getList with value. if not set, $DEVICETOPIC will be replaced with the
  344. name of the device.
  345. </li><br>
  346. <li><a href="#disable">disable</a><br>
  347. <a href="#disabledForIntervals">disabledForIntervals</a></li><br>
  348. <a name="readingList"></a>
  349. <li>readingList &lt;regexp&gt; [readingName|perl-Expression] ...
  350. <br>
  351. If the regexp matches topic:message or cid:topic:message either set
  352. readingName to the published message, or evaluate the perl expression,
  353. which has to return a hash consisting of readingName=>readingValue
  354. entries.
  355. You can define multiple such tuples, separated by newline, the newline
  356. does not have to be entered in the FHEMWEB frontend. cid is the client-id
  357. of the sending device.<br>
  358. Example:<br>
  359. <code>
  360. &nbsp;&nbsp;attr dev readingList\<br>
  361. &nbsp;&nbsp;&nbsp;&nbsp;myDev/temp:.* temperature\<br>
  362. &nbsp;&nbsp;&nbsp;&nbsp;myDev/hum:.* { { humidity=>$EVTPART0 } }<br>
  363. </code><br>
  364. Notes:
  365. <ul>
  366. <li>in the perl expression the variables $TOPIC, $NAME, $DEVICETOPIC
  367. and $EVENT are available (the letter containing the whole message),
  368. as well as $EVTPART0, $EVTPART1, ... each containing a single word of
  369. the message.</li>
  370. <li>the helper function json2nameValue($EVENT) can be used to parse a
  371. json encoded value. Importing all values from a Sonoff device with a
  372. Tasmota firmware can be done with:
  373. <ul><code>
  374. attr sonoff_th10 readingList tele/sonoff/S.* {
  375. json2nameValue($EVENT) }
  376. </code></ul></li>
  377. </ul>
  378. </li><br>
  379. <a name="setList"></a>
  380. <li>setList cmd [topic|perl-Expression] ...<br>
  381. When the FHEM command cmd is issued, publish the topic.
  382. Multiple tuples can be specified, each of them separated by newline, the
  383. newline does not have to be entered in the FHEMWEB frontend.
  384. Example:<br>
  385. <code>
  386. &nbsp;&nbsp;attr dev setList\<br>
  387. &nbsp;&nbsp;&nbsp;&nbsp;on tasmota/sonoff/cmnd/Power1 on\<br>
  388. &nbsp;&nbsp;&nbsp;&nbsp;off tasmota/sonoff/cmnd/Power1 off
  389. </code><br>
  390. This example defines 2 set commands (on and off), which both publish
  391. the same topic, but with different messages (arguments).<br>
  392. Notes:
  393. <ul>
  394. <li>arguments to the set command will be appended to the message
  395. published (not for the perl expression)</li>
  396. <li>the command arguments are available as $EVENT, $EVTPART0, etc.,
  397. bot in the perl expression and the "normal" topic variant.</li>
  398. <li>the perl expression must return a string containing the topic and
  399. the message separated by a space.</li>
  400. <li>SetExtensions is activated</li>
  401. <li>if the topic name ends with :r, then the retain flag is set</li>
  402. </ul>
  403. </li><br>
  404. <a name="getList"></a>
  405. <li>getList cmd reading [topic|perl-Expression] ...<br>
  406. When the FHEM command cmd is issued, publish the topic, wait for the
  407. answer (the specified reading), and show it in the user interface.
  408. Multiple triples can be specified, each of them separated by newline, the
  409. newline does not have to be entered in the FHEMWEB frontend.<br>
  410. Example:<br>
  411. <code>
  412. &nbsp;&nbsp;attr dev getList\<br>
  413. &nbsp;&nbsp;&nbsp;&nbsp;temp temperature myDev/cmd/getstatus\<br>
  414. &nbsp;&nbsp;&nbsp;&nbsp;hum hum myDev/cmd/getStatus
  415. </code><br>
  416. This example defines 2 get commands (temp and hum), which both publish
  417. the same topic, but wait for different readings to be set.<br>
  418. Notes:
  419. <ul>
  420. <li>the readings must be parsed by a readingList</li>
  421. <li>get is asynchron, it is intended for frontends like FHEMWEB or
  422. telnet, the result cannot be used in self-written perl expressions.
  423. Use a set and a notify/DOIF/etc definition for such a purpose</li>
  424. <li>arguments to the get command will be appended to the message
  425. published (not for the perl expression)</li>
  426. <li>the command arguments are available as $EVENT, $EVTPART0, etc.
  427. </li>
  428. <li>the perl expression must return a string containing the topic and
  429. the message separated by a space.</li>
  430. </ul>
  431. </li><br>
  432. </ul>
  433. </ul>
  434. =end html
  435. =cut