93_FHEM2FHEM.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. ##############################################
  2. # $Id: 93_FHEM2FHEM.pm 13024 2017-01-09 15:16:44Z rudolfkoenig $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use Time::HiRes qw(gettimeofday);
  7. use HttpUtils;
  8. sub FHEM2FHEM_Read($);
  9. sub FHEM2FHEM_Ready($);
  10. sub FHEM2FHEM_OpenDev($$);
  11. sub FHEM2FHEM_CloseDev($);
  12. sub FHEM2FHEM_Disconnected($);
  13. sub FHEM2FHEM_Define($$);
  14. sub FHEM2FHEM_Undef($$);
  15. sub
  16. FHEM2FHEM_Initialize($)
  17. {
  18. my ($hash) = @_;
  19. # Provider
  20. $hash->{ReadFn} = "FHEM2FHEM_Read";
  21. $hash->{WriteFn} = "FHEM2FHEM_Write";
  22. $hash->{ReadyFn} = "FHEM2FHEM_Ready";
  23. $hash->{SetFn} = "FHEM2FHEM_Set";
  24. $hash->{noRawInform} = 1;
  25. # Normal devices
  26. $hash->{DefFn} = "FHEM2FHEM_Define";
  27. $hash->{UndefFn} = "FHEM2FHEM_Undef";
  28. $hash->{AttrList}= "dummy:1,0 disable:0,1 ".
  29. "disabledForIntervals excludeEvents eventOnly:1,0";
  30. }
  31. #####################################
  32. sub
  33. FHEM2FHEM_Define($$)
  34. {
  35. my ($hash, $def) = @_;
  36. my @a = split("[ \t][ \t]*", $def);
  37. if(@a < 4 || @a > 5 || !($a[3] =~ m/^(LOG|RAW):(.*)$/)) {
  38. my $msg = "wrong syntax: define <name> FHEM2FHEM host[:port][:SSL] ".
  39. "[LOG:regexp|RAW:device] {portpasswort}";
  40. Log3 $hash, 2, $msg;
  41. return $msg;
  42. }
  43. $hash->{informType} = $1;
  44. if($1 eq "LOG") {
  45. $hash->{regexp} = $2;
  46. } else {
  47. my $rdev = $2;
  48. my $iodev = $defs{$rdev};
  49. return "Undefined local device $rdev" if(!$iodev);
  50. $hash->{rawDevice} = $rdev;
  51. $hash->{Clients} = $iodev->{Clients};
  52. $hash->{Clients} = $modules{$iodev->{TYPE}}{Clients}
  53. if(!$hash->{Clients});
  54. }
  55. my $dev = $a[2];
  56. if($dev =~ m/^(.*):SSL$/) {
  57. $dev = $1;
  58. $hash->{SSL} = 1;
  59. }
  60. if($dev !~ m/^.+:[0-9]+$/) { # host:port
  61. $dev = "$dev:7072";
  62. $hash->{Host} = $dev;
  63. }
  64. if($hash->{OLDDEF} && $hash->{OLDDEF} =~ m/^([^ \t]+)/) {; # Forum #30242
  65. delete($readyfnlist{"$hash->{NAME}.$1"});
  66. }
  67. $hash->{Host} = $dev;
  68. $hash->{portpassword} = $a[4] if(@a == 5);
  69. FHEM2FHEM_CloseDev($hash); # Modify...
  70. return FHEM2FHEM_OpenDev($hash, 0);
  71. }
  72. #####################################
  73. sub
  74. FHEM2FHEM_Undef($$)
  75. {
  76. my ($hash, $arg) = @_;
  77. FHEM2FHEM_CloseDev($hash);
  78. return undef;
  79. }
  80. sub
  81. FHEM2FHEM_Write($$)
  82. {
  83. my ($hash,$fn,$msg) = @_;
  84. my $dev = $hash->{Host};
  85. if(!$hash->{TCPDev2}) {
  86. my $conn;
  87. if($hash->{SSL}) {
  88. $conn = IO::Socket::SSL->new(PeerAddr => $dev);
  89. } else {
  90. $conn = IO::Socket::INET->new(PeerAddr => $dev);
  91. }
  92. return if(!$conn); # Hopefuly it is reported elsewhere
  93. $hash->{TCPDev2} = $conn;
  94. syswrite($hash->{TCPDev2}, $hash->{portpassword} . "\n")
  95. if($hash->{portpassword});
  96. }
  97. my $rdev = $hash->{rawDevice};
  98. syswrite($hash->{TCPDev2}, "iowrite $rdev $fn $msg\n");
  99. }
  100. #####################################
  101. # called from the global loop, when the select for hash->{FD} reports data
  102. sub
  103. FHEM2FHEM_Read($)
  104. {
  105. my ($hash) = @_;
  106. my $buf = FHEM2FHEM_SimpleRead($hash);
  107. my $name = $hash->{NAME};
  108. ###########
  109. # Lets' try again: Some drivers return len(0) on the first read...
  110. if(defined($buf) && length($buf) == 0) {
  111. $buf = FHEM2FHEM_SimpleRead($hash);
  112. }
  113. if(!defined($buf) || length($buf) == 0) {
  114. FHEM2FHEM_Disconnected($hash);
  115. return;
  116. }
  117. return if(IsDisabled($name));
  118. my $excl = AttrVal($name, "excludeEvents", undef);
  119. my $data = $hash->{PARTIAL};
  120. #Log3 $hash, 5, "FHEM2FHEM/RAW: $data/$buf";
  121. $data .= $buf;
  122. while($data =~ m/\n/) {
  123. my $rmsg;
  124. ($rmsg,$data) = split("\n", $data, 2);
  125. $rmsg =~ s/\r//;
  126. if($hash->{informType} eq "LOG") {
  127. my ($type, $rname, $msg) = split(" ", $rmsg, 3);
  128. next if(!defined($msg)); # Bogus data
  129. my $re = $hash->{regexp};
  130. next if($re && !($rname =~ m/^$re$/ || "$rname:$msg" =~ m/^$re$/));
  131. next if($excl && ($rname =~ m/^$excl$/ || "$rname:$msg" =~ m/^$excl$/));
  132. Log3 $name, 4, "$rname: $rmsg";
  133. if(!$defs{$rname}) {
  134. $defs{$rname}{NAME} = $rname;
  135. $defs{$rname}{TYPE} = $type;
  136. $defs{$rname}{STATE} = $msg;
  137. $defs{$rname}{FAKEDEVICE} = 1; # Avoid set/attr/delete/etc in notify
  138. $defs{$rname}{TEMPORARY} = 1; # Do not save it
  139. DoTrigger($rname, $msg);
  140. delete($defs{$rname});
  141. } else {
  142. if(AttrVal($name,"eventOnly",0)) {
  143. DoTrigger($rname, $msg);
  144. } else {
  145. if($msg =~ m/^([^:]*): (.*)$/) {
  146. readingsSingleUpdate($defs{$rname}, $1, $2, 1);
  147. } else {
  148. readingsSingleUpdate($defs{$rname}, "state", $msg, 1);
  149. }
  150. }
  151. }
  152. } else { # RAW
  153. my ($type, $rname, $msg) = split(" ", $rmsg, 3);
  154. my $rdev = $hash->{rawDevice};
  155. next if($rname ne $rdev);
  156. Log3 $name, 4, "$name: $rmsg";
  157. Dispatch($defs{$rdev}, $msg, undef);
  158. }
  159. }
  160. $hash->{PARTIAL} = $data;
  161. }
  162. #####################################
  163. sub
  164. FHEM2FHEM_Ready($)
  165. {
  166. my ($hash) = @_;
  167. return FHEM2FHEM_OpenDev($hash, 1);
  168. }
  169. ########################
  170. sub
  171. FHEM2FHEM_CloseDev($)
  172. {
  173. my ($hash) = @_;
  174. my $name = $hash->{NAME};
  175. my $dev = $hash->{Host};
  176. return if(!$dev);
  177. $hash->{TCPDev}->close() if($hash->{TCPDev});
  178. $hash->{TCPDev2}->close() if($hash->{TCPDev2});
  179. delete($hash->{NEXT_OPEN});
  180. delete($hash->{TCPDev});
  181. delete($hash->{TCPDev2});
  182. delete($selectlist{"$name.$dev"});
  183. delete($readyfnlist{"$name.$dev"});
  184. delete($hash->{FD});
  185. }
  186. ########################
  187. sub
  188. FHEM2FHEM_OpenDev($$)
  189. {
  190. my ($hash, $reopen) = @_;
  191. my $dev = $hash->{Host};
  192. my $name = $hash->{NAME};
  193. $hash->{PARTIAL} = "";
  194. Log3 $name, 3, "FHEM2FHEM opening $name at $dev"
  195. if(!$reopen);
  196. return if($hash->{NEXT_OPEN} && time() <= $hash->{NEXT_OPEN});
  197. return if(IsDisabled($name));
  198. my $doTailWork = sub($$$) {
  199. my ($h, $err, undef) = @_;
  200. if($err) {
  201. Log3($name, 3, "Can't connect to $dev: $!") if(!$reopen);
  202. $readyfnlist{"$name.$dev"} = $hash;
  203. $hash->{STATE} = "disconnected";
  204. $hash->{NEXT_OPEN} = time()+60;
  205. return;
  206. }
  207. my $conn = $h->{conn};
  208. delete($hash->{NEXT_OPEN});
  209. $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1);
  210. $hash->{TCPDev} = $conn;
  211. $hash->{FD} = $conn->fileno();
  212. delete($readyfnlist{"$name.$dev"});
  213. $selectlist{"$name.$dev"} = $hash;
  214. if($reopen) {
  215. Log3 $name, 1, "FHEM2FHEM $dev reappeared ($name)";
  216. } else {
  217. Log3 $name, 3, "FHEM2FHEM device opened ($name)";
  218. }
  219. $hash->{STATE}= "connected";
  220. DoTrigger($name, "CONNECTED") if($reopen);
  221. syswrite($hash->{TCPDev}, $hash->{portpassword} . "\n")
  222. if($hash->{portpassword});
  223. my $msg = $hash->{informType} eq "LOG" ?
  224. "inform on $hash->{regexp}" : "inform raw";
  225. syswrite($hash->{TCPDev}, $msg . "\n");
  226. };
  227. return HttpUtils_Connect({ # Nonblocking
  228. url => $hash->{SSL} ? "https://$dev/" : "http://$dev/",
  229. NAME => $name,
  230. noConn2 => 1,
  231. callback=> $doTailWork
  232. });
  233. }
  234. sub
  235. FHEM2FHEM_Disconnected($)
  236. {
  237. my $hash = shift;
  238. my $dev = $hash->{Host};
  239. my $name = $hash->{NAME};
  240. return if(!defined($hash->{FD})); # Already deleted
  241. Log3 $name, 1, "$dev disconnected, waiting to reappear";
  242. FHEM2FHEM_CloseDev($hash);
  243. $readyfnlist{"$name.$dev"} = $hash; # Start polling
  244. $hash->{STATE} = "disconnected";
  245. return if(IsDisabled($name)); #Forum #39386
  246. # Without the following sleep the open of the device causes a SIGSEGV,
  247. # and following opens block infinitely. Only a reboot helps.
  248. sleep(5);
  249. DoTrigger($name, "DISCONNECTED");
  250. }
  251. ########################
  252. sub
  253. FHEM2FHEM_SimpleRead($)
  254. {
  255. my ($hash) = @_;
  256. my $buf;
  257. if(!defined(sysread($hash->{TCPDev}, $buf, 256))) {
  258. FHEM2FHEM_Disconnected($hash);
  259. return undef;
  260. }
  261. return $buf;
  262. }
  263. sub
  264. FHEM2FHEM_Set($@)
  265. {
  266. my ($hash, @a) = @_;
  267. return "set needs at least one parameter" if(@a < 2);
  268. return "Unknown argument $a[1], choose one of reopen:noArg"
  269. if($a[1] ne "reopen");
  270. FHEM2FHEM_CloseDev($hash);
  271. FHEM2FHEM_OpenDev($hash, 0);
  272. return undef;
  273. }
  274. 1;
  275. =pod
  276. =item helper
  277. =item summary connect two FHEM instances
  278. =item summary_DE verbindet zwei FHEM Installationen
  279. =begin html
  280. <a name="FHEM2FHEM"></a>
  281. <h3>FHEM2FHEM</h3>
  282. <ul>
  283. FHEM2FHEM is a helper module to connect separate FHEM installations.
  284. <br><br>
  285. <a name="FHEM2FHEMdefine"></a>
  286. <b>Define</b>
  287. <ul>
  288. <code>define &lt;name&gt; FHEM2FHEM &lt;host&gt;[:&lt;portnr&gt;][:SSL]
  289. [LOG:regexp|RAW:devicename] {portpassword}
  290. </code>
  291. <br>
  292. <br>
  293. Connect to the <i>remote</i> FHEM on &lt;host&gt;. &lt;portnr&gt; is a telnet
  294. port on the remote FHEM, defaults to 7072. The optional :SSL suffix is
  295. needed, if the remote FHEM configured SSL for this telnet port. In this case
  296. the IO::Socket::SSL perl module must be installed for the local host too.<br>
  297. Note: if the remote FHEM is on a separate host, the telnet port on the remote
  298. FHEM musst be specified with the global option.<br>
  299. The next parameter specifies the connection
  300. type:
  301. <ul>
  302. <li>LOG<br>
  303. Using this type you will receive all events generated by the remote FHEM,
  304. just like when using the <a href="#inform">inform on</a> command, and you
  305. can use these events just like any local event for <a
  306. href="#FileLog">FileLog </a> or <a href="#notify">notify</a>.
  307. The regexp will prefilter the events distributed locally, for the syntax
  308. see the notify definition.<br>
  309. Drawbacks: the remote devices wont be created locally, so list wont
  310. show them and it is not possible to manipulate them from the local
  311. FHEM. It is possible to create a device with the same name on both FHEM
  312. instances, but if both of them receive the same event (e.g. because both
  313. of them have a CUL attached), then all associated FileLogs/notifys will be
  314. triggered twice.<br>
  315. If the remote device is created with the same name locally (e.g. as dummy),
  316. then the local readings are also updated.
  317. </li>
  318. <li>RAW<br>
  319. By using this type the local FHEM will receive raw events from the remote
  320. FHEM device <i>devicename</i>, just like if it would be attached to the
  321. local FHEM.
  322. Drawback: only devices using the Dispatch function (CUL, FHZ, CM11,
  323. SISPM, RFXCOM, TCM, TRX, TUL) generate raw messages, and you must create a
  324. FHEM2FHEM instance for each remote device.<br>
  325. <i>devicename</i> must exist on the local
  326. FHEM server too with the same name and same type as the remote device, but
  327. with the device-node "none", so it is only a dummy device.
  328. All necessary attributes (e.g. <a href="#rfmode">rfmode</a> if the remote
  329. CUL is in HomeMatic mode) must also be set for the local device.
  330. Do not reuse a real local device, else duplicate filtering (see dupTimeout)
  331. won't work correctly.
  332. </li>
  333. </ul>
  334. The last parameter specifies an optional portpassword, if the remote server
  335. activated <a href="#portpassword">portpassword</a>.
  336. <br>
  337. Examples:
  338. <ul>
  339. <code>define ds1 FHEM2FHEM 192.168.178.22:7072 LOG:.*</code><br>
  340. <br>
  341. <code>define RpiCUL CUL none 0000</code><br>
  342. <code>define ds2 FHEM2FHEM 192.168.178.22:7072 RAW:RpiCUL</code><br>
  343. and on the RPi (192.168.178.22):<br>
  344. <code>rename CUL_0 RpiCUL</code><br>
  345. </ul>
  346. </ul>
  347. <br>
  348. <a name="FHEM2FHEMset"></a>
  349. <b>Set </b>
  350. <ul>
  351. <li>reopen<br>
  352. Reopens the connection to the device and reinitializes it.</li><br>
  353. </ul>
  354. <a name="FHEM2FHEMget"></a>
  355. <b>Get</b> <ul>N/A</ul><br>
  356. <a name="FHEM2FHEMattr"></a>
  357. <b>Attributes</b>
  358. <ul>
  359. <li><a href="#dummy">dummy</a></li>
  360. <li><a href="#disable">disable</a></li>
  361. <li><a href="#disabledForIntervals">disabledForIntervals</a></li>
  362. <li><a name="#eventOnly">eventOnly</a><br>
  363. if set, generate only events, do not set corresponding readings.
  364. This is a compatibility feature, available only for LOG-Mode.
  365. </li>
  366. <li><a name="#excludeEvents">excludeEvents &lt;regexp&gt;</a>
  367. do not publish events matching &lt;regexp&gt;
  368. </li>
  369. </ul>
  370. </ul>
  371. =end html
  372. =begin html_DE
  373. <a name="FHEM2FHEM"></a>
  374. <h3>FHEM2FHEM</h3>
  375. <ul>
  376. FHEM2FHEM ist ein Hilfsmodul, um mehrere FHEM-Installationen zu verbinden.
  377. <br><br>
  378. <a name="FHEM2FHEMdefine"></a>
  379. <b>Define</b>
  380. <ul>
  381. <code>define &lt;name&gt; FHEM2FHEM &lt;host&gt;[:&lt;portnr&gt;][:SSL] [LOG:regexp|RAW:devicename] {portpassword}
  382. </code>
  383. <br>
  384. <br>
  385. Zum <i>remote (entfernten)</i> FHEM auf Rechner &lt;host&gt; verbinden.
  386. &lt;portnr&gt; ist der telnetPort des remote FHEM, Standardport ist 7072.
  387. Der Zusatz :SSL wird ben&ouml;tigt, wenn das remote FHEM
  388. SSL-Verschl&uuml;sselung voraussetzt. Auch auf dem lokalen Host muss dann
  389. das Perl-Modul IO::Socket::SSL installiert sein.<br>
  390. Anmerkung: Wenn das remote FHEM auf einem eigenen Host l&auml;uft, muss
  391. "telnetPort" des remote FHEM als global festgelegt sein. <br>
  392. Der n&auml;chste Parameter spezifiziert den Verbindungs-Typ:
  393. <ul>
  394. <li>LOG<br>
  395. Bei Verwendung dieses Verbindungstyps werden alle Ereignisse (Events) der
  396. remote FHEM-Installation empfangen. Die Ereignisse sehen aus wie die, die
  397. nach <a href="#inform">inform on</a> Befehl erzeugt werden. Sie k&ouml;nnen
  398. wie lokale Ereignisse durch <a href="#FileLog">FileLog </a> oder <a
  399. href="#notify">notify</a> genutzt werden und mit einem regul&auml;ren
  400. Ausdruck gefiltert werden. Die Syntax daf&uuml;r ist unter der
  401. notify-Definition beschrieben.<br>
  402. Einschr&auml;nkungen: die Ger&auml;te der remote Installation werden nicht
  403. lokal angelegt und k&ouml;nnen weder mit list angezeigt noch lokal
  404. angesprochen werden. Auf beiden FHEM-Installationen k&ouml;nnen
  405. Ger&auml;te gleichen Namens angelegt werden, aber wenn beide dasselbe
  406. Ereignis empfangen (z.B. wenn an beiden Installationen CULs angeschlossen
  407. sind), werden alle FileLogs und notifys doppelt ausgel&ouml;st.<br>
  408. Falls man lokal Ger&auml;te mit dem gleichen Namen (z.Bsp. als dummy)
  409. angelegt hat, dann werden die Readings von dem lokalen Ger&auml;t
  410. aktualisiert.
  411. </li>
  412. <li>RAW<br>
  413. Bei diesem Verbindungstyp werden unaufbereitete Ereignisse (raw messages)
  414. des remote FHEM-Ger&auml;ts <i>devicename</i> genau so empfangen, als
  415. w&auml;re das Ger&auml;t lokal verbunden.<br>
  416. Einschr&auml;nkungen: nur Ger&auml;te, welche die "Dispatch-Funktion"
  417. unterst&uuml;tzen (CUL, FHZ, CM11, SISPM, RFXCOM, TCM, TRX, TUL) erzeugen
  418. raw messages, und f&uuml;r jedes entfernte Ger&auml;t muss ein eigenes
  419. FHEM2FHEM Objekt erzeugt werden.<br>
  420. <i>devicename</i> muss mit demselben Namen und Typ wie das Remote Devive
  421. angelegt sein, aber als Dummy, d.h. als device-node "none".
  422. Zus&auml;tzlich m&uuml;ssen alle notwendigen Attribute lokal gesetzt sein
  423. (z.B. <a href="#rfmode">rfmode</a>, wenn die remote CUL im HomeMatic-Modus
  424. l&auml;uft). Die Verwendung bereits bestehender lokaler Ger&auml;te ist zu
  425. vermeiden, weil sonst die Duplikatsfilterung nicht richtig funktioniert
  426. (siehe dupTimeout). </li>
  427. </ul>
  428. Der letzte Parameter enth&auml;lt das Passwort des Remote-Servers, wenn dort
  429. eines aktiviert ist <a href="#portpassword">portpassword</a>.
  430. <br>
  431. Beispiele:
  432. <ul>
  433. <code>define ds1 FHEM2FHEM 192.168.178.22:7072 LOG:.*</code><br>
  434. <br>
  435. <code>define RpiCUL CUL none 0000</code><br>
  436. <code>define ds2 FHEM2FHEM 192.168.178.22:7072 RAW:RpiCUL</code><br> und auf dem RPi (192.168.178.22):<br>
  437. <code>rename CUL_0 RpiCUL</code><br>
  438. </ul>
  439. </ul>
  440. <br>
  441. <a name="FHEM2FHEMset"></a>
  442. <b>Set </b>
  443. <ul>
  444. <li>reopen<br>
  445. &Ouml;ffnet die Verbindung erneut.</li>
  446. </ul>
  447. <a name="FHEM2FHEMget"></a>
  448. <b>Get</b> <ul>N/A</ul><br>
  449. <a name="FHEM2FHEMattr"></a>
  450. <b>Attribute</b>
  451. <ul>
  452. <li><a href="#dummy">dummy</a></li>
  453. <li><a href="#disable">disable</a></li>
  454. <li><a href="#disabledForIntervals">disabledForIntervals</a></li>
  455. <li><a name="#eventOnly">eventOnly</a><br>
  456. falls gesetzt, werden nur die Events generiert, und es wird kein
  457. Reading aktualisiert. Ist nur im LOG-Mode aktiv.
  458. </li>
  459. <li><a name="#excludeEvents">excludeEvents &lt;regexp&gt;</a>
  460. die auf das &lt;regexp&gt; zutreffende Events werden nicht
  461. bereitgestellt.
  462. </li>
  463. </ul>
  464. </ul>
  465. =end html_DE
  466. =cut