37_fakeRoku.pm 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. # $Id: 37_fakeRoku.pm 16381 2018-03-11 09:08:02Z justme1968 $
  2. package main;
  3. use strict;
  4. use warnings;
  5. use Sys::Hostname;
  6. use IO::Socket::INET;
  7. #use Net::Address::IP::Local;
  8. use Encode qw(encode);
  9. use XML::Simple qw(:strict);
  10. use Digest::MD5 qw(md5_hex);
  11. use HttpUtils;
  12. use Time::Local;
  13. use Data::Dumper;
  14. my $fakeRoku_hasMulticast = 1;
  15. sub
  16. fakeRoku_Initialize($)
  17. {
  18. my ($hash) = @_;
  19. eval "use IO::Socket::Multicast;";
  20. $fakeRoku_hasMulticast = 0 if($@);
  21. $hash->{ReadFn} = "fakeRoku_Read";
  22. $hash->{DefFn} = "fakeRoku_Define";
  23. $hash->{NOTIFYDEV} = "global";
  24. $hash->{NotifyFn} = "fakeRoku_Notify";
  25. $hash->{UndefFn} = "fakeRoku_Undefine";
  26. #$hash->{SetFn} = "fakeRoku_Set";
  27. #$hash->{GetFn} = "fakeRoku_Get";
  28. $hash->{AttrFn} = "fakeRoku_Attr";
  29. $hash->{AttrList} = "disable:1,0 favourites fhemIP httpPort reusePort:1,0 serial";
  30. }
  31. #####################################
  32. sub
  33. fakeRoku_getLocalIP()
  34. {
  35. my $socket = IO::Socket::INET->new(
  36. Proto => 'udp',
  37. PeerAddr => '8.8.8.8:53', # google dns
  38. #PeerAddr => '198.41.0.4:53', # a.root-servers.net
  39. );
  40. return '<unknown>' if( !$socket );
  41. my $ip = $socket->sockhost;
  42. close( $socket );
  43. return $ip if( $ip );
  44. #$ip = inet_ntoa( scalar gethostbyname( hostname() || 'localhost' ) );
  45. #return $ip if( $ip );
  46. return '<unknown>';
  47. }
  48. sub
  49. fakeRoku_Define($$)
  50. {
  51. my ($hash, $def) = @_;
  52. my @a = split("[ \t][ \t]*", $def);
  53. return "Usage: define <name> fakeRoku" if(@a < 2);
  54. my $name = $a[0];
  55. my $id = $a[2];
  56. $hash->{NAME} = $name;
  57. $hash->{ID} = $id?$id:'';
  58. my $defptr = $modules{fakeRoku}{defptr}{$hash->{ID}?$hash->{ID}:'MASTER'};
  59. return "fakeRoku $hash->{ID} already defined as '$defptr->{NAME}'" if( defined($defptr) && $defptr->{NAME} ne $name);
  60. $modules{fakeRoku}{defptr}{$hash->{ID}?$hash->{ID}:'MASTER'} = $hash;
  61. return "install IO::Socket::Multicast to use autodiscovery" if(!$fakeRoku_hasMulticast);
  62. $hash->{"HAS_IO::Socket::Multicast"} = $fakeRoku_hasMulticast;
  63. $hash->{helper}{serial} = md5_hex(getUniqueId());
  64. $hash->{helper}{serial} .= "-$hash->{ID}" if( $hash->{ID} );
  65. $attr{$name}{serial} = $hash->{helper}{serial} if( !defined($attr{$name}{serial}) );
  66. $hash->{fhemHostname} = hostname();
  67. $hash->{fhemIP} = fakeRoku_getLocalIP();
  68. if( $init_done ) {
  69. fakeRoku_startDiscovery($hash);
  70. fakeRoku_startListener($hash);
  71. } else {
  72. readingsSingleUpdate($hash, 'state', 'initialized', 1 );
  73. }
  74. return undef;
  75. }
  76. sub
  77. fakeRoku_Notify($$)
  78. {
  79. my ($hash,$dev) = @_;
  80. return if($dev->{NAME} ne "global");
  81. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  82. fakeRoku_startDiscovery($hash);
  83. fakeRoku_startListener($hash);
  84. return undef;
  85. }
  86. sub
  87. fakeRoku_closeSocket($)
  88. {
  89. my ($hash) = @_;
  90. my $name = $hash->{NAME};
  91. if( !$hash->{CD} ) {
  92. my $pname = $hash->{PNAME} || $name;
  93. Log3 $pname, 2, "$name: trying to close a non socket hash";
  94. return undef;
  95. }
  96. RemoveInternalTimer($hash);
  97. close($hash->{CD});
  98. delete($hash->{CD});
  99. delete($selectlist{$name});
  100. delete($hash->{FD});
  101. }
  102. sub
  103. fakeRoku_newChash($$$)
  104. {
  105. my ($hash,$socket,$chash) = @_;
  106. $chash->{TYPE} = $hash->{TYPE};
  107. $chash->{NR} = $devcount++;
  108. $chash->{phash} = $hash;
  109. $chash->{PNAME} = $hash->{NAME};
  110. $chash->{CD} = $socket;
  111. $chash->{FD} = $socket->fileno();
  112. $chash->{PORT} = $socket->sockport if( $socket->sockport );
  113. $chash->{TEMPORARY} = 1;
  114. $attr{$chash->{NAME}}{room} = 'hidden';
  115. $defs{$chash->{NAME}} = $chash;
  116. $selectlist{$chash->{NAME}} = $chash;
  117. }
  118. sub
  119. fakeRoku_startDiscovery($)
  120. {
  121. my ($hash) = @_;
  122. my $name = $hash->{NAME};
  123. return undef if( !$fakeRoku_hasMulticast );
  124. fakeRoku_stopDiscovery($hash);
  125. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  126. if( 1 ) {
  127. # respond to multicast client discovery messages
  128. $hash->{reusePort} = AttrVal($name, 'reusePort', defined(&SO_REUSEPORT)?1:0)?1:0;
  129. if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>1900, ReuseAddr=>1, ReusePort=>$hash->{reusePort} ) ) {
  130. $socket->mcast_add('239.255.255.250');
  131. my $chash = fakeRoku_newChash( $hash, $socket,
  132. {NAME=>"$name:responder", STATE=>'listening', multicast => 1} );
  133. $hash->{helper}{responder} = $chash;
  134. Log3 $name, 3, "$name: ssdp responder started";
  135. } else {
  136. Log3 $name, 3, "$name: failed to start ssdp responder: $@";
  137. InternalTimer(gettimeofday()+10, "fakeRoku_startDiscovery", $hash, 0);
  138. }
  139. }
  140. }
  141. sub
  142. fakeRoku_stopDiscovery($)
  143. {
  144. my ($hash) = @_;
  145. my $name = $hash->{NAME};
  146. RemoveInternalTimer($hash, "fakeRoku_startDiscovery");
  147. if( my $chash = $hash->{helper}{responder} ) {
  148. my $cname = $chash->{NAME};
  149. fakeRoku_closeSocket($chash);
  150. delete($defs{$cname});
  151. delete $hash->{helper}{responder};
  152. Log3 $name, 3, "$name: ssdp responder stoped";
  153. }
  154. }
  155. sub
  156. fakeRoku_startListener($)
  157. {
  158. my ($hash) = @_;
  159. my $name = $hash->{NAME};
  160. fakeRoku_stopListener($hash);
  161. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  162. my $port = AttrVal($name, 'httpPort', 0);
  163. if( my $socket = IO::Socket::INET->new(LocalPort=>$port, Listen=>10, Blocking=>0, ReuseAddr=>1) ) {
  164. readingsSingleUpdate($hash, 'state', 'listening', 1 );
  165. my $chash = fakeRoku_newChash( $hash, $socket, {NAME=>"$name:listener", STATE=>'accepting'} );
  166. $chash->{connections} = {};
  167. $hash->{helper}{listener} = $chash;
  168. Log3 $name, 3, "$name: listener started";
  169. } else {
  170. Log3 $name, 3, "$name: failed to start listener on port $port: $@";
  171. readingsSingleUpdate($hash, 'state', 'disconnected', 1 );
  172. InternalTimer(gettimeofday()+10, "fakeRoku_startListener", $hash, 0);
  173. }
  174. }
  175. sub
  176. fakeRoku_stopListener($)
  177. {
  178. my ($hash) = @_;
  179. my $name = $hash->{NAME};
  180. RemoveInternalTimer($hash, "fakeRoku_startListener");
  181. if( my $chash = $hash->{helper}{listener} ) {
  182. my $cname = $chash->{NAME};
  183. foreach my $key ( keys %{$chash->{connections}} ) {
  184. my $hash = $chash->{connections}{$key};
  185. my $name = $hash->{NAME};
  186. fakeRoku_closeSocket($hash);
  187. delete($defs{$name});
  188. delete($chash->{connections}{$name});
  189. }
  190. fakeRoku_closeSocket($chash);
  191. delete($defs{$cname});
  192. delete $hash->{helper}{listener};
  193. readingsSingleUpdate($hash, 'state', 'stopped', 1 );
  194. Log3 $name, 3, "$name: listener stoped";
  195. }
  196. }
  197. sub
  198. fakeRoku_Undefine($$)
  199. {
  200. my ($hash, $arg) = @_;
  201. fakeRoku_stopListener($hash);
  202. fakeRoku_stopDiscovery($hash);
  203. delete $modules{fakeRoku}{defptr}{$hash->{ID}?$hash->{ID}:'MASTER'};
  204. return undef;
  205. }
  206. sub
  207. fakeRoku_Set($$@)
  208. {
  209. my ($hash, $name, $cmd, @params) = @_;
  210. $hash->{".triggerUsed"} = 1;
  211. my $list = '';
  212. $list =~ s/ $//;
  213. return "Unknown argument $cmd, choose one of $list";
  214. }
  215. sub
  216. fakeRoku_makeLink($$$$;$)
  217. {
  218. my ($hash, $cmd, $parentSection, $key, $txt) = @_;
  219. return $txt if( !$key );
  220. $txt = $key if( !$txt );
  221. if( defined($parentSection) && $parentSection eq '' && $key !~ '^/' ) {
  222. $cmd = "get $hash->{NAME} $cmd /library/sections/$key";
  223. } elsif( defined($parentSection) && $key !~ '^/' ) {
  224. $cmd = "get $hash->{NAME} $cmd $parentSection/$key";
  225. } elsif( $key !~ '^/' ) {
  226. $cmd = "get $hash->{NAME} $cmd /library/metadata/$key";
  227. } else {
  228. $cmd = "get $hash->{NAME} $cmd $key";
  229. }
  230. return $txt if( !$FW_ME );
  231. return "<a style=\"cursor:pointer\" onClick=\"FW_cmd(\\\'$FW_ME$FW_subdir?XHR=1&cmd=$cmd\\\')\">$txt</a>";
  232. }
  233. sub
  234. fakeRoku_Get($$@)
  235. {
  236. my ($hash, $name, $cmd, @params) = @_;
  237. my $list = '';
  238. $list =~ s/ $//;
  239. return "Unknown argument $cmd, choose one of $list";
  240. }
  241. sub
  242. fakeRoku_Attr($$$)
  243. {
  244. my ($cmd, $name, $attrName, $attrVal) = @_;
  245. my $orig = $attrVal;
  246. $attrVal = int($attrVal) if($attrName eq "interval");
  247. $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0);
  248. my $hash = $defs{$name};
  249. if( $attrName eq 'disable' ) {
  250. if( $cmd eq "set" && $attrVal ) {
  251. fakeRoku_stopListener($hash);
  252. fakeRoku_stopDiscovery($hash);
  253. } else {
  254. $attr{$name}{$attrName} = 0;
  255. fakeRoku_startDiscovery($hash);
  256. fakeRoku_startListener($hash);
  257. }
  258. } elsif( $attrName eq 'fhemIP' ) {
  259. if( $cmd eq "set" && $attrVal ) {
  260. $hash->{fhemIP} = $attrVal;
  261. } else {
  262. $hash->{fhemIP} = fakeRoku_getLocalIP();
  263. }
  264. fakeRoku_startDiscovery($hash);
  265. fakeRoku_startListener($hash);
  266. } elsif( $attrName eq 'reusePort' ) {
  267. if( $cmd eq "set" ) {
  268. $attr{$name}{$attrName} = $attrVal;
  269. } else {
  270. delete $attr{$name}{$attrName};
  271. }
  272. fakeRoku_startDiscovery($hash);
  273. }
  274. if( $cmd eq "set" ) {
  275. if( $attrVal && $orig ne $attrVal ) {
  276. $attr{$name}{$attrName} = $attrVal;
  277. return $attrName ." set to ". $attrVal if( $init_done );
  278. }
  279. }
  280. return;
  281. }
  282. sub
  283. fakeRoku_msg2hash($;$)
  284. {
  285. my ($string,$keep) = @_;
  286. my %hash = ();
  287. if( $string !~ m/\r/ ) {
  288. $string =~ s/\n/\r\n/g;
  289. }
  290. foreach my $line (split("\r\n", $string)) {
  291. my ($key,$value) = split( ": ", $line );
  292. next if( !$value );
  293. if( !$keep ) {
  294. $key =~ s/-//g;
  295. $key = uc( $key );
  296. }
  297. $value =~ s/^ //;
  298. $hash{$key} = $value;
  299. }
  300. return \%hash;
  301. }
  302. sub
  303. fakeRoku_hash2header($)
  304. {
  305. my ($hash) = @_;
  306. return $hash if( ref($hash) ne 'HASH' );
  307. my $header;
  308. foreach my $key (keys %{$hash}) {
  309. #$header .= "\r\n" if( $header );
  310. $header .= "$key: $hash->{$key}\r\n";
  311. }
  312. return $header;
  313. }
  314. sub
  315. fakeRoku_Parse($$;$$$)
  316. {
  317. my ($hash,$msg,$peerhost,$peerport,$sockport) = @_;
  318. my $name = $hash->{NAME};
  319. Log3 $name, 5, "$name: from: $peerhost" if( $peerhost );
  320. Log3 $name, 5, "$name: $msg";
  321. my $handled = 0;
  322. if( $peerhost ) { #from broadcast
  323. if( $msg =~ '^([\w\-]+) \* HTTP/1.\d' ) {
  324. my $type = $1;
  325. my $params = fakeRoku_msg2hash($msg);
  326. if( $type eq 'M-SEARCH' ) {
  327. $handled = 1;
  328. if( $peerhost eq $hash->{fhemIP} ) {
  329. if( $hash->{helper}{discoverClientsBcast} && $hash->{helper}{discoverClientsBcast}->{CD}->sockport() == $peerport ) {
  330. #Log3 $name, 5, "$name: ignoring broadcast M-Search from self ($peerhost:$peerport)";
  331. return undef;
  332. }
  333. }
  334. if( !$params->{MAN} ) {
  335. Log3 $name, 5, "$name: ignoring broadcast M-Search without MAN";
  336. return undef;
  337. } elsif( $params->{MAN} ne '"ssdp:discover"' ) {
  338. Log3 $name, 5, "$name: ignoring broadcast M-Search with MAN $params->{MAN}";
  339. return undef;
  340. }
  341. Log3 $name, 5, "$name: received from: $peerhost:$peerport to $sockport: $msg";
  342. my $msg = "HTTP/1.1 200 OK\r\n";
  343. $msg .= fakeRoku_hash2header( { 'Cache-Control' => 'max-age=300',
  344. 'ST' => 'roku:ecp',
  345. 'Location' => "http://$hash->{fhemIP}:$hash->{helper}{listener}{PORT}/",
  346. 'USN' => "uuid:roku:ecp:". AttrVal($name,'serial',$hash->{helper}{serial}), } );
  347. $msg .= "\r\n";
  348. my $sin = sockaddr_in($peerport, inet_aton($peerhost));
  349. $hash->{helper}{responder}->{CD}->send($msg, 0, $sin );
  350. }
  351. elsif( $type eq 'NOTIFY' ) {
  352. $handled = 1;
  353. }
  354. }
  355. } elsif( $msg =~ '^GET\s*([^\s]*)\s*HTTP/1.\d' ) {
  356. my $request = $1;
  357. if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) {
  358. my $header = $1;
  359. my $body = $2;
  360. my $params;
  361. if( $request =~ m/^([^?]*)(\?(.*))?/ ) {
  362. #$request = $1;
  363. if( $3 ) {
  364. foreach my $param (split("&", $3)) {
  365. my ($key,$value) = split("=",$param);
  366. $params->{$key} = $value;
  367. }
  368. }
  369. }
  370. $header = fakeRoku_msg2hash($header, 1);
  371. my $ret;
  372. if( $request =~ m'^/$' ) {
  373. $handled = 1;
  374. #Log3 $name, 4, "$name: request: $msg";
  375. Log3 $name, 4, "$name: answering $request";
  376. my $xml = { root => { xmlns => 'urn:schemas-upnp-org:device-1-0',
  377. specVersion => { major => [1], minor => [0] },
  378. device => { deviceType => ['urn:roku-com:device:player:1-0'],
  379. friendlyName => ['FHEM'],
  380. manufacturer => ['FHEM'],
  381. manufacturerURL => ['http://www.fhem.de/'],
  382. modelDescription => ['FHEM fake Roku player'],
  383. modelName => ['FHEM'],
  384. modelNumber => ['4200X'],
  385. modelURL => ['http://www.fhem.de/'],
  386. serialNumber => [AttrVal($name,'serial',$hash->{helper}{serial})],
  387. UDN => ["uuid:roku:ecp:".AttrVal($name,'serial',$hash->{helper}{serial})],
  388. serviceList => [ { service => [ { serviceType => ['urn:roku-com:service:ecp:1'],
  389. serviceId => ['urn:roku-com:serviceId:ecp1-0'],
  390. controlURL => [''],
  391. eventSubURL => [''],
  392. SCPDURL => ['ecp_SCPD.xml'],
  393. } ],
  394. }, ],
  395. },
  396. }, };
  397. my $body = '<?xml version="1.0" encoding="utf-8" ?>';
  398. $body .= XMLout( $xml, KeyAttr => { }, RootName => undef, NoIndent => 1 );
  399. #$body =~ s/\n/\r\n/g;
  400. $ret = "HTTP/1.1 200 OK\r\n";
  401. $ret .= fakeRoku_hash2header( { 'Connection' => 'Close',
  402. 'Content-Type' => 'text/xml; charset=utf-8',
  403. 'Content-Length' => length($body), } );
  404. $ret .= "\r\n";
  405. $ret .= $body;
  406. } elsif( $request =~ m'^/query/apps' ) {
  407. $handled = 1;
  408. #Log3 $name, 4, "$name: request: $msg";
  409. Log3 $name, 4, "$name: answering $request";
  410. my $xml = { app => [], };
  411. if( my $favourites = AttrVal($name, "favourites", undef ) ) {
  412. my @favourites = split( ',', $favourites );
  413. for (my $i=0; $i<=$#favourites; $i++) {
  414. $xml->{app}[$i] = { id => $i+1, content => $favourites[$i], };
  415. }
  416. }
  417. #my $body = '<?xml version="1.0" encoding="utf-8" ?>';
  418. my $body .= XMLout( $xml, KeyAttr => { }, RootName => 'apps' );
  419. #$body =~ s/\n/\r\n/g;
  420. $ret = "HTTP/1.1 200 OK\r\n";
  421. $ret .= fakeRoku_hash2header( { 'Connection' => 'Close',
  422. 'Content-Type' => 'text/xml; charset=utf-8',
  423. 'Content-Length' => length($body), } );
  424. $ret .= "\r\n";
  425. $ret .= $body;
  426. }
  427. if( !$handled ) {
  428. $peerhost = $peerhost ? " from $peerhost" : '';
  429. Log3 $name, 2, "$name: unhandled request: $msg";
  430. }
  431. #Log 1, $ret;
  432. return $ret;
  433. }
  434. } elsif( $msg =~ '^POST\s*([^\s]*)\s*HTTP/1.\d' ) {
  435. my $request = $1;
  436. if( $request =~ '^/key(down|up|press)/(.*)' ) {
  437. $handled = 1;
  438. my $action = $1;
  439. my $key = $2;
  440. if( $key =~ /Lit_(%.*)/ ) {
  441. $key = urlDecode($1);
  442. } elsif( $key =~ /Lit_(.*)/ ) {
  443. $key = $1;
  444. }
  445. DoTrigger( $name, "key$action: $key" );
  446. } elsif( $request =~ '^/launch/(.*)' ) {
  447. $handled = 1;
  448. DoTrigger( $name, "launch: $1" );
  449. }
  450. }
  451. if( !$handled ) {
  452. $peerhost = $peerhost ? " from $peerhost" : '';
  453. Log3 $name, 2, "$name: unhandled message$peerhost: $msg";
  454. }
  455. return undef;
  456. }
  457. sub
  458. fakeRoku_Read($)
  459. {
  460. my ($hash) = @_;
  461. my $name = $hash->{NAME};
  462. my $len;
  463. my $buf;
  464. if( $hash->{multicast} || $hash->{broadcast} ) {
  465. my $phash = $hash->{phash};
  466. $len = $hash->{CD}->recv($buf, 1024);
  467. if( !defined($len) || !$len ) {
  468. Log 1, "!!!!!!!!!!";
  469. return;
  470. }
  471. my $peerhost = $hash->{CD}->peerhost;
  472. my $peerport = $hash->{CD}->peerport;
  473. my $sockport = $hash->{CD}->sockport;
  474. fakeRoku_Parse($phash, $buf, $peerhost, $peerport, $sockport);
  475. } elsif( $hash->{timeline} ) {
  476. $len = sysread($hash->{CD}, $buf, 10240);
  477. #Log 1, "1:$len: $buf";
  478. my $peerhost = $hash->{CD}->peerhost;
  479. my $peerport = $hash->{CD}->peerport;
  480. if( !defined($len) || !$len ) {
  481. fakeRoku_closeSocket( $hash );
  482. delete($defs{$name});
  483. return undef;
  484. }
  485. #Log 1, "timeline ($peerhost:$peerport): $buf";
  486. return undef;
  487. } elsif ( $hash->{phash} ) {
  488. my $phash = $hash->{phash};
  489. my $pname = $hash->{PNAME};
  490. if( $phash->{helper}{listener} == $hash ) {
  491. my @clientinfo = $hash->{CD}->accept();
  492. if( !@clientinfo ) {
  493. Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN);
  494. return undef;
  495. }
  496. $hash->{CONNECTS}++;
  497. my ($port, $iaddr) = sockaddr_in($clientinfo[1]);
  498. my $caddr = inet_ntoa($iaddr);
  499. my $chash = fakeRoku_newChash( $phash, $clientinfo[0], {NAME=>"$name:$port", STATE=>'listening'} );
  500. $chash->{buf} = '';
  501. $hash->{connections}{$chash->{NAME}} = $chash;
  502. Log3 $name, 5, "$name: timeline sender $caddr connected to $port";
  503. return;
  504. }
  505. $len = sysread($hash->{CD}, $buf, 10240);
  506. #Log 1, "2:$len: $buf";
  507. do {
  508. my $close = 1;
  509. if( $len ) {
  510. $hash->{buf} .= $buf;
  511. return if $hash->{buf} !~ m/^(.*?)\r?\n\r?\n(.*)?$/s;
  512. my $header = $1;
  513. my $body = $2;
  514. my $content_length;
  515. my $length = length($body);
  516. if( $header =~ m/Content-Length:\s*(\d+)/si ) {
  517. $content_length = $1;
  518. return if( $length < $content_length );
  519. if( $header !~ m/Connection: Close/si ) {
  520. $close = 0;
  521. Log3 $pname, 5, "$name: keepalive";
  522. #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n" );
  523. if( $length > $content_length ) {
  524. $buf = substr( $body, $content_length );
  525. $hash->{buf} = "$header\r\n\r\n". substr( $body, 0, $content_length );
  526. } else {
  527. $buf ='';
  528. }
  529. } else {
  530. Log3 $pname, 5, "$name: close";
  531. #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Close\r\n\r\n" );
  532. }
  533. } elsif( $length == 0 && $header =~ m/^GET/ ) {
  534. $buf = '';
  535. } else {
  536. return;
  537. }
  538. }
  539. Log3 $pname, 4, "$name: disconnected" if( !$len );
  540. my $ret;
  541. $ret = fakeRoku_Parse($phash, $hash->{buf}) if( $hash->{buf} );
  542. if( $len ) {
  543. my $add_header;
  544. if( !$ret || $ret !~ m/^HTTP/si ) {
  545. $add_header .= "HTTP/1.1 200 OK\r\n";
  546. }
  547. if( !$ret || $ret !~ m/Connection:/si ) {
  548. if( $close ) {
  549. $add_header .= "Connection: Close\r\n";
  550. } else {
  551. $add_header .= "Connection: Keep-Alive\r\n";
  552. }
  553. }
  554. if( !$ret ) {
  555. $add_header .= "Content-Length: 0\r\n";
  556. }
  557. syswrite($hash->{CD}, $add_header) if( $add_header );
  558. Log3 $pname, 5, "$name: add header: $add_header" if( $add_header );
  559. if( $ret ) {
  560. syswrite($hash->{CD}, $ret);
  561. if( $ret !~ m/Connection: Close/si ) {
  562. $close = 0;
  563. Log3 $pname, 5, "$name: keepalive";
  564. }
  565. } else {
  566. syswrite($hash->{CD}, "\r\n" );
  567. }
  568. }
  569. $hash->{buf} = $buf;
  570. $buf = '';
  571. if( $close || !$len ) {
  572. fakeRoku_closeSocket( $hash );
  573. delete($defs{$name});
  574. delete($hash->{phash}{helper}{listener}{connections}{$hash->{NAME}});
  575. return;
  576. }
  577. } while( $hash->{buf} );
  578. }
  579. return undef;
  580. }
  581. 1;
  582. =pod
  583. =item summary roku remote control protocol server
  584. =item summary_DE Roku Remote Control Protokoll Server
  585. =begin html
  586. <a name="fakeRoku"></a>
  587. <h3>fakeRoku</h3>
  588. <ul>
  589. This module allows you to add a 'fake' roku player device to a harmony hub based remote and to receive and
  590. process configured key presses in FHEM.
  591. <br><br>
  592. Notes:
  593. <ul>
  594. <li>XML::Simple is needed.</li>
  595. <li>IO::Socket::Multicast is needed.</li>
  596. <li>The following 12 functions are available and can be used:
  597. <ul>
  598. <li>InstantReplay</li>
  599. <li>Home</li>
  600. <li>Info</li>
  601. <li>Search</li>
  602. <li>Back</li>
  603. <li>FastForward = Fwd</li>
  604. <li>Rewind = Rev</li>
  605. <li>Select</li>
  606. <li>DirectionUp</li>
  607. <li>DirectionRight</li>
  608. <li>DirectionLeft</li>
  609. <li>DirectionDown</li>
  610. </ul></li>
  611. </ul>
  612. <br><br>
  613. <a name="fakeRoku_Define"></a>
  614. <b>Define</b>
  615. <ul>
  616. <code>define &lt;name&gt; fakeRoku</code>
  617. <br><br>
  618. </ul>
  619. <a name="fakeRoku_Set"></a>
  620. <b>Set</b>
  621. <ul>none
  622. </ul><br>
  623. <a name="fakeRoku_Get"></a>
  624. <b>Get</b>
  625. <ul>none
  626. </ul><br>
  627. <a name="fakeRoku_Attr"></a>
  628. <b>Attr</b>
  629. <ul>
  630. <li>favourites<br>
  631. comma separated list of names to use as apps/channels/favourites. the list can be reloaded on the harmony with edit->reset.</li>
  632. <li>fhemIP<br>
  633. overwrites autodetected local ip used in advertising</li>
  634. <li>httpPort</li>
  635. <li>reusePort<br>
  636. not set -> set ReusePort on multicast socket if SO_REUSEPORT flag ist known. should work in most cases.<br>
  637. 0 -> don't set ReusePort on multicast socket<br>
  638. 1 -> set ReusePort on multicast socket</li>
  639. <li>serial</li>
  640. </ul>
  641. </ul><br>
  642. =end html
  643. =cut