00_MQTT2_CLIENT.pm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. ##############################################
  2. # $Id: 00_MQTT2_CLIENT.pm 17725 2018-11-10 17:43:12Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use DevIo;
  7. sub MQTT2_CLIENT_Read($@);
  8. sub MQTT2_CLIENT_Write($$$);
  9. sub MQTT2_CLIENT_Undef($@);
  10. my $keepalive = 30;
  11. # See also:
  12. # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
  13. sub
  14. MQTT2_CLIENT_Initialize($)
  15. {
  16. my ($hash) = @_;
  17. $hash->{Clients} = ":MQTT2_DEVICE:";
  18. $hash->{MatchList}= { "1:MQTT2_DEVICE" => "^.*" },
  19. $hash->{ReadFn} = "MQTT2_CLIENT_Read";
  20. $hash->{DefFn} = "MQTT2_CLIENT_Define";
  21. $hash->{AttrFn} = "MQTT2_CLIENT_Attr";
  22. $hash->{SetFn} = "MQTT2_CLIENT_Set";
  23. $hash->{UndefFn} = "MQTT2_CLIENT_Undef";
  24. $hash->{DeleteFn}= "MQTT2_CLIENT_Delete";
  25. $hash->{WriteFn} = "MQTT2_CLIENT_Write";
  26. $hash->{ReadyFn} = "MQTT2_CLIENT_connect";
  27. no warnings 'qw';
  28. my @attrList = qw(
  29. autocreate
  30. clientId
  31. disable:0,1
  32. disabledForIntervals
  33. lwt
  34. lwtRetain
  35. mqttVersion:3.1.1,3.1
  36. onConnect
  37. rawEvents
  38. subscriptions
  39. SSL
  40. username
  41. );
  42. use warnings 'qw';
  43. $hash->{AttrList} = join(" ", @attrList);
  44. }
  45. #####################################
  46. sub
  47. MQTT2_CLIENT_Define($$)
  48. {
  49. my ($hash, $def) = @_;
  50. my ($name, $type, $host) = split("[ \t]+", $def);
  51. return "Usage: define <name> MQTT2_CLIENT <hostname>:<tcp-portnr>"
  52. if(!$host);
  53. MQTT2_CLIENT_Undef($hash, undef) if($hash->{OLDDEF}); # modify
  54. $hash->{DeviceName} = $host;
  55. $hash->{clientId} = $hash->{NAME};
  56. $hash->{clientId} =~ s/[^0-9a-zA-Z]//g;
  57. $hash->{clientId} = "MQTT2_CLIENT" if(!$hash->{clientId});
  58. $hash->{connecting} = 1;
  59. InternalTimer(1, "MQTT2_CLIENT_connect", $hash, 0); # need attributes
  60. return undef;
  61. }
  62. sub
  63. MQTT2_CLIENT_connect($)
  64. {
  65. my ($hash) = @_;
  66. my $disco = (ReadingsVal($hash->{NAME}, "state", "") eq "disconnected");
  67. $hash->{connecting} = 1 if($disco && !$hash->{connecting});
  68. $hash->{nextOpenDelay} = 5;
  69. return DevIo_OpenDev($hash, $disco, "MQTT2_CLIENT_doinit", sub(){})
  70. if($hash->{connecting});
  71. }
  72. sub
  73. MQTT2_CLIENT_doinit($)
  74. {
  75. my ($hash) = @_;
  76. my $name = $hash->{NAME};
  77. ############################## CONNECT
  78. if($hash->{connecting} == 1) {
  79. my $usr = AttrVal($name, "username", "");
  80. my ($err, $pwd) = getKeyValue($name);
  81. if($err) {
  82. Log 1, "ERROR: $err";
  83. return;
  84. }
  85. my ($lwtt, $lwtm) = split(" ",AttrVal($name, "lwt", ""),2);
  86. my $lwtr = AttrVal($name, "lwtRetain", 0);
  87. my $m31 = (AttrVal($name, "mqttVersion", "3.1") eq "3.1");
  88. my $msg =
  89. ($m31 ? pack("n",6)."MQIsdp".pack("C",3):
  90. pack("n",4)."MQTT" .pack("C",4)).
  91. pack("C", ($usr ? 0x80 : 0)+
  92. ($pwd ? 0x40 : 0)+
  93. ($lwtr ? 0x20 : 0)+
  94. ($lwtt ? 0x04 : 0)+2). # clean session
  95. pack("n", $keepalive).
  96. pack("n", length($hash->{clientId})).$hash->{clientId}.
  97. ($lwtt ? (pack("n", length($lwtt)).$lwtt).
  98. (pack("n", length($lwtm)).$lwtm) : "").
  99. ($usr ? (pack("n", length($usr)).$usr) : "").
  100. ($pwd ? (pack("n", length($pwd)).$pwd) : "");
  101. $hash->{connecting} = 2;
  102. addToWritebuffer($hash,
  103. pack("C",0x10).
  104. MQTT2_CLIENT_calcRemainingLength(length($msg)).$msg);
  105. RemoveInternalTimer($hash);
  106. InternalTimer(gettimeofday()+$keepalive, "MQTT2_CLIENT_keepalive",$hash,0);
  107. ############################## SUBSCRIBE
  108. } elsif($hash->{connecting} == 2) {
  109. my $msg =
  110. pack("n", $hash->{FD}). # packed Identifier
  111. join("", map { pack("n", length($_)).$_.pack("C",0) } # QOS:0
  112. split(" ", AttrVal($name, "subscriptions", "#")));
  113. addToWritebuffer($hash,
  114. pack("C",0x80).
  115. MQTT2_CLIENT_calcRemainingLength(length($msg)).$msg);
  116. $hash->{connecting} = 3;
  117. }
  118. return undef;
  119. }
  120. sub
  121. MQTT2_CLIENT_keepalive($)
  122. {
  123. my ($hash) = @_;
  124. my $name = $hash->{NAME};
  125. return if(ReadingsVal($name, "state", "") ne "opened");
  126. Log3 $name, 5, "$name: keepalive $keepalive";
  127. my $msg = join("", map { pack("n", length($_)).$_.pack("C",0) } # QOS:0
  128. split(" ", AttrVal($name, "subscriptions", "#")));
  129. addToWritebuffer($hash,
  130. pack("C",0xC0).pack("C",0));
  131. InternalTimer(gettimeofday()+$keepalive, "MQTT2_CLIENT_keepalive", $hash, 0);
  132. }
  133. sub
  134. MQTT2_CLIENT_Undef($@)
  135. {
  136. my ($hash, $arg) = @_;
  137. DevIo_CloseDev($hash);
  138. return undef;
  139. }
  140. sub
  141. MQTT2_CLIENT_Delete($@)
  142. {
  143. my ($hash, $arg) = @_;
  144. setKeyValue($hash->{NAME}, undef);
  145. return undef;
  146. }
  147. sub
  148. MQTT2_CLIENT_Attr(@)
  149. {
  150. my ($type, $devName, $attrName, @param) = @_;
  151. my $hash = $defs{$devName};
  152. if($type eq "set" && $attrName eq "SSL") {
  153. $hash->{SSL} = $param[0] ? $param[0] : 1;
  154. }
  155. if($attrName eq "clientId") {
  156. $hash->{clientId} = $param[0];
  157. $hash->{clientId} =~ s/[^0-9a-zA-Z]//g;
  158. $hash->{clientId} = "MQTT2_CLIENT" if(!$hash->{clientId});
  159. }
  160. my %h = (clientId=>1,lwt=>1,lwtRetain=>1,subscriptions=>1,SSL=>1,username=>1);
  161. if($init_done && $h{$attrName}) {
  162. MQTT2_CLIENT_Disco($hash);
  163. }
  164. return undef;
  165. }
  166. sub
  167. MQTT2_CLIENT_Disco($)
  168. {
  169. my ($hash) = @_;
  170. RemoveInternalTimer($hash);
  171. $hash->{connecting} = 1;
  172. DevIo_Disconnected($hash);
  173. }
  174. sub
  175. MQTT2_CLIENT_Set($@)
  176. {
  177. my ($hash, @a) = @_;
  178. my %sets = ( password=>2, publish=>2 );
  179. my $name = $hash->{NAME};
  180. shift(@a);
  181. return "Unknown argument ?, choose one of ".join(" ", keys %sets)
  182. if(!$a[0] || !$sets{$a[0]});
  183. if($a[0] eq "publish") {
  184. shift(@a);
  185. my $retain;
  186. if(@a>2 && $a[0] eq "-r") {
  187. $retain = 1;
  188. shift(@a);
  189. }
  190. return "Usage: set $name publish -r topic [value]" if(@a < 1);
  191. my $tp = shift(@a);
  192. my $val = join(" ", @a);
  193. MQTT2_CLIENT_doPublish($hash, $tp, $val, $retain);
  194. }
  195. if($a[0] eq "password") {
  196. return "Usage: set $name password <password>" if(@a < 1);
  197. setKeyValue($name, $a[1]);
  198. MQTT2_CLIENT_Disco($hash) if($init_done);
  199. }
  200. }
  201. my %cptype = (
  202. 0 => "RESERVED_0",
  203. 1 => "CONNECT",
  204. 2 => "CONNACK", #
  205. 3 => "PUBLISH", #
  206. 4 => "PUBACK", #
  207. 5 => "PUBREC",
  208. 6 => "PUBREL",
  209. 7 => "PUBCOMP",
  210. 8 => "SUBSCRIBE",
  211. 9 => "SUBACK", #
  212. 10 => "UNSUBSCRIBE",
  213. 11 => "UNSUBACK",
  214. 12 => "PINGREQ",
  215. 13 => "PINGRESP", #
  216. 14 => "DISCONNECT",#
  217. 15 => "RESERVED_15",
  218. );
  219. #####################################
  220. sub
  221. MQTT2_CLIENT_Read($@)
  222. {
  223. my ($hash, $reread) = @_;
  224. my $name = $hash->{NAME};
  225. my $fd = $hash->{FD};
  226. if(!$reread) {
  227. my $buf = DevIo_SimpleRead($hash);
  228. return "" if(!defined($buf));
  229. $hash->{BUF} .= $buf;
  230. }
  231. my ($tlen, $off) = MQTT2_CLIENT_getRemainingLength($hash);
  232. if($tlen < 0 || $tlen+$off<=0) {
  233. Log3 $name, 1, "Bogus data from $name, closing connection";
  234. MQTT2_CLIENT_Disco($hash);
  235. return;
  236. }
  237. return if(length($hash->{BUF}) < $tlen+$off);
  238. my $fb = substr($hash->{BUF}, 0, 1);
  239. my $pl = substr($hash->{BUF}, $off, $tlen); # payload
  240. $hash->{BUF} = substr($hash->{BUF}, $tlen+$off);
  241. my $cp = ord(substr($fb,0,1)) >> 4;
  242. my $cpt = $cptype{$cp};
  243. $hash->{lastMsgTime} = gettimeofday();
  244. # Lowlevel debugging
  245. if(AttrVal($name, "verbose", 1) >= 5) {
  246. my $pltxt = $pl;
  247. $pltxt =~ s/([^ -~])/"(".ord($1).")"/ge;
  248. Log3 $name, 5, "$name: received $cpt $pltxt";
  249. }
  250. ####################################
  251. if($cpt eq "CONNACK") {
  252. my $rc = ord(substr($pl,1,1));
  253. if($rc == 0) {
  254. my $onc = AttrVal($name, "onConnect", "");
  255. MQTT2_CLIENT_doPublish($hash, split(" ", $onc, 2)) if($onc);
  256. MQTT2_CLIENT_doinit($hash);
  257. } else {
  258. my @txt = ("Accepted", "bad proto", "bad id", "server unavailable",
  259. "bad user name or password", "not authorized");
  260. Log3 $name, 1, "$name: Connection refused, ".
  261. ($rc <= int(@txt) ? $txt[$rc] : "unknown error $rc");
  262. MQTT2_CLIENT_Disco($hash);
  263. return;
  264. }
  265. } elsif($cpt eq "PUBACK") { # ignore it
  266. } elsif($cpt eq "SUBACK") {
  267. delete($hash->{connecting});
  268. } elsif($cpt eq "PINGRESP") { # ignore it
  269. } elsif($cpt eq "PUBLISH") {
  270. my $cf = ord(substr($fb,0,1)) & 0xf;
  271. my $qos = ($cf & 0x06) >> 1;
  272. my ($tp, $val, $pid);
  273. ($tp, $off) = MQTT2_CLIENT_getStr($pl, 0);
  274. if($qos) {
  275. $pid = unpack('n', substr($pl, $off, 2));
  276. $off += 2;
  277. }
  278. $val = substr($pl, $off);
  279. addToWritebuffer($hash, pack("CCnC*", 0x40, 2, $pid)) if($qos); # PUBACK
  280. if(!IsDisabled($name)) {
  281. $val = "" if(!defined($val));
  282. my $ac = AttrVal($name, "autocreate", undef) ? "autocreate:":"";
  283. my $cid = $hash->{clientId};
  284. Dispatch($hash, "$ac$cid:$tp:$val", undef, !$ac);
  285. my $re = AttrVal($name, "rawEvents", undef);
  286. DoTrigger($name, "$tp:$val") if($re && $tp =~ m/$re/);
  287. }
  288. } else {
  289. Log 1, "M2: Unhandled packet $cpt, disconneting $name";
  290. MQTT2_CLIENT_Disco($hash);
  291. }
  292. return MQTT2_CLIENT_Read($hash, 1);
  293. }
  294. ######################################
  295. # send topic to client if its subscription matches the topic
  296. sub
  297. MQTT2_CLIENT_doPublish($$$$)
  298. {
  299. my ($hash, $topic, $val, $retain) = @_;
  300. my $name = $hash->{NAME};
  301. return if(IsDisabled($name));
  302. $val = "" if(!defined($val));
  303. Log3 $name, 5, "$name: sending PUBLISH $topic $val";
  304. addToWritebuffer($hash,
  305. pack("C",0x30).
  306. MQTT2_CLIENT_calcRemainingLength(2+length($topic)+length($val)).
  307. pack("n", length($topic)).
  308. $topic.$val);
  309. }
  310. sub
  311. MQTT2_CLIENT_Write($$$)
  312. {
  313. my ($hash,$topic,$msg) = @_;
  314. my $retain;
  315. if($topic =~ m/^(.*):r$/) {
  316. $topic = $1;
  317. $retain = 1;
  318. }
  319. MQTT2_CLIENT_doPublish($hash, $topic, $msg, $retain);
  320. }
  321. sub
  322. MQTT2_CLIENT_calcRemainingLength($)
  323. {
  324. my ($l) = @_;
  325. my @r;
  326. while($l > 0) {
  327. my $eb = $l % 128;
  328. $l = int($l/128);
  329. $eb += 128 if($l);
  330. push(@r, $eb);
  331. }
  332. return pack("C*", @r);
  333. }
  334. sub
  335. MQTT2_CLIENT_getRemainingLength($)
  336. {
  337. my ($hash) = @_;
  338. return (2,2) if(length($hash->{BUF}) < 2);
  339. my $ret = 0;
  340. my $mul = 1;
  341. for(my $off = 1; $off <= 4; $off++) {
  342. my $b = ord(substr($hash->{BUF},$off,1));
  343. $ret += ($b & 0x7f)*$mul;
  344. return ($ret, $off+1) if(($b & 0x80) == 0);
  345. $mul *= 128;
  346. }
  347. return -1;
  348. }
  349. sub
  350. MQTT2_CLIENT_getStr($$)
  351. {
  352. my ($in, $off) = @_;
  353. my $l = unpack("n", substr($in, $off, 2));
  354. return (substr($in, $off+2, $l), $off+2+$l);
  355. }
  356. 1;
  357. =pod
  358. =item helper
  359. =item summary Connection to an external MQTT server
  360. =item summary_DE Verbindung zu einem externen MQTT Server
  361. =begin html
  362. <a name="MQTT2_CLIENT"></a>
  363. <h3>MQTT2_CLIENT</h3>
  364. <ul>
  365. MQTT2_CLIENT is a cleanroom implementation of an MQTT client (which connects
  366. to an external server, like mosquitto) using no perl libraries. It serves as
  367. an IODev to MQTT2_DEVICES.
  368. <br> <br>
  369. <a name="MQTT2_CLIENTdefine"></a>
  370. <b>Define</b>
  371. <ul>
  372. <code>define &lt;name&gt; MQTT2_CLIENT &lt;host&gt;:&lt;port&gt;</code>
  373. <br><br>
  374. Connect to the server on &lt;host&gt; and &lt;port&gt;. &lt;port&gt; 1883
  375. is default for mosquitto.
  376. <br>
  377. Notes:<br>
  378. <ul>
  379. <li>only QOS 0 and 1 is implemented</li>
  380. </ul>
  381. </ul>
  382. <br>
  383. <a name="MQTT2_CLIENTset"></a>
  384. <b>Set</b>
  385. <ul>
  386. <li>publish -r topic value<br>
  387. publish a message, -r denotes setting the retain flag.
  388. </li><br>
  389. <li>password &lt;password&gt; value<br>
  390. set the password, which is stored in the FHEM/FhemUtils/uniqueID file.
  391. </li>
  392. </ul>
  393. <br>
  394. <a name="MQTT2_CLIENTget"></a>
  395. <b>Get</b>
  396. <ul>N/A</ul><br>
  397. <a name="MQTT2_CLIENTattr"></a>
  398. <b>Attributes</b>
  399. <ul>
  400. <a name="autocreate"></a>
  401. <li>autocreate<br>
  402. if set, at least one MQTT2_DEVICE will be created, and its readingsList
  403. will be expanded upon reception of published messages. Note: this is
  404. slightly different from MQTT2_SERVER, where each connection has its own
  405. clientId. This parameter is sadly not transferred via the MQTT protocol,
  406. so the clientId of this MQTT2_CLIENT instance will be used.
  407. </li></br>
  408. <a name="clientId"></a>
  409. <li>clientId &lt;name&gt;<br>
  410. set the MQTT clientId. If not set, the name of the MQTT2_CLIENT instance
  411. is used, after deleting everything outside 0-9a-zA-Z
  412. </li></br>
  413. <li><a href="#disable">disable</a><br>
  414. <a href="#disabledForIntervals">disabledForIntervals</a><br>
  415. disable dispatching of messages.
  416. </li><br>
  417. <a name="lwt"></a>
  418. <li>lwt &lt;topic&gt; &lt;message&gt; <br>
  419. set the LWT (last will and testament) topic and message, default is empty.
  420. </li></br>
  421. <a name="lwtRetain"></a>
  422. <li>lwtRetain<br>
  423. if set, the lwt retain flag is set
  424. </li></br>
  425. <a name="mqttVersion"></a>
  426. <li>mqttVersion 3.1,3.1.1<br>
  427. set the MQTT protocol version in the CONNECT header, default is 3.1
  428. </li></br>
  429. <a name="onConnect"></a>
  430. <li>onConnect topic message<br>
  431. publish the topic after each connect or reconnect.
  432. </li></br>
  433. <a name="rawEvents"></a>
  434. <li>rawEvents &lt;topic-regexp&gt;<br>
  435. send all messages as events attributed to this MQTT2_CLIENT instance.
  436. Should only be used, if there is no MQTT2_DEVICE to process the topic.
  437. </li><br>
  438. <a name="subscriptions"></a>
  439. <li>subscriptions &lt;subscriptions&gt;<br>
  440. space separated list of MQTT subscriptions, default is #
  441. </li><br>
  442. <a name="SSL"></a>
  443. <li>SSL<br>
  444. Enable SSL (i.e. TLS)
  445. </li><br>
  446. <a name="username"></a>
  447. <li>username &lt;username&gt;<br>
  448. set the username. The password is set via the set command, and is stored
  449. separately, see above.
  450. </li><br>
  451. </ul>
  452. </ul>
  453. =end html
  454. =cut