00_MYSENSORS.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. ##############################################
  2. #
  3. # fhem driver for MySensors serial or network gateway (see http://mysensors.org)
  4. #
  5. # Copyright (C) 2014 Norbert Truchsess
  6. #
  7. # This file is part of fhem.
  8. #
  9. # Fhem is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation, either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # Fhem is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License
  20. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. # $Id: 00_MYSENSORS.pm 15106 2017-09-20 13:23:20Z Hauswart $
  23. #
  24. ##############################################
  25. my %sets = (
  26. "connect" => [],
  27. "disconnect" => [],
  28. "inclusion-mode" => [qw(on off)],
  29. );
  30. my %gets = (
  31. "version" => ""
  32. );
  33. my @clients = qw(
  34. MYSENSORS_DEVICE
  35. );
  36. sub MYSENSORS_Initialize($) {
  37. my $hash = shift @_;
  38. require "$main::attr{global}{modpath}/FHEM/DevIo.pm";
  39. # Provider
  40. $hash->{Clients} = join (':',@clients);
  41. $hash->{ReadyFn} = "MYSENSORS::Ready";
  42. $hash->{ReadFn} = "MYSENSORS::Read";
  43. # Consumer
  44. $hash->{DefFn} = "MYSENSORS::Define";
  45. $hash->{UndefFn} = "MYSENSORS::Undef";
  46. $hash->{SetFn} = "MYSENSORS::Set";
  47. $hash->{AttrFn} = "MYSENSORS::Attr";
  48. $hash->{NotifyFn} = "MYSENSORS::Notify";
  49. $hash->{AttrList} =
  50. "autocreate:1 ".
  51. "requestAck:1 ".
  52. "first-sensorid ".
  53. "last-sensorid ".
  54. "stateFormat";
  55. }
  56. package MYSENSORS;
  57. use Exporter ('import');
  58. @EXPORT = ();
  59. @EXPORT_OK = qw(sendMessage);
  60. %EXPORT_TAGS = (all => [@EXPORT_OK]);
  61. use strict;
  62. use warnings;
  63. use GPUtils qw(:all);
  64. use Device::MySensors::Constants qw(:all);
  65. use Device::MySensors::Message qw(:all);
  66. BEGIN {GP_Import(qw(
  67. CommandDefine
  68. CommandModify
  69. CommandAttr
  70. gettimeofday
  71. readingsSingleUpdate
  72. DevIo_OpenDev
  73. DevIo_SimpleWrite
  74. DevIo_SimpleRead
  75. DevIo_CloseDev
  76. RemoveInternalTimer
  77. InternalTimer
  78. AttrVal
  79. Log3
  80. ))};
  81. my %sensorAttr = (
  82. LIGHT => ['setCommands on:V_LIGHT:1 off:V_LIGHT:0' ],
  83. ARDUINO_NODE => [ 'config M' ],
  84. ARDUINO_REPEATER_NODE => [ 'config M' ],
  85. );
  86. sub Define($$) {
  87. my ( $hash, $def ) = @_;
  88. $hash->{NOTIFYDEV} = "global";
  89. if ($main::init_done) {
  90. return Start($hash);
  91. } else {
  92. return undef;
  93. }
  94. }
  95. sub Undef($) {
  96. Stop(shift);
  97. return undef;
  98. }
  99. sub Set($@) {
  100. my ($hash, @a) = @_;
  101. return "Need at least one parameters" if(@a < 2);
  102. return "Unknown argument $a[1], choose one of " . join(" ", map {@{$sets{$_}} ? $_.':'.join ',', @{$sets{$_}} : $_} sort keys %sets)
  103. if(!defined($sets{$a[1]}));
  104. my $command = $a[1];
  105. my $value = $a[2];
  106. COMMAND_HANDLER: {
  107. $command eq "connect" and do {
  108. Start($hash);
  109. last;
  110. };
  111. $command eq "disconnect" and do {
  112. Stop($hash);
  113. last;
  114. };
  115. $command eq "inclusion-mode" and do {
  116. sendMessage($hash,radioId => 0, childId => 0, cmd => C_INTERNAL, ack => 0, subType => I_INCLUSION_MODE, payload => $value eq 'on' ? 1 : 0);
  117. $hash->{'inclusion-mode'} = $value eq 'on' ? 1 : 0;
  118. last;
  119. };
  120. };
  121. }
  122. sub Attr($$$$) {
  123. my ($command,$name,$attribute,$value) = @_;
  124. my $hash = $main::defs{$name};
  125. ATTRIBUTE_HANDLER: {
  126. $attribute eq "autocreate" and do {
  127. if ($main::init_done) {
  128. my $mode = $command eq "set" ? 1 : 0;
  129. sendMessage($hash,radioId => $hash->{radioId}, childId => $hash->{childId}, ack => 0, subType => I_INCLUSION_MODE, payload => $mode);
  130. $hash->{'inclusion-mode'} = $mode;
  131. }
  132. last;
  133. };
  134. $attribute eq "requestAck" and do {
  135. if ($command eq "set") {
  136. $hash->{ack} = 1;
  137. } else {
  138. $hash->{ack} = 0;
  139. $hash->{messages} = {};
  140. $hash->{outstandingAck} = 0;
  141. }
  142. last;
  143. };
  144. }
  145. }
  146. sub Notify($$) {
  147. my ($hash,$dev) = @_;
  148. if( grep(m/^(INITIALIZED|REREADCFG)$/, @{$dev->{CHANGED}}) ) {
  149. Start($hash);
  150. } elsif( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) {
  151. }
  152. }
  153. sub Start($) {
  154. my $hash = shift;
  155. my ($dev) = split("[ \t]+", $hash->{DEF});
  156. $hash->{DeviceName} = $dev;
  157. CommandAttr(undef, "$hash->{NAME} stateFormat connection") unless AttrVal($hash->{NAME},"stateFormat",undef);
  158. DevIo_CloseDev($hash);
  159. return DevIo_OpenDev($hash, 0, "MYSENSORS::Init");
  160. }
  161. sub Stop($) {
  162. my $hash = shift;
  163. DevIo_CloseDev($hash);
  164. RemoveInternalTimer($hash);
  165. readingsSingleUpdate($hash,"connection","disconnected",1);
  166. }
  167. sub Ready($) {
  168. my $hash = shift;
  169. return DevIo_OpenDev($hash, 1, "MYSENSORS::Init") if($hash->{STATE} eq "disconnected");
  170. if(defined($hash->{USBDev})) {
  171. my $po = $hash->{USBDev};
  172. my ( $BlockingFlags, $InBytes, $OutBytes, $ErrorFlags ) = $po->status;
  173. return ( $InBytes > 0 );
  174. }
  175. }
  176. sub Init($) {
  177. my $hash = shift;
  178. my $name = $hash->{NAME};
  179. $hash->{'inclusion-mode'} = AttrVal($name,"autocreate",0);
  180. $hash->{ack} = AttrVal($name,"requestAck",0);
  181. $hash->{outstandingAck} = 0;
  182. if ($hash->{ack}) {
  183. GP_ForallClients($hash,sub {
  184. my $client = shift;
  185. $hash->{messagesForRadioId}->{$client->{radioId}} = {
  186. lastseen => -1,
  187. nexttry => -1,
  188. numtries => 1,
  189. messages => [],
  190. };
  191. });
  192. }
  193. readingsSingleUpdate($hash,"connection","connected",1);
  194. sendMessage($hash, radioId => 0, childId => 0, cmd => C_INTERNAL, ack => 0, subType => I_VERSION, payload => '');
  195. return undef;
  196. }
  197. sub Timer($) {
  198. my $hash = shift;
  199. my $now = time;
  200. foreach my $radioid (keys %{$hash->{messagesForRadioId}}) {
  201. my $msgsForId = $hash->{messagesForRadioId}->{$radioid};
  202. if ($now > $msgsForId->{nexttry}) {
  203. foreach my $msg (@{$msgsForId->{messages}}) {
  204. my $txt = createMsg(%$msg);
  205. Log3 ($hash->{NAME},5,"MYSENSORS outstanding ack, re-send: ".dumpMsg($msg));
  206. DevIo_SimpleWrite($hash,"$txt\n",undef);
  207. }
  208. $msgsForId->{numtries}++;
  209. $msgsForId->{nexttry} = gettimeofday()+$msgsForId->{numtries};
  210. }
  211. }
  212. _scheduleTimer($hash);
  213. }
  214. sub Read {
  215. my ($hash) = @_;
  216. my $name = $hash->{NAME};
  217. my $buf = DevIo_SimpleRead($hash);
  218. return "" if(!defined($buf));
  219. my $data = $hash->{PARTIAL};
  220. Log3 ($name, 5, "MYSENSORS/RAW: $data/$buf");
  221. $data .= $buf;
  222. while ($data =~ m/\n/) {
  223. my $txt;
  224. ($txt,$data) = split("\n", $data, 2);
  225. $txt =~ s/\r//;
  226. if (my $msg = parseMsg($txt)) {
  227. Log3 ($name,5,"MYSENSORS Read: ".dumpMsg($msg));
  228. if ($msg->{ack}) {
  229. onAcknowledge($hash,$msg);
  230. }
  231. my $type = $msg->{cmd};
  232. MESSAGE_TYPE: {
  233. $type == C_PRESENTATION and do {
  234. onPresentationMsg($hash,$msg);
  235. last;
  236. };
  237. $type == C_SET and do {
  238. onSetMsg($hash,$msg);
  239. last;
  240. };
  241. $type == C_REQ and do {
  242. onRequestMsg($hash,$msg);
  243. last;
  244. };
  245. $type == C_INTERNAL and do {
  246. onInternalMsg($hash,$msg);
  247. last;
  248. };
  249. $type == C_STREAM and do {
  250. onStreamMsg($hash,$msg);
  251. last;
  252. };
  253. }
  254. } else {
  255. Log3 ($name,5,"MYSENSORS Read: ".$txt."is no parsable mysensors message");
  256. }
  257. }
  258. $hash->{PARTIAL} = $data;
  259. return undef;
  260. };
  261. sub onPresentationMsg($$) {
  262. my ($hash,$msg) = @_;
  263. my $client = matchClient($hash,$msg);
  264. my $clientname;
  265. my $sensorType = $msg->{subType};
  266. unless ($client) {
  267. if ($hash->{'inclusion-mode'}) {
  268. $clientname = "MYSENSOR_$msg->{radioId}";
  269. CommandDefine(undef,"$clientname MYSENSORS_DEVICE $msg->{radioId}");
  270. $client = $main::defs{$clientname};
  271. return unless ($client);
  272. } else {
  273. Log3($hash->{NAME},3,"MYSENSORS: ignoring presentation-msg from unknown radioId $msg->{radioId}, childId $msg->{childId}, sensorType $sensorType");
  274. return;
  275. }
  276. }
  277. MYSENSORS::DEVICE::onPresentationMessage($client,$msg);
  278. };
  279. sub onSetMsg($$) {
  280. my ($hash,$msg) = @_;
  281. if (my $client = matchClient($hash,$msg)) {
  282. MYSENSORS::DEVICE::onSetMessage($client,$msg);
  283. } else {
  284. Log3($hash->{NAME},3,"MYSENSORS: ignoring set-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType}));
  285. }
  286. };
  287. sub onRequestMsg($$) {
  288. my ($hash,$msg) = @_;
  289. if (my $client = matchClient($hash,$msg)) {
  290. MYSENSORS::DEVICE::onRequestMessage($client,$msg);
  291. } else {
  292. Log3($hash->{NAME},3,"MYSENSORS: ignoring req-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".variableTypeToStr($msg->{subType}));
  293. }
  294. };
  295. sub onInternalMsg($$) {
  296. my ($hash,$msg) = @_;
  297. my $address = $msg->{radioId};
  298. my $type = $msg->{subType};
  299. if ($address == 0 or $address == 255) { #msg to or from gateway
  300. TYPE: {
  301. $type == I_INCLUSION_MODE and do {
  302. if (AttrVal($hash->{NAME},"autocreate",0)) { #if autocreate is switched on, keep gateways inclusion-mode active
  303. if ($msg->{payload} == 0) {
  304. sendMessage($hash,radioId => $msg->{radioId}, childId => $msg->{childId}, ack => 0, subType => I_INCLUSION_MODE, payload => 1);
  305. }
  306. } else {
  307. $hash->{'inclusion-mode'} = $msg->{payload};
  308. }
  309. last;
  310. };
  311. $type == I_GATEWAY_READY and do {
  312. readingsSingleUpdate($hash,'connection','startup complete',1);
  313. GP_ForallClients($hash,sub {
  314. my $client = shift;
  315. MYSENSORS::DEVICE::onGatewayStarted($client);
  316. });
  317. last;
  318. };
  319. $type == I_VERSION and do {
  320. $hash->{version} = $msg->{payload};
  321. last;
  322. };
  323. $type == I_LOG_MESSAGE and do {
  324. Log3($hash->{NAME},5,"MYSENSORS gateway $hash->{NAME}: $msg->{payload}");
  325. last;
  326. };
  327. $type == I_ID_REQUEST and do {
  328. if ($hash->{'inclusion-mode'}) {
  329. my %nodes = map {$_ => 1} (AttrVal($hash->{NAME},"first-sensorid",20) ... AttrVal($hash->{NAME},"last-sensorid",254));
  330. GP_ForallClients($hash,sub {
  331. my $client = shift;
  332. delete $nodes{$client->{radioId}};
  333. });
  334. if (keys %nodes) {
  335. my $newid = (sort keys %nodes)[0];
  336. sendMessage($hash,radioId => 255, childId => 255, cmd => C_INTERNAL, ack => 0, subType => I_ID_RESPONSE, payload => $newid);
  337. Log3($hash->{NAME},4,"MYSENSORS $hash->{NAME} assigned new nodeid $newid");
  338. } else {
  339. Log3($hash->{NAME},4,"MYSENSORS $hash->{NAME} cannot assign new nodeid");
  340. }
  341. } else {
  342. Log3($hash->{NAME},4,"MYSENSORS: ignoring id-request-msg from unknown radioId $msg->{radioId}");
  343. }
  344. last;
  345. };
  346. }
  347. } elsif (my $client = matchClient($hash,$msg)) {
  348. MYSENSORS::DEVICE::onInternalMessage($client,$msg);
  349. } else {
  350. Log3($hash->{NAME},3,"MYSENSORS: ignoring internal-msg from unknown radioId $msg->{radioId}, childId $msg->{childId} for ".internalMessageTypeToStr($msg->{subType}));
  351. }
  352. };
  353. sub onStreamMsg($$) {
  354. my ($hash,$msg) = @_;
  355. };
  356. sub onAcknowledge($$) {
  357. my ($hash,$msg) = @_;
  358. my $ack;
  359. if (defined (my $outstanding = $hash->{messagesForRadioId}->{$msg->{radioId}}->{messages})) {
  360. my @remainMsg = grep {
  361. $_->{childId} != $msg->{childId}
  362. or $_->{cmd} != $msg->{cmd}
  363. or $_->{subType} != $msg->{subType}
  364. or $_->{payload} ne $msg->{payload}
  365. } @$outstanding;
  366. if ($ack = @remainMsg < @$outstanding) {
  367. $hash->{outstandingAck} -= 1;
  368. @$outstanding = @remainMsg;
  369. }
  370. $hash->{messagesForRadioId}->{$msg->{radioId}}->{numtries} = 1;
  371. }
  372. Log3 ($hash->{NAME},4,"MYSENSORS Read: unexpected ack ".dumpMsg($msg)) unless $ack;
  373. }
  374. sub sendMessage($%) {
  375. my ($hash,%msg) = @_;
  376. $msg{ack} = $hash->{ack} unless defined $msg{ack};
  377. my $txt = createMsg(%msg);
  378. Log3 ($hash->{NAME},5,"MYSENSORS send: ".dumpMsg(\%msg));
  379. DevIo_SimpleWrite($hash,"$txt\n",undef);
  380. if ($msg{ack}) {
  381. my $messagesForRadioId = $hash->{messagesForRadioId}->{$msg{radioId}};
  382. unless (defined $messagesForRadioId) {
  383. $messagesForRadioId = {
  384. lastseen => -1,
  385. numtries => 1,
  386. messages => [],
  387. };
  388. $hash->{messagesForRadioId}->{$msg{radioId}} = $messagesForRadioId;
  389. }
  390. my $messages = $messagesForRadioId->{messages};
  391. @$messages = grep {
  392. $_->{childId} != $msg{childId}
  393. or $_->{cmd} != $msg{cmd}
  394. or $_->{subType} != $msg{subType}
  395. } @$messages;
  396. push @$messages,\%msg;
  397. $messagesForRadioId->{nexttry} = gettimeofday()+$messagesForRadioId->{numtries};
  398. _scheduleTimer($hash);
  399. }
  400. };
  401. sub _scheduleTimer($) {
  402. my ($hash) = @_;
  403. $hash->{outstandingAck} = 0;
  404. RemoveInternalTimer($hash);
  405. my $next;
  406. foreach my $radioid (keys %{$hash->{messagesForRadioId}}) {
  407. my $msgsForId = $hash->{messagesForRadioId}->{$radioid};
  408. $hash->{outstandingAck} += @{$msgsForId->{messages}};
  409. $next = $msgsForId->{nexttry} unless (defined $next and $next < $msgsForId->{nexttry});
  410. };
  411. InternalTimer($next, "MYSENSORS::Timer", $hash, 0) if (defined $next);
  412. }
  413. sub matchClient($$) {
  414. my ($hash,$msg) = @_;
  415. my $radioId = $msg->{radioId};
  416. my $found;
  417. GP_ForallClients($hash,sub {
  418. return if $found;
  419. my $client = shift;
  420. if ($client->{radioId} == $radioId) {
  421. $found = $client;
  422. }
  423. });
  424. return $found;
  425. }
  426. 1;
  427. =pod
  428. =item device
  429. =item summary includes a MYSENSORS gateway
  430. =item summary_DE integriert ein MYSENSORS Gateway
  431. =begin html
  432. <a name="MYSENSORS"></a>
  433. <h3>MYSENSORS</h3>
  434. <ul>
  435. <p>connects fhem to <a href="http://MYSENSORS.org">MYSENSORS</a>.</p>
  436. <p>A single MYSENSORS device can serve multiple <a href="#MYSENSORS_DEVICE">MYSENSORS_DEVICE</a> clients.<br/>
  437. Each <a href="#MYSENSORS_DEVICE">MYSENSORS_DEVICE</a> represents a mysensors node.<br/>
  438. <a name="MYSENSORSdefine"></a>
  439. <p><b>Define</b></p>
  440. <ul>
  441. <p><code>define &lt;name&gt; MYSENSORS &lt;serial device&gt|&lt;ip:port&gt;</code></p>
  442. <p>Specifies the MYSENSORS device.</p>
  443. </ul>
  444. <a name="MYSENSORSset"></a>
  445. <p><b>Set</b></p>
  446. <ul>
  447. <li>
  448. <p><code>set &lt;name&gt; connect</code><br/>
  449. (re-)connects the MYSENSORS-device to the MYSENSORS-gateway</p>
  450. </li>
  451. <li>
  452. <p><code>set &lt;name&gt; disconnect</code><br/>
  453. disconnects the MYSENSORS-device from the MYSENSORS-gateway</p>
  454. </li>
  455. <li>
  456. <p><code>set &lt;name&gt; inclusion-mode on|off</code><br/>
  457. turns the gateways inclusion-mode on or off</p>
  458. </li>
  459. </ul>
  460. <a name="MYSENSORSattr"></a>
  461. <p><b>Attributes</b></p>
  462. <ul>
  463. <li>
  464. <p><code>att &lt;name&gt; autocreate</code><br/>
  465. enables auto-creation of MYSENSOR_DEVICE-devices on receival of presentation-messages</p>
  466. </li>
  467. <li>
  468. <p><code>att &lt;name&gt; requestAck</code><br/>
  469. request acknowledge from nodes.<br/>
  470. if set the Readings of nodes are updated not before requested acknowledge is received<br/>
  471. if not set the Readings of nodes are updated immediatly (not awaiting the acknowledge).
  472. May also be configured for individual nodes if not set for gateway.</p>
  473. </li>
  474. <li>
  475. <p><code>att &lt;name&gt; first-sensorid <&lt;number &lth; 255&gt;></code><br/>
  476. configures the lowest node-id assigned to a mysensor-node on request (defaults to 20)</p>
  477. </li>
  478. </ul>
  479. </ul>
  480. =end html
  481. =cut