30_MilightBridge.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. # $Id: 30_MilightBridge.pm 16085 2018-02-04 18:35:23Z mattwire $
  2. ##############################################
  3. #
  4. # 30_MilightBridge.pm (Use with 31_MilightDevice.pm)
  5. # FHEM module for Milight Wifi bridges which control Milight lightbulbs.
  6. #
  7. # Author: Matthew Wire (mattwire)
  8. #
  9. # This file is part of fhem.
  10. #
  11. # Fhem is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU General Public License as published by
  13. # the Free Software Foundation, either version 2 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # Fhem is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License
  22. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  23. #
  24. ##############################################################################
  25. package main;
  26. use strict;
  27. use warnings;
  28. use Blocking;
  29. use IO::Handle;
  30. use IO::Socket;
  31. use IO::Select;
  32. use Time::HiRes;
  33. use Net::Ping;
  34. sub MilightBridge_Initialize($)
  35. {
  36. my ($hash) = @_;
  37. # Provider
  38. # $hash->{ReadFn} = "MilightBridge_Read";
  39. $hash->{WriteFn} = "MilightBridge_Write";
  40. #Consumer
  41. $hash->{DefFn} = "MilightBridge_Define";
  42. $hash->{UndefFn} = "MilightBridge_Undefine";
  43. $hash->{NOTIFYDEV} = "global";
  44. $hash->{NotifyFn} = "MilightBridge_Notify";
  45. $hash->{AttrFn} = "MilightBridge_Attr";
  46. $hash->{AttrList} = "port protocol:udp,tcp sendInterval disable:0,1 tcpPing:1 checkInterval ".$readingFnAttributes;
  47. return undef;
  48. }
  49. #####################################
  50. # Define bridge device
  51. sub MilightBridge_Define($$)
  52. {
  53. my ($hash, $def) = @_;
  54. my @args = split("[ \t][ \t]*", $def);
  55. return "Usage: define <name> MilightBridge <host/ip:port>" if(@args < 3);
  56. my ($name, $type, $hostandport) = @args;
  57. $hash->{Clients} = ":MilightDevice:";
  58. my %matchList = ( "1:MilightDevice" => ".*" );
  59. $hash->{MatchList} = \%matchList;
  60. my ($host, $port) = split(":", $hostandport);
  61. # Parameters
  62. $hash->{HOST} = $host;
  63. # Set Port (Default 8899, old bridge (V2) uses 50000
  64. $port = "8899" if (!defined($port));
  65. $hash->{PORT} = $port;
  66. $attr{$name}{"protocol"} = "udp" if (!defined($attr{$name}{"protocol"}));
  67. # Create local socket
  68. my $sock = IO::Socket::INET-> new (
  69. PeerPort => 48899,
  70. Blocking => 0,
  71. Proto => $attr{$name}{"protocol"},
  72. Broadcast => 1) or return "can't bind: $@";
  73. my $select = IO::Select->new($sock);
  74. $hash->{SOCKET} = $sock;
  75. $hash->{SELECT} = $select;
  76. # Note: Milight API specifies 100ms bridge delay for sending commands
  77. # Define sendInterval
  78. $attr{$name}{"sendInterval"} = 100 if (!defined($attr{$name}{"sendInterval"}));
  79. $hash->{INTERVAL} = $attr{$name}{"sendInterval"};
  80. # Create command queue to hold commands
  81. my @cmdQueue = ();
  82. $hash->{cmdQueue} = \@cmdQueue;
  83. $hash->{cmdQueueLock} = 0;
  84. $hash->{cmdLastSent} = gettimeofday();
  85. # Set Attributes
  86. $attr{$name}{"event-on-change-reading"} = "state" if (!defined($attr{$name}{"event-on-change-reading"}));
  87. $attr{$name}{"checkInterval"} = 10 if (!defined($attr{$name}{"checkInterval"}));
  88. delete $hash->{helper}{RUNNING_PID};
  89. readingsSingleUpdate($hash, "state", "Initialized", 1);
  90. # Set state
  91. $hash->{SENDFAIL} = 0;
  92. # Get initial bridge state
  93. MilightBridge_SetNextTimer($hash);
  94. return undef;
  95. }
  96. #####################################
  97. # Undefine Bridge device
  98. sub MilightBridge_Undefine($$)
  99. {
  100. my ($hash,$arg) = @_;
  101. RemoveInternalTimer($hash);
  102. BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID}));
  103. return undef;
  104. }
  105. #####################################
  106. # Manage attribute changes
  107. sub MilightBridge_Attr($$$$) {
  108. my ($command,$name,$attribute,$value) = @_;
  109. my $hash = $defs{$name};
  110. $value = "" if(!defined($value));
  111. Log3 ($hash, 5, "$hash->{NAME}_Attr: Attr $attribute; Value $value");
  112. # Handle "sendInterval" attribute which defaults to 100(ms)
  113. if ($attribute eq "sendInterval")
  114. {
  115. if (($value !~ /^\d*$/) || ($value < 1))
  116. {
  117. $attr{$name}{"sendInterval"} = 100;
  118. $hash->{INTERVAL} = $attr{$name}{"sendInterval"};
  119. return "sendInterval is required in ms (default: 100)";
  120. }
  121. else
  122. {
  123. $hash->{INTERVAL} = $attr{$name}{"sendInterval"};
  124. }
  125. }
  126. if ($attribute eq "checkInterval")
  127. {
  128. if (($value !~ /^\d*$/) || ($value < 0))
  129. {
  130. $attr{$name}{"checkInterval"} = 10;
  131. return "checkInterval is required in s (default: 10, min: 0)";
  132. }
  133. readingsSingleUpdate($hash, "state", "Initialized", 1);
  134. MilightBridge_SetNextTimer($hash);
  135. }
  136. elsif ($attribute eq "protocol")
  137. {
  138. if (($value eq "tcp" || $value eq "udp"))
  139. {
  140. my $protocolchanged = (defined($attr{$name}{"protocol"}) && $attr{$name}{"protocol"} ne $value);
  141. $attr{$name}{"protocol"} = $value;
  142. return "You need to restart fhem or modify to enable new protocol." if($protocolchanged);
  143. }
  144. else
  145. {
  146. return "protocol must be one of 'tcp|udp'";
  147. }
  148. }
  149. # Handle "disable" attribute by opening/closing connection to device
  150. elsif ($attribute eq "disable")
  151. {
  152. # Disable on 1, enable on anything else.
  153. if ($value eq "1")
  154. {
  155. readingsSingleUpdate($hash, "state", "disabled", 1);
  156. }
  157. else
  158. {
  159. readingsSingleUpdate($hash, "state", "Initialized", 1);
  160. }
  161. }
  162. return undef;
  163. }
  164. #####################################
  165. # Update slot information when a global notify event is fired
  166. sub MilightBridge_Notify($$)
  167. {
  168. my ($hash,$dev) = @_;
  169. if(grep(m/^(INITIALIZED|REREADCFG|DEFINED.*|MODIFIED.*|DELETED.*)$/, @{$dev->{CHANGED}}))
  170. {
  171. MilightBridge_SlotUpdate($hash);
  172. }
  173. return undef;
  174. }
  175. #####################################
  176. # Set next timer for ping check
  177. sub MilightBridge_SetNextTimer($)
  178. {
  179. my ($hash) = @_;
  180. # Check state every X seconds
  181. RemoveInternalTimer($hash);
  182. my $interval=AttrVal($hash->{NAME}, "checkInterval", "10");
  183. if ($interval > 0) {
  184. InternalTimer(gettimeofday() + $interval, "MilightBridge_DoPingStart", $hash, 0);
  185. }
  186. }
  187. #####################################
  188. # Prepare and start the blocking call in new thread
  189. sub MilightBridge_DoPingStart($)
  190. {
  191. my ($hash) = @_;
  192. return undef if (IsDisabled($hash->{NAME}));
  193. my $timeout = 2;
  194. my $mode = 'udp';
  195. $mode = 'tcp' if(defined($attr{$hash->{NAME}}{tcpPing}));
  196. my $arg = $hash->{NAME}."|".$hash->{HOST}."|".$mode."|".$timeout;
  197. my $blockingFn = "MilightBridge_DoPing";
  198. my $finishFn = "MilightBridge_DoPingDone";
  199. my $abortFn = "MilightBridge_DoPingAbort";
  200. if (!(exists($hash->{helper}{RUNNING_PID}))) {
  201. $hash->{helper}{RUNNING_PID} =
  202. BlockingCall($blockingFn, $arg, $finishFn, $timeout, $abortFn, $hash);
  203. } else {
  204. Log3 $hash, 3, "$hash->{NAME} Blocking Call running no new started";
  205. MilightBridge_SetNextTimer($hash);
  206. }
  207. }
  208. #####################################
  209. # BlockingCall DoPing in separate thread
  210. sub MilightBridge_DoPing(@)
  211. {
  212. my ($string) = @_;
  213. my ($name, $host, $mode, $timeout) = split("\\|", $string);
  214. Log3 ($name, 5, $name."_DoPing: Executing ping");
  215. # check via ping
  216. my $p;
  217. $p = Net::Ping->new($mode);
  218. my $result = $p->ping($host, $timeout);
  219. $p->close();
  220. $result="" if !(defined($result));
  221. return "$name|$result";
  222. }
  223. #####################################
  224. # Ping thread completed
  225. sub MilightBridge_DoPingDone($)
  226. {
  227. my ($string) = @_;
  228. my ($name, $result) = split("\\|", $string);
  229. my $hash = $defs{$name};
  230. my $status = "ok";
  231. $status = "unreachable" if !($result);
  232. # Update readings
  233. readingsBeginUpdate($hash);
  234. readingsBulkUpdate($hash, "state", $status);
  235. readingsBulkUpdate( $hash, "sendFail", $hash->{SENDFAIL});
  236. readingsEndUpdate($hash, 1);
  237. delete($hash->{helper}{RUNNING_PID});
  238. MilightBridge_SetNextTimer($hash);
  239. }
  240. #####################################
  241. # Ping thread timeout
  242. sub MilightBridge_DoPingAbort($)
  243. {
  244. my ($hash) = @_;
  245. delete($hash->{helper}{RUNNING_PID});
  246. Log3 $hash->{NAME}, 3, "BlockingCall for ".$hash->{NAME}." was aborted";
  247. MilightBridge_SetNextTimer($hash);
  248. }
  249. #####################################
  250. # Update readings to show which slots have devices defined
  251. sub MilightBridge_SlotUpdate(@)
  252. {
  253. # Update readings to show what is connected to which slot
  254. my ($hash) = @_;
  255. Log3 ( $hash, 5, "$hash->{NAME}_State: Updating Slot readings");
  256. readingsBeginUpdate($hash);
  257. readingsBulkUpdate($hash, "slot0", (defined($hash->{0}->{NAME}) ? $hash->{0}->{NAME} : ""));
  258. readingsBulkUpdate($hash, "slot1", (defined($hash->{1}->{NAME}) ? $hash->{1}->{NAME} : ""));
  259. readingsBulkUpdate($hash, "slot2", (defined($hash->{2}->{NAME}) ? $hash->{2}->{NAME} : ""));
  260. readingsBulkUpdate($hash, "slot3", (defined($hash->{3}->{NAME}) ? $hash->{3}->{NAME} : ""));
  261. readingsBulkUpdate($hash, "slot4", (defined($hash->{4}->{NAME}) ? $hash->{4}->{NAME} : ""));
  262. readingsBulkUpdate($hash, "slot5", (defined($hash->{5}->{NAME}) ? $hash->{5}->{NAME} : ""));
  263. readingsBulkUpdate($hash, "slot6", (defined($hash->{6}->{NAME}) ? $hash->{6}->{NAME} : ""));
  264. readingsBulkUpdate($hash, "slot7", (defined($hash->{7}->{NAME}) ? $hash->{7}->{NAME} : ""));
  265. readingsBulkUpdate($hash, "slot8", (defined($hash->{8}->{NAME}) ? $hash->{8}->{NAME} : ""));
  266. readingsEndUpdate($hash, 1);
  267. return undef;
  268. }
  269. #####################################
  270. # Device write function. Receives a command and triggers the send queue
  271. sub MilightBridge_Write(@)
  272. {
  273. # Client sent a new command
  274. my ($hash, $cmd) = @_;
  275. Log3 ($hash, 3, "$hash->{NAME}_Write: Command not defined") if (!defined($cmd));
  276. my $hexStr = unpack("H*", $cmd || '');
  277. Log3 ($hash, 4, "$hash->{NAME}_Write: Command: $hexStr");
  278. # Add command to queue
  279. push @{$hash->{cmdQueue}}, $cmd;
  280. MilightBridge_CmdQueue_Send($hash);
  281. }
  282. #####################################
  283. # Send a queued command to the bridge hardware
  284. sub MilightBridge_CmdQueue_Send(@)
  285. {
  286. my ($hash) = @_;
  287. # Check that queue is not locked. If it is we should just return because another instance of this function has locked it.
  288. if ($hash->{cmdQueueLock} != 0)
  289. {
  290. Log3 ($hash, 5, "$hash->{NAME}_cmdQueue_Send: Send Queue Locked: cmdQueueLock = $hash->{cmdQueueLock}. Return.");
  291. return undef;
  292. }
  293. # Check if we are called again before send interval has elapsed
  294. my $now = gettimeofday();
  295. if ((($hash->{cmdLastSent} + ($hash->{INTERVAL} / 1000)) < $now) && $init_done)
  296. {
  297. # Lock cmdQueue
  298. $hash->{cmdQueueLock} = 1;
  299. # Extract current command
  300. my $command = @{$hash->{cmdQueue}}[0];
  301. # Check if we have any commands in queue
  302. if (!defined($command))
  303. {
  304. Log3 ($hash, 5, "$hash->{NAME}_cmdQueue_Send: No commands in queue");
  305. }
  306. else
  307. {
  308. # Send the command
  309. my $hexStr = unpack("H*", $command || '');
  310. Log3 ($hash, 5, "$hash->{NAME} send: $hexStr@".gettimeofday()."; Queue Length: ".@{$hash->{cmdQueue}});
  311. # Check bridge is not disabled, and send command
  312. if (!IsDisabled($hash->{NAME}))
  313. {
  314. my $hostip = inet_aton($hash->{HOST});
  315. if (!defined($hostip) || $hostip eq '')
  316. {
  317. Log3 ($hash, 3, "$hash->{NAME}: Could not resolve hostname " . $hash->{HOST});
  318. return undef;
  319. }
  320. # sockaddr_in crashes if ip address is undef
  321. my $portaddr = sockaddr_in($hash->{PORT}, $hostip);
  322. if (!send($hash->{SOCKET}, $command, 0, $portaddr))
  323. {
  324. # Send failed
  325. Log3 ($hash, 3, "$hash->{NAME} Send FAILED! ".gettimeofday().":$hexStr. Queue Length: ".@{$hash->{cmdQueue}});
  326. $hash->{SENDFAIL} = 1;
  327. }
  328. else
  329. {
  330. # Send successful
  331. $hash->{cmdLastSent} = gettimeofday(); # Update time last sent
  332. shift @{$hash->{cmdQueue}}; # transmission complete, remove command from queue
  333. }
  334. }
  335. }
  336. }
  337. elsif (!$init_done)
  338. {
  339. # fhem not initialized, wait for init
  340. Log3 ($hash, 3, "$hash->{NAME}_cmdQueue_Send: init not done, delay sending from queue");
  341. }
  342. else
  343. {
  344. # We were called again before send interval elapsed
  345. Log3 ($hash, 5, "$hash->{NAME}_cmdQueue_Send: Waiting for send interval. cmdLastSent: $hash->{cmdLastSent}. Now: $now");
  346. }
  347. # Unlock cmdQueue
  348. $hash->{cmdQueueLock} = 0;
  349. # Set next cycle if there are commands in the queue
  350. if (@{$hash->{cmdQueue}} > 0)
  351. {
  352. # INTERVAL is in msec, need to add seconds to gettimeofday (eg 100/1000 = 0.1 seconds)
  353. #Log3 ($hash, 5, "$hash->{NAME}_cmdQueue_Send: cmdLastSent: $hash->{cmdLastSent}; Next: ".(gettimeofday()+($hash->{INTERVAL}/1000)));
  354. # Remove any existing timers and trigger a new one
  355. RemoveInternalTimer($hash, 'MilightBridge_CmdQueue_Send');
  356. InternalTimer(gettimeofday()+($hash->{INTERVAL}/1000), "MilightBridge_CmdQueue_Send", $hash, 0);
  357. }
  358. return undef;
  359. }
  360. 1;
  361. =pod
  362. =item device
  363. =item summary Interface to a Milight Bridge connected to the network using a Wifi connection
  364. =begin html
  365. <a name="MilightBridge"></a>
  366. <h3>MilightBridge</h3>
  367. <ul>
  368. <p>This module is the interface to a Milight Bridge which is connected to the network using a Wifi connection. It uses a UDP protocal with no acknowledgement so there is no guarantee that your command was received.</p>
  369. <p>The Milight system is sold under various brands around the world including "LimitlessLED, EasyBulb, AppLamp"</p>
  370. <p>The API documentation is available here: <a href="http://www.limitlessled.com/dev/">http://www.limitlessled.com/dev/</a></p>
  371. <a name="MilightBridge_define"></a>
  372. <p><b>Define</b></p>
  373. <ul>
  374. <p><code>define &lt;name&gt; MilightBridge &lt;host/ip:port&gt;</code></p>
  375. <p>Specifies the MilightBridge device.<br/>
  376. &lt;host/ip&gt; is the hostname or IP address of the Bridge with optional port (defaults to 8899 if not defined, use 50000 for V1,V2 bridges)</p>
  377. </ul>
  378. <a name="MilightBridge_readings"></a>
  379. <p><b>Readings</b></p>
  380. <ul>
  381. <li>
  382. <b>state</b><br/>
  383. [Initialized|ok|unreachable]: Shows reachable status of bridge using "ping" check every 10 (checkInterval) seconds.
  384. </li>
  385. <li>
  386. <b>sendFail</b><br/>
  387. 0 if everything is OK. 1 if the send function was unable to send the command - this would indicate a problem with your network and/or host/port parameters.
  388. </li>
  389. <li>
  390. <b>slot[0|1|2|3|4|5|6|7|8]</b><br/>
  391. The slotX reading will display the name of the <a href="#MilightDevice">MilightDevice</a> that is defined with this Bridge as it's <a href="#IODev">IODev</a>. It will be blank if no device is defined for that slot.
  392. </li>
  393. </ul>
  394. <a name="MilightBridge_attr"></a>
  395. <p><b>Attributes</b></p>
  396. <ul>
  397. <li>
  398. <b>sendInterval</b><br/>
  399. Default: 100ms. The bridge has a minimum send delay of 100ms between commands.
  400. </li>
  401. <li>
  402. <b>checkInterval</b><br/>
  403. Default: 10s. Time after the bridge connection is re-checked.<br>
  404. If this is set to 0 checking is disabled and state = "Initialized".
  405. </li>
  406. <li>
  407. <b>protocol</b><br/>
  408. Default: udp. Change to tcp if you have enabled tcp mode on your bridge.
  409. </li>
  410. <li>
  411. <b>tcpPing</b><br/>
  412. If this attribute is defined, ping will use TCP instead of UDP.
  413. </li>
  414. </ul>
  415. </ul>
  416. =end html
  417. =cut