10_MQTT2_DEVICE.pm 16 KB

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