TradfriIo.pm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. ##############################################
  2. # $Id$
  3. package main;
  4. sub DevIo_CloseDev($@);
  5. sub DevIo_Disconnected($);
  6. sub DevIo_OpenDev($$$;$);
  7. sub DevIo_SimpleRead($);
  8. sub DevIo_SimpleWrite($$$;$);
  9. sub
  10. DevIo_setStates($$)
  11. {
  12. my ($hash, $val) = @_;
  13. $hash->{STATE} = $val;
  14. setReadingsVal($hash, "state", $val, TimeNow());
  15. }
  16. ########################
  17. # Try to read once from the device.
  18. # "private" function
  19. sub
  20. DevIo_DoSimpleRead($)
  21. {
  22. my ($hash) = @_;
  23. my ($buf, $res);
  24. if($hash->{TCPDev}) {
  25. $res = sysread($hash->{TCPDev}, $buf, 4096);
  26. $buf = "" if(!defined($res));
  27. }
  28. return $buf;
  29. }
  30. ########################
  31. # This is the function to read data, to be called in ReadFn.
  32. # If there is no data, sets the device to disconnected, which results in
  33. # polling via ReadyFn, trying to open it.
  34. sub
  35. DevIo_SimpleRead($)
  36. {
  37. my ($hash) = @_;
  38. my $buf = DevIo_DoSimpleRead($hash);
  39. ###########
  40. # Lets' try again: Some drivers return len(0) on the first read...
  41. if(defined($buf) && length($buf) == 0) {
  42. $buf = DevIo_SimpleReadWithTimeout($hash, 0.01); # Forum #57806
  43. }
  44. if(!defined($buf) || length($buf) == 0) {
  45. DevIo_Disconnected($hash);
  46. return undef;
  47. }
  48. return $buf;
  49. }
  50. ########################
  51. # wait at most timeout seconds until the file handle gets ready
  52. # for reading; returns undef on timeout
  53. # NOTE1: FHEM can be blocked for $timeout seconds, DO NOT USE IT!
  54. # NOTE2: This works on Windows only for TCP connections
  55. sub
  56. DevIo_SimpleReadWithTimeout($$)
  57. {
  58. my ($hash, $timeout) = @_;
  59. my $rin = "";
  60. vec($rin, $hash->{FD}, 1) = 1;
  61. my $nfound = select($rin, undef, undef, $timeout);
  62. return DevIo_DoSimpleRead($hash) if($nfound> 0);
  63. return undef;
  64. }
  65. ########################
  66. # Function to write data
  67. sub
  68. DevIo_SimpleWrite($$$;$)
  69. {
  70. my ($hash, $msg, $type, $addnl) = @_; # Type: 0:binary, 1:hex, 2:ASCII
  71. return if(!$hash);
  72. my $name = $hash->{NAME};
  73. Log3 ($name, 5, $type ? "SW: $msg" : "SW: ".unpack("H*",$msg));
  74. $msg = pack('H*', $msg) if($type && $type == 1);
  75. $msg .= "\n" if($addnl);
  76. if($hash->{TCPDev}) {
  77. syswrite($hash->{TCPDev}, $msg);
  78. }
  79. select(undef, undef, undef, 0.001);
  80. }
  81. ########################
  82. # Open a device for reading/writing data.
  83. # Possible values for $hash->{DeviceName}:
  84. # - device@baud[78][NEO][012] => open device, set serial-line parameters
  85. # - hostname:port => TCP/IP client connection
  86. # - device@directio => open device without additional "magic"
  87. # - UNIX:(SEQPACKET|STREAM):filename => Open filename as a UNIX socket
  88. # - FHEM:DEVIO:IoDev[:IoPort] => Cascade I/O over another FHEM Device
  89. #
  90. # callback is only meaningful for TCP/IP (in which case a nonblocking connect
  91. # is executed) every cases. It will be called with $hash and a (potential)
  92. # error message. If $hash->{SSL} is set, SSL encryption is activated.
  93. sub
  94. DevIo_OpenDev($$$;$)
  95. {
  96. my ($hash, $reopen, $initfn, $callback) = @_;
  97. my $dev = $hash->{DeviceName};
  98. my $name = $hash->{NAME};
  99. my $po;
  100. my $baudrate;
  101. ($dev, $baudrate) = split("@", $dev);
  102. my ($databits, $parity, $stopbits) = (8, 'none', 1);
  103. my $nextOpenDelay = ($hash->{nextOpenDelay} ? $hash->{nextOpenDelay} : 60);
  104. # Call the callback if specified, simply return in other cases
  105. my $doCb = sub ($) {
  106. my ($r) = @_;
  107. Log3 $name, 1, "$name: Can't connect to $dev: $r" if(!$reopen && $r);
  108. $callback->($hash,$r) if($callback);
  109. return $r;
  110. };
  111. # Call initFn
  112. # if fails: disconnect, schedule the next polltime for reopen
  113. # if ok: log message, trigger CONNECTED on reopen
  114. my $doTailWork = sub {
  115. DevIo_setStates($hash, "opened");
  116. my $ret;
  117. if($initfn) {
  118. my $hadFD = defined($hash->{FD});
  119. $ret = &$initfn($hash);
  120. if($ret) {
  121. if($hadFD && !defined($hash->{FD})) { # Forum #54732 / ser2net
  122. DevIo_Disconnected($hash);
  123. $hash->{NEXT_OPEN} = time() + $nextOpenDelay;
  124. } else {
  125. DevIo_CloseDev($hash);
  126. Log3 $name, 1, "Cannot init $dev, ignoring it ($name)";
  127. }
  128. }
  129. }
  130. if(!$ret) {
  131. my $l = $hash->{devioLoglevel}; # Forum #61970
  132. if($reopen) {
  133. Log3 $name, ($l ? $l:1), "$dev reappeared ($name)";
  134. } else {
  135. Log3 $name, ($l ? $l:3), "$name device opened" if(!$hash->{DevioText});
  136. }
  137. }
  138. DoTrigger($name, "CONNECTED") if($reopen && !$ret);
  139. return undef;
  140. };
  141. if($baudrate =~ m/(\d+)(,([78])(,([NEO])(,([012]))?)?)?/) {
  142. $baudrate = $1 if(defined($1));
  143. $databits = $3 if(defined($3));
  144. $parity = 'odd' if(defined($5) && $5 eq 'O');
  145. $parity = 'even' if(defined($5) && $5 eq 'E');
  146. $stopbits = $7 if(defined($7));
  147. }
  148. if($hash->{DevIoJustClosed}) {
  149. delete $hash->{DevIoJustClosed};
  150. return &$doCb(undef);
  151. }
  152. $hash->{PARTIAL} = "";
  153. Log3 $name, 3, ($hash->{DevioText} ? $hash->{DevioText} : "Opening").
  154. " $name device $dev" if(!$reopen);
  155. if($dev =~ m/^UNIX:(SEQPACKET|STREAM):(.*)$/) { # FBAHA
  156. my ($type, $fname) = ($1, $2);
  157. my $conn;
  158. eval {
  159. require IO::Socket::UNIX;
  160. $conn = IO::Socket::UNIX->new(
  161. Type=>($type eq "STREAM" ? SOCK_STREAM:SOCK_SEQPACKET), Peer=>$fname);
  162. };
  163. if($@) {
  164. Log3 $name, 1, $@;
  165. return &$doCb($@);
  166. }
  167. if(!$conn) {
  168. Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen);
  169. $readyfnlist{"$name.$dev"} = $hash;
  170. DevIo_setStates($hash, "disconnected");
  171. return &$doCb("");
  172. }
  173. $hash->{TCPDev} = $conn;
  174. $hash->{FD} = $conn->fileno();
  175. delete($readyfnlist{"$name.$dev"});
  176. $selectlist{"$name.$dev"} = $hash;
  177. } elsif($dev =~ m/^FHEM:DEVIO:(.*)(:(.*))/) { # Forum #46276
  178. my ($devName, $devPort) = ($1, $3);
  179. AssignIoPort($hash, $devName);
  180. if (defined($hash->{IODev})) {
  181. ($dev, $baudrate) = split("@", $hash->{DeviceName});
  182. $hash->{IODevPort} = $devPort if (defined($devPort));
  183. $hash->{IODevParameters} = $baudrate if (defined($baudrate));
  184. if (!CallFn($devName, "IOOpenFn", $hash)) {
  185. Log3 $name, 1, "$name: Can't open $dev!";
  186. DevIo_setStates($hash, "disconnected");
  187. return &$doCb("");
  188. }
  189. } else {
  190. DevIo_setStates($hash, "disconnected");
  191. return &$doCb("");
  192. }
  193. } elsif($dev =~ m/^(.+):([0-9]+)$/) { # host:port
  194. # This part is called every time the timeout (5sec) is expired _OR_
  195. # somebody is communicating over another TCP connection. As the connect
  196. # for non-existent devices has a delay of 3 sec, we are sitting all the
  197. # time in this connect. NEXT_OPEN tries to avoid this problem.
  198. if($hash->{NEXT_OPEN} && time() < $hash->{NEXT_OPEN}) {
  199. return &$doCb(undef); # Forum 53309
  200. }
  201. delete($readyfnlist{"$name.$dev"});
  202. my $timeout = $hash->{TIMEOUT} ? $hash->{TIMEOUT} : 3;
  203. # Do common TCP/IP "afterwork":
  204. # if connected: set keepalive, fill selectlist, FD, TCPDev.
  205. # if not: report the error and schedule reconnect
  206. my $doTcpTail = sub($) {
  207. my ($conn) = @_;
  208. if($conn) {
  209. delete($hash->{NEXT_OPEN});
  210. $conn->setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1) if(defined($conn));
  211. } else {
  212. Log3 $name, 1, "$name: Can't connect to $dev: $!" if(!$reopen && $!);
  213. $readyfnlist{"$name.$dev"} = $hash;
  214. DevIo_setStates($hash, "disconnected");
  215. $hash->{NEXT_OPEN} = time() + $nextOpenDelay;
  216. return 0;
  217. }
  218. $hash->{TCPDev} = $conn;
  219. $hash->{FD} = $conn->fileno();
  220. $selectlist{"$name.$dev"} = $hash;
  221. return 1;
  222. };
  223. if($callback) { # reuse the nonblocking connect from HttpUtils.
  224. use HttpUtils;
  225. my $err = HttpUtils_Connect({ # Nonblocking
  226. timeout => $timeout,
  227. url => $hash->{SSL} ? "https://$dev/" : "http://$dev/",
  228. NAME => $hash->{NAME},
  229. noConn2 => 1,
  230. callback=> sub() {
  231. my ($h, $err, undef) = @_;
  232. &$doTcpTail($err ? undef : $h->{conn});
  233. return &$doCb($err ? $err : &$doTailWork());
  234. }
  235. });
  236. return &$doCb($err) if($err);
  237. return undef; # no double callback: connect is running in bg now
  238. } else { # blocking connect
  239. my $conn = $haveInet6 ?
  240. IO::Socket::INET6->new(PeerAddr => $dev, Timeout => $timeout) :
  241. IO::Socket::INET ->new(PeerAddr => $dev, Timeout => $timeout);
  242. return "" if(!&$doTcpTail($conn)); # no callback: no doCb
  243. }
  244. } elsif($baudrate && lc($baudrate) eq "directio") { # w/o Device::SerialPort
  245. if(!open($po, "+<$dev")) {
  246. return &$doCb(undef) if($reopen);
  247. Log3 $name, 1, "$name: Can't open $dev: $!";
  248. $readyfnlist{"$name.$dev"} = $hash;
  249. DevIo_setStates($hash, "disconnected");
  250. return &$doCb("");
  251. }
  252. $hash->{DIODev} = $po;
  253. if( $^O =~ /Win/ ) {
  254. $readyfnlist{"$name.$dev"} = $hash;
  255. } else {
  256. $hash->{FD} = fileno($po);
  257. delete($readyfnlist{"$name.$dev"});
  258. $selectlist{"$name.$dev"} = $hash;
  259. }
  260. } else { # USB/Serial device
  261. if ($^O=~/Win/) {
  262. eval {
  263. require Win32::SerialPort;
  264. $po = new Win32::SerialPort ($dev);
  265. }
  266. } else {
  267. eval {
  268. require Device::SerialPort;
  269. $po = new Device::SerialPort ($dev);
  270. }
  271. }
  272. if($@) {
  273. Log3 $name, 1, $@;
  274. return &$doCb($@);
  275. }
  276. if(!$po) {
  277. return &$doCb(undef) if($reopen);
  278. Log3 $name, 1, "$name: Can't open $dev: $!";
  279. $readyfnlist{"$name.$dev"} = $hash;
  280. DevIo_setStates($hash, "disconnected");
  281. return &$doCb("");
  282. }
  283. $hash->{USBDev} = $po;
  284. if( $^O =~ /Win/ ) {
  285. $readyfnlist{"$name.$dev"} = $hash;
  286. } else {
  287. $hash->{FD} = $po->FILENO;
  288. delete($readyfnlist{"$name.$dev"});
  289. $selectlist{"$name.$dev"} = $hash;
  290. }
  291. if($baudrate) {
  292. $po->reset_error();
  293. my $p = ($parity eq "none" ? "N" : ($parity eq "odd" ? "O" : "E"));
  294. Log3 $name, 3, "Setting $name serial parameters to ".
  295. "$baudrate,$databits,$p,$stopbits" if(!$hash->{DevioText});
  296. $po->baudrate($baudrate);
  297. $po->databits($databits);
  298. $po->parity($parity);
  299. $po->stopbits($stopbits);
  300. $po->handshake('none');
  301. # This part is for some Linux kernel versions whih has strange default
  302. # settings. Device::SerialPort is nice: if the flag is not defined for
  303. # your OS then it will be ignored.
  304. $po->stty_icanon(0);
  305. #$po->stty_parmrk(0); # The debian standard install does not have it
  306. $po->stty_icrnl(0);
  307. $po->stty_echoe(0);
  308. $po->stty_echok(0);
  309. $po->stty_echoctl(0);
  310. # Needed for some strange distros
  311. $po->stty_echo(0);
  312. $po->stty_icanon(0);
  313. $po->stty_isig(0);
  314. $po->stty_opost(0);
  315. $po->stty_icrnl(0);
  316. }
  317. $po->write_settings;
  318. }
  319. return &$doCb(&$doTailWork());
  320. }
  321. ########################
  322. # close the device, remove it from selectlist,
  323. # delete DevIo specific internals from $hash
  324. sub
  325. DevIo_CloseDev($@)
  326. {
  327. my ($hash,$isFork) = @_;
  328. my $name = $hash->{NAME};
  329. my $dev = $hash->{DeviceName};
  330. return if(!$dev);
  331. if($hash->{TCPDev}) {
  332. $hash->{TCPDev}->close();
  333. delete($hash->{TCPDev});
  334. }
  335. ($dev, undef) = split("@", $dev); # Remove the baudrate
  336. delete($selectlist{"$name.$dev"});
  337. delete($readyfnlist{"$name.$dev"});
  338. delete($hash->{FD});
  339. delete($hash->{EXCEPT_FD});
  340. delete($hash->{PARTIAL});
  341. delete($hash->{NEXT_OPEN});
  342. }
  343. sub
  344. DevIo_IsOpen($)
  345. {
  346. my ($hash) = @_;
  347. return ($hash->{TCPDev});
  348. }
  349. # Close the device, schedule the reopen via ReadyFn, trigger DISCONNECTED
  350. sub
  351. DevIo_Disconnected($)
  352. {
  353. my $hash = shift;
  354. my $dev = $hash->{DeviceName};
  355. my $name = $hash->{NAME};
  356. my $baudrate;
  357. ($dev, $baudrate) = split("@", $dev);
  358. return if(!defined($hash->{FD})); # Already deleted or RFR
  359. my $l = $hash->{devioLoglevel}; # Forum #61970
  360. Log3 $name, ($l ? $l:1), "$dev disconnected, waiting to reappear ($name)";
  361. DevIo_CloseDev($hash);
  362. $readyfnlist{"$name.$dev"} = $hash; # Start polling
  363. DevIo_setStates($hash, "disconnected");
  364. $hash->{DevIoJustClosed} = 1; # Avoid a direct reopen
  365. DoTrigger($name, "DISCONNECTED");
  366. }
  367. 1;