38_JawboneUp.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. # $Id: 38_JawboneUp.pm 10774 2016-02-08 19:50:04Z domschlo $
  2. #
  3. # See: http://www.fhemwiki.de/wiki/Jawbone_Up
  4. # Forum: http://forum.fhem.de/index.php/topic,24889.msg179505.html#msg179505
  5. package main;
  6. use strict;
  7. use warnings;
  8. use 5.14.0;
  9. use LWP::UserAgent 6;
  10. use IO::Socket::SSL;
  11. use WWW::Jawbone::Up;
  12. use Blocking;
  13. ############# Extensions to WWW:Jawbone::Up for bandevents entry point ############
  14. use constant URI_BASE => 'https://jawbone.com';
  15. use constant URI_API => URI_BASE . '/nudge/api/v.1.32';
  16. sub jawboneGetBandEvents($) {
  17. my ($up) = @_;
  18. my $options ||= {};
  19. # my $t0=time()-3600; # Time-intervalls lead to delay in update.
  20. # my $t1=time()+3600;
  21. # my $tt0="$t0";
  22. # my $tt1="$t1";
  23. # $options->{start_time} = $tt0;
  24. # $options->{end_time} = $tt1;
  25. my $json = $up->_get(URI_API . '/users/@me/bandevents', $options);
  26. return $json;
  27. }
  28. ######################################
  29. sub
  30. jawboneUp_Initialize($)
  31. {
  32. my ($hash) = @_;
  33. $hash->{DefFn} = "jawboneUp_Define";
  34. $hash->{NOTIFYDEV} = "global";
  35. $hash->{NotifyFn} = "jawboneUp_Notify";
  36. $hash->{UndefFn} = "jawboneUp_Undefine";
  37. #$hash->{SetFn} = "jawboneUp_Set";
  38. $hash->{GetFn} = "jawboneUp_Get";
  39. $hash->{AttrFn} = "jawboneUp_Attr";
  40. $hash->{AttrList} = "disable:1 ".
  41. "interval ".
  42. $readingFnAttributes;
  43. }
  44. #####################################
  45. my $min_poll = 300; # Minium poll reate of Jawbone API in seconds
  46. my $safe_poll = 900; # Safe default value in seconds.
  47. sub
  48. jawboneUp_Define($$)
  49. {
  50. my ($hash, $def) = @_;
  51. my @a = split("[ \t][ \t]*", $def);
  52. return "Usage: define <name> JawboneUp <user> <password> [<interval>]" if(@a < 4);
  53. my $name = $a[0];
  54. my $user = $a[2];
  55. my $password = $a[3];
  56. $hash->{"module_version"} = "0.1.4";
  57. $hash->{user}=$user;
  58. $hash->{password}=$password;
  59. $hash->{NAME} = $name;
  60. $hash->{"API_Failures"} = 0;
  61. $hash->{"API_Timeouts"} = 0;
  62. $hash->{"API_Success"} = 0;
  63. $hash->{"API_Status"} = "Initializing...";
  64. $hash->{INTERVAL} = 3600;
  65. if (defined($a[4])) {
  66. $hash->{INTERVAL} = $a[4];
  67. }
  68. if ($hash->{INTERVAL} < $min_poll) {
  69. $hash->{INTERVAL} = $min_poll;
  70. }
  71. delete($hash->{helper}{RUNNING_PID});
  72. $hash->{STATE} = "Initialized";
  73. if( $init_done ) {
  74. jawboneUp_Connect($hash);
  75. }
  76. return undef;
  77. }
  78. sub
  79. jawboneUp_Notify($$)
  80. {
  81. my ($hash,$dev) = @_;
  82. return if($dev->{NAME} ne "global");
  83. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  84. jawboneUp_Connect($hash);
  85. }
  86. sub
  87. jawboneUp_Connect($)
  88. {
  89. my ($hash) = @_;
  90. my $name = $hash->{NAME};
  91. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  92. jawboneUp_poll($hash);
  93. }
  94. sub
  95. jawboneUp_Disconnect($)
  96. {
  97. my ($hash) = @_;
  98. my $name = $hash->{NAME};
  99. RemoveInternalTimer($hash);
  100. $hash->{STATE} = "Disconnected";
  101. $hash->{"API_Status"} = "Disconnected";
  102. $hash->{"API_NextSchedule"} = "- - -";
  103. }
  104. sub
  105. jawboneUp_Undefine($$)
  106. {
  107. my ($hash, $arg) = @_;
  108. jawboneUp_Disconnect($hash);
  109. return undef;
  110. }
  111. sub
  112. jawboneUp_Set($$@)
  113. {
  114. my ($hash, $name, $cmd) = @_;
  115. my $list = "";
  116. return "Unknown argument $cmd, choose one of $list";
  117. }
  118. ############ Background Worker ################
  119. sub jawboneUp_DoBackground($)
  120. {
  121. my ($hash) = @_;
  122. # Expensive API-call:
  123. my $up = WWW::Jawbone::Up->connect($hash->{user}, $hash->{password});
  124. if (defined($up)) {
  125. # Expensive API-call:
  126. my $score = $up->score;
  127. my $na=$hash->{NAME};
  128. my $st="0"; my $ca="0";
  129. my $di="0"; my $bc="0";
  130. my $bd="0"; my $at="0";
  131. my $li="0";
  132. my $aw="0"; my $ak="0";
  133. my $lt="0"; my $ts="0";
  134. my $bt="0"; my $dp="0";
  135. my $as="0";
  136. $st=$score->{"move"}{"bg_steps"};
  137. $ca=$score->{"move"}{"calories"};
  138. $di=$score->{"move"}{"distance"};
  139. $bc=$score->{"move"}{"bmr_calories"};
  140. $bd=$score->{"move"}{"bmr_calories_day"};
  141. $at=$score->{"move"}{"active_time"};
  142. $li=$score->{"move"}{"longest_idle"};
  143. $aw=$score->{"sleep"}{"awake"};
  144. $ak=$score->{"sleep"}{"awakenings"};
  145. $lt=$score->{"sleep"}{"light"};
  146. $ts=$score->{"sleep"}{"time_to_sleep"};
  147. $bt=$score->{"sleep"}{"goals"}{"bedtime"}[0];
  148. $dp=$score->{"sleep"}{"goals"}{"deep"}[0];
  149. $as=$score->{"sleep"}{"goals"}{"total"}[0];
  150. if (not defined($st)) { $st="0" }
  151. if (not defined($ca)) { $ca="0" }
  152. if (not defined($di)) { $di="0" }
  153. if (not defined($bc)) { $bc="0" }
  154. if (not defined($bd)) { $bd="0" }
  155. if (not defined($at)) { $at="0" }
  156. if (not defined($li)) { $li="0" }
  157. if (not defined($aw)) { $aw="0" }
  158. if (not defined($ak)) { $ak="0" }
  159. if (not defined($lt)) { $lt="0" }
  160. if (not defined($ts)) { $ts="0" }
  161. if (not defined($bt)) { $bt="0" }
  162. if (not defined($dp)) { $dp="0" }
  163. if (not defined($as)) { $as="0" }
  164. # Second expensive call for band events
  165. my $json=jawboneGetBandEvents($up);
  166. my $nr=0;
  167. $nr=$json->{"data"}->{"size"};
  168. #my $json="";
  169. #my $nr=0;
  170. my $sl=0; # sleep-mode
  171. my $sw=0; # stopwatch-mode
  172. for (my $i=0; $i<$nr; $i++) {
  173. # my $tx=localtime($json->{"data"}->{"items"}[$i]->{"time_created"});
  174. my $act="";
  175. $act = $json->{"data"}->{"items"}[$i]->{"action"};
  176. if (not defined($act)) { $act="" }
  177. if ($act eq "enter_sleep_mode")
  178. {
  179. $sl=1;
  180. last;
  181. }
  182. if ($act eq "exit_sleep_mode")
  183. {
  184. $sl=0;
  185. last;
  186. }
  187. }
  188. for (my $i=0; $i<$nr; $i++) {
  189. # my $tx=localtime($json->{"data"}->{"items"}[$i]->{"time_created"});
  190. my $act="";
  191. $act = $json->{"data"}->{"items"}[$i]->{"action"};
  192. if (not defined($act)) { $act="" }
  193. if ($act eq "enter_stopwatch_mode")
  194. {
  195. $sw=1;
  196. last;
  197. }
  198. if ($act eq "exit_stopwatch_mode")
  199. {
  200. $sw=0;
  201. last;
  202. }
  203. }
  204. return "OK|$na|$st|$ca|$di|$bc|$bd|$at|$li|$aw|$as|$sl|$sw|$ak|$lt|$ts|$bt|$dp";
  205. }
  206. #Error: API doesn't return any information about errors...
  207. my $na=$hash->{NAME};
  208. return "ERR|$na";
  209. }
  210. ############ Accept result from background process: ##############
  211. sub updReading($$$) {
  212. my ($hash,$name,$val) = @_;
  213. if (defined($val)) {
  214. if ($hash->{READINGS}{$name}{VAL} != $val) {
  215. readingsBulkUpdate($hash,$name,$val,1);
  216. }
  217. }
  218. }
  219. sub jawboneUp_DoneBackground($)
  220. {
  221. my ($string) = @_;
  222. if (!defined($string)) {
  223. # Internal error.
  224. print ("Internal error at DoneBackground (0x001).\n");
  225. return undef;
  226. }
  227. my @a = split("\\|",$string);
  228. if (@a < 2) {
  229. print ("Internal error at DoneBackground (0x002).\n");
  230. return undef;
  231. }
  232. my $hash = $defs{$a[1]};
  233. delete($hash->{helper}{RUNNING_PID});
  234. if ($a[0] eq "ERR") {
  235. $hash->{"API_LastError"} = FmtDateTime(gettimeofday());
  236. $hash->{"API_Status"} = "API Failure. Check credentials and internet connectivity, retrying...";
  237. $hash->{"API_Success"} = 0;
  238. $hash->{"API_Failures"} = $hash->{"API_Failures"}+1;
  239. if ($hash->{"API_Failures"} > 2) {
  240. $hash->{STATE} = "Disconnected - disabled";
  241. $attr{$hash->{NAME}}{"disable"} = 1;
  242. RemoveInternalTimer($hash);
  243. $hash->{"API_NextSchedule"} = "- - -";
  244. $hash->{"API_Status"} = "API Failure. Check credentials and internet connectivity, disabled. (Use manual 'get update' to re-enable.)";
  245. } else {
  246. $hash->{STATE} = "Connect-failure, retries: ".$hash->{"API_Failures"};
  247. }
  248. } else {
  249. if (@a < 18) {
  250. print ("Internal error at DoneBackground (0x003).\n");
  251. $hash->{STATE} = "Disconnected - disabled";
  252. $attr{$hash->{NAME}}{"disable"} = 1;
  253. RemoveInternalTimer($hash);
  254. $hash->{"API_NextSchedule"} = "- - -";
  255. $hash->{"API_Status"} = "API Failure. Unexpected format of return values: )".$string;
  256. return undef;
  257. }
  258. readingsBeginUpdate($hash);
  259. updReading($hash,"bg_steps",$a[2]);
  260. updReading($hash,"calories",$a[3]);
  261. updReading($hash,"distance",$a[4]);
  262. updReading($hash,"bmr_calories",$a[5]);
  263. updReading($hash,"bmr_calories_day",$a[6]);
  264. updReading($hash,"active_time",$a[7]);
  265. updReading($hash,"longest_idle",$a[8]);
  266. updReading($hash,"sleep_awake",$a[9]);
  267. updReading($hash,"sleep_asleep",$a[10]);
  268. updReading($hash,"sleep_mode",$a[11]);
  269. updReading($hash,"stopwatch_mode",$a[12]);
  270. updReading($hash,"awakenings",$a[13]);
  271. updReading($hash,"light",$a[14]);
  272. updReading($hash,"time_to_sleep",$a[15]);
  273. updReading($hash,"bedtime",$a[16]);
  274. updReading($hash,"deep",$a[17]);
  275. readingsEndUpdate($hash, 1);
  276. $hash->{LAST_POLL} = FmtDateTime( gettimeofday() );
  277. $hash->{STATE} = "Connected";
  278. $hash->{"API_Success"} = $hash->{"API_Success"}+1;
  279. $hash->{"API_Status"} = "API OK Success.";
  280. $hash->{"API_LastSuccess"} = FmtDateTime(gettimeofday());
  281. }
  282. return undef;
  283. }
  284. ############ Background Worker timeout #########################
  285. sub jawboneUp_AbortBackground($)
  286. {
  287. my ($hash) = @_;
  288. delete($hash->{helper}{RUNNING_PID});
  289. $hash->{"API_Timeouts"} = $hash->{"API_Timeouts"}+1;
  290. $hash->{STATE} = "Timeout";
  291. $hash->{"API_Status"} = "Timeout, retrying...";
  292. $hash->{"API_LastError"} = FmtDateTime(gettimeofday());
  293. return undef if( AttrVal($hash->{NAME}, "disable", 0 ) == 1 );
  294. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "jawboneUp_poll", $hash, 0);
  295. $hash->{"API_NextSchedule"} = FmtDateTime(gettimeofday()+$hash->{INTERVAL});
  296. return undef;
  297. }
  298. # Request update from Jawbone servers by spawning a background task (via BlockingCall)
  299. sub
  300. jawboneUp_poll($)
  301. {
  302. my ($hash) = @_;
  303. my $name = $hash->{NAME};
  304. RemoveInternalTimer($hash);
  305. $hash->{"API_NextSchedule"} = "- - -";
  306. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  307. # Getting values from Jawbone server sometimes takes several seconds - therefore we background the request.
  308. if (exists($hash->{helper}{RUNNING_PID})) {
  309. $hash->{"API_ReentranceAvoided"} = $hash->{"API_ReentranceAvoided"}+1;
  310. if ($hash->{"API_ReentranceAvoided"} > 1) {
  311. $hash->{"API_ReentranceAvoided"} = 0;
  312. $hash->{"API_Failures"} = $hash->{"API_Failures"}+1;
  313. $hash->{"API_Status"} = "Reentrance-Problem, retrying...";
  314. # This is potentially dangerous, because it cannot be verified if the old process is still running,
  315. # However there were cases when neither the Abort nor the Done callback were activitated, leading
  316. # to a stall of the module
  317. delete($hash->{helper}{RUNNING_PID});
  318. }
  319. } else {
  320. $hash->{helper}{RUNNING_PID} = BlockingCall("jawboneUp_DoBackground",$hash,"jawboneUp_DoneBackground",60,"jawboneUp_AbortBackground",$hash);
  321. }
  322. return undef if( AttrVal($hash->{NAME}, "disable", 0 ) == 1 );
  323. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "jawboneUp_poll", $hash, 0);
  324. $hash->{"API_NextSchedule"} = FmtDateTime(gettimeofday()+$hash->{INTERVAL});
  325. return undef;
  326. }
  327. sub
  328. jawboneUp_Get($$@)
  329. {
  330. my ($hash, $name, $cmd) = @_;
  331. my $list = "update:noArg";
  332. if( $cmd eq "update" ) {
  333. if ( AttrVal($hash->{NAME}, "disable", 0 ) == 1 ) {
  334. $attr{$hash->{NAME}}{"disable"} = 0;
  335. }
  336. jawboneUp_poll($hash);
  337. return undef;
  338. }
  339. return "Unknown argument $cmd, choose one of $list";
  340. }
  341. sub
  342. jawboneUp_Attr($$$)
  343. {
  344. my ($cmd, $name, $attrName, $attrVal) = @_;
  345. my $orig = $attrVal;
  346. $attrVal = int($attrVal) if($attrName eq "interval");
  347. $attrVal = $safe_poll if($attrName eq "interval" && $attrVal < $min_poll && $attrVal != 0);
  348. if( $attrName eq "interval" ) {
  349. my $hash = $defs{$name};
  350. $hash->{INTERVAL} = $attrVal;
  351. $hash->{INTERVAL} = $safe_poll if( !$attrVal );
  352. } elsif( $attrName eq "disable" ) {
  353. my $hash = $defs{$name};
  354. RemoveInternalTimer($hash);
  355. $hash->{"API_NextSchedule"} = "- - -";
  356. if( $cmd eq "set" && $attrVal ne "0" ) {
  357. } else {
  358. $attr{$name}{$attrName} = 0;
  359. jawboneUp_poll($hash);
  360. }
  361. }
  362. if( $cmd eq "set" ) {
  363. if( $orig ne $attrVal ) {
  364. $attr{$name}{$attrName} = $attrVal;
  365. return $attrName ." set to ". $attrVal;
  366. }
  367. }
  368. return;}
  369. 1;
  370. =pod
  371. =begin html
  372. <a name="JawboneUp"></a>
  373. <h3>JawboneUp</h3>
  374. <ul>
  375. This module supports the Jawbone Up[24] fitness tracker. The module collects calories, steps and distance walked (and a few other metrics) on a given day.<br><br>
  376. All communication with the Jawbone services is handled as background-tasks, in order not to interfere with other FHEM services.
  377. <br><br>
  378. <b>Installation</b>
  379. Among the perl modules required for this module are: LWP::UserAgent, IO::Socket::SSL, WWW::Jawbone::Up.<br>
  380. At least WWW:Jawbone::Up doesn't seem to have a debian equivalent, so you'll need CPAN to install the modules.<br>
  381. Example: <code>cpan -i WWW::Jawbone::Up</code> should install the required perl modules for the Jawbone up.<br>
  382. Unfortunately the WWW::Jawbone::Up module relies on quite a number of dependencies, so in case of error, check the CPAN output for missing modules.<br>
  383. Some dependent modules might fail during self-test, in that case try a forced install: <code>cpan -i -f module-name</code>
  384. <br><br>
  385. <b>Error handling</b>
  386. If there are more than three consecutive API errors, the module disables itself. A "get update" re-enables the module.<br>
  387. API errors can be caused by wrong credentials or missing internet-connectivity or by a failure of the Jawbone server.<br><br>
  388. <b>Configuration</b>
  389. <a name="jawboneUp_Define"></a>
  390. <b>Define</b>
  391. <ul>
  392. <code>define &lt;name&gt; JawboneUp &lt;user&gt; &lt;password&gt; [&lt;interval&gt;] </code><br>
  393. <br>
  394. Defines a JawboneUp device.<br>
  395. <b>Parameters</b>
  396. <ul>
  397. <li>name<br>
  398. A name for your jawbone device.</li>
  399. <li>user<br>
  400. Username (email) used as account-name for the jawbone service.</li>
  401. <li>password<br>
  402. The password for the jawbone service.</li>
  403. <li>interval<br>
  404. Optional polling intervall in seconds. Default is 3600, minimum is 300 (=5min). It is not recommended to go below 900sec.</li>
  405. </ul><br>
  406. Example:
  407. <ul>
  408. <code>define myJawboneUp JawboneUp me@foo.org myS3cret 3600</code><br>
  409. <code>attr myJawboneUp room Jawbone</code><br>
  410. </ul>
  411. </ul><br>
  412. <a name="jawboneUp_Readings"></a>
  413. <b>Readings</b>
  414. <ul>
  415. <li>active_time<br>
  416. (Active time (seconds))</li>
  417. <li>bg_steps<br>
  418. (Step count)</li>
  419. <li>bmr_calories<br>
  420. (Resting calories)</li>
  421. <li>bmr_calories_day<br>
  422. (Average daily calories (without activities))</li>
  423. <li>calories<br>
  424. (Activity calories)</li>
  425. <li>distance<br>
  426. (Distance in km)</li>
  427. <li>longest_idle<br>
  428. (Inactive time in seconds)<br></li>
  429. <li>sleep_awake<br>
  430. (Awake time during sleep in seconds)</li>
  431. <li>sleep_asleep<br>
  432. (Actual sleep during sleep period, time in seconds)</li>
  433. <li>awakenings<br>
  434. (Awakenings)</li>
  435. <li>light<br>
  436. (Light sleep during sleep period, time in seconds)</li>
  437. <li>time_to_sleep<br>
  438. (Time to fall asleep in seconds)</li>
  439. <li>bedtime<br>
  440. (Time in bed)</li>
  441. <li>deep<br>
  442. (Deep sleep in seconds)</li>
  443. <li>awake<br>
  444. (Time awake in seconds)</li>
  445. <li>sleep_mode<br>
  446. (0: sleep mode inactive, 1: sleep mode active) Note: this is not real-time since updates depend on the module's poll-intervall</li>
  447. <li>stopwatch_mode<br>
  448. (0: not in stopwatch mode, 1: stopwatch mode active) Note: not suitable for real-time measurements for the reasons above.</li>
  449. </ul><br>
  450. <a name="jawboneUp_Get"></a>
  451. <b>Get</b>
  452. <ul>
  453. <li>update<br>
  454. trigger an update</li>
  455. </ul><br>
  456. <a name="jawboneUp_Attr"></a>
  457. <b>Attributes</b>
  458. <ul>
  459. <li>interval<br>
  460. the interval in seconds for updates. the default ist 3600 (=1h), minimum is 300 (=5min). It is not recommended to go below 900sec.</li>
  461. <li>disable<br>
  462. 1 -> disconnect and stop polling</li>
  463. </ul>
  464. </ul>
  465. =end html
  466. =cut