| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- # $Id: 38_JawboneUp.pm 10774 2016-02-08 19:50:04Z domschlo $
- #
- # See: http://www.fhemwiki.de/wiki/Jawbone_Up
- # Forum: http://forum.fhem.de/index.php/topic,24889.msg179505.html#msg179505
- package main;
- use strict;
- use warnings;
- use 5.14.0;
- use LWP::UserAgent 6;
- use IO::Socket::SSL;
- use WWW::Jawbone::Up;
- use Blocking;
- ############# Extensions to WWW:Jawbone::Up for bandevents entry point ############
- use constant URI_BASE => 'https://jawbone.com';
- use constant URI_API => URI_BASE . '/nudge/api/v.1.32';
- sub jawboneGetBandEvents($) {
- my ($up) = @_;
- my $options ||= {};
- # my $t0=time()-3600; # Time-intervalls lead to delay in update.
- # my $t1=time()+3600;
- # my $tt0="$t0";
- # my $tt1="$t1";
- # $options->{start_time} = $tt0;
- # $options->{end_time} = $tt1;
- my $json = $up->_get(URI_API . '/users/@me/bandevents', $options);
- return $json;
- }
- ######################################
- sub
- jawboneUp_Initialize($)
- {
- my ($hash) = @_;
- $hash->{DefFn} = "jawboneUp_Define";
- $hash->{NOTIFYDEV} = "global";
- $hash->{NotifyFn} = "jawboneUp_Notify";
- $hash->{UndefFn} = "jawboneUp_Undefine";
- #$hash->{SetFn} = "jawboneUp_Set";
- $hash->{GetFn} = "jawboneUp_Get";
- $hash->{AttrFn} = "jawboneUp_Attr";
- $hash->{AttrList} = "disable:1 ".
- "interval ".
- $readingFnAttributes;
- }
- #####################################
- my $min_poll = 300; # Minium poll reate of Jawbone API in seconds
- my $safe_poll = 900; # Safe default value in seconds.
- sub
- jawboneUp_Define($$)
- {
- my ($hash, $def) = @_;
- my @a = split("[ \t][ \t]*", $def);
- return "Usage: define <name> JawboneUp <user> <password> [<interval>]" if(@a < 4);
- my $name = $a[0];
- my $user = $a[2];
- my $password = $a[3];
- $hash->{"module_version"} = "0.1.4";
- $hash->{user}=$user;
- $hash->{password}=$password;
- $hash->{NAME} = $name;
- $hash->{"API_Failures"} = 0;
- $hash->{"API_Timeouts"} = 0;
- $hash->{"API_Success"} = 0;
- $hash->{"API_Status"} = "Initializing...";
- $hash->{INTERVAL} = 3600;
- if (defined($a[4])) {
- $hash->{INTERVAL} = $a[4];
- }
- if ($hash->{INTERVAL} < $min_poll) {
- $hash->{INTERVAL} = $min_poll;
- }
- delete($hash->{helper}{RUNNING_PID});
- $hash->{STATE} = "Initialized";
- if( $init_done ) {
- jawboneUp_Connect($hash);
- }
- return undef;
- }
- sub
- jawboneUp_Notify($$)
- {
- my ($hash,$dev) = @_;
- return if($dev->{NAME} ne "global");
- return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
- jawboneUp_Connect($hash);
- }
- sub
- jawboneUp_Connect($)
- {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- return undef if( AttrVal($name, "disable", 0 ) == 1 );
- jawboneUp_poll($hash);
- }
- sub
- jawboneUp_Disconnect($)
- {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- RemoveInternalTimer($hash);
- $hash->{STATE} = "Disconnected";
- $hash->{"API_Status"} = "Disconnected";
- $hash->{"API_NextSchedule"} = "- - -";
- }
- sub
- jawboneUp_Undefine($$)
- {
- my ($hash, $arg) = @_;
- jawboneUp_Disconnect($hash);
- return undef;
- }
- sub
- jawboneUp_Set($$@)
- {
- my ($hash, $name, $cmd) = @_;
- my $list = "";
- return "Unknown argument $cmd, choose one of $list";
- }
- ############ Background Worker ################
- sub jawboneUp_DoBackground($)
- {
- my ($hash) = @_;
- # Expensive API-call:
- my $up = WWW::Jawbone::Up->connect($hash->{user}, $hash->{password});
- if (defined($up)) {
- # Expensive API-call:
- my $score = $up->score;
- my $na=$hash->{NAME};
- my $st="0"; my $ca="0";
- my $di="0"; my $bc="0";
- my $bd="0"; my $at="0";
- my $li="0";
- my $aw="0"; my $ak="0";
- my $lt="0"; my $ts="0";
- my $bt="0"; my $dp="0";
- my $as="0";
- $st=$score->{"move"}{"bg_steps"};
- $ca=$score->{"move"}{"calories"};
- $di=$score->{"move"}{"distance"};
- $bc=$score->{"move"}{"bmr_calories"};
- $bd=$score->{"move"}{"bmr_calories_day"};
- $at=$score->{"move"}{"active_time"};
- $li=$score->{"move"}{"longest_idle"};
- $aw=$score->{"sleep"}{"awake"};
- $ak=$score->{"sleep"}{"awakenings"};
- $lt=$score->{"sleep"}{"light"};
- $ts=$score->{"sleep"}{"time_to_sleep"};
- $bt=$score->{"sleep"}{"goals"}{"bedtime"}[0];
- $dp=$score->{"sleep"}{"goals"}{"deep"}[0];
- $as=$score->{"sleep"}{"goals"}{"total"}[0];
- if (not defined($st)) { $st="0" }
- if (not defined($ca)) { $ca="0" }
- if (not defined($di)) { $di="0" }
- if (not defined($bc)) { $bc="0" }
- if (not defined($bd)) { $bd="0" }
- if (not defined($at)) { $at="0" }
- if (not defined($li)) { $li="0" }
- if (not defined($aw)) { $aw="0" }
- if (not defined($ak)) { $ak="0" }
- if (not defined($lt)) { $lt="0" }
- if (not defined($ts)) { $ts="0" }
- if (not defined($bt)) { $bt="0" }
- if (not defined($dp)) { $dp="0" }
- if (not defined($as)) { $as="0" }
- # Second expensive call for band events
- my $json=jawboneGetBandEvents($up);
- my $nr=0;
- $nr=$json->{"data"}->{"size"};
- #my $json="";
- #my $nr=0;
- my $sl=0; # sleep-mode
- my $sw=0; # stopwatch-mode
- for (my $i=0; $i<$nr; $i++) {
- # my $tx=localtime($json->{"data"}->{"items"}[$i]->{"time_created"});
- my $act="";
- $act = $json->{"data"}->{"items"}[$i]->{"action"};
- if (not defined($act)) { $act="" }
- if ($act eq "enter_sleep_mode")
- {
- $sl=1;
- last;
- }
- if ($act eq "exit_sleep_mode")
- {
- $sl=0;
- last;
- }
- }
- for (my $i=0; $i<$nr; $i++) {
- # my $tx=localtime($json->{"data"}->{"items"}[$i]->{"time_created"});
- my $act="";
- $act = $json->{"data"}->{"items"}[$i]->{"action"};
- if (not defined($act)) { $act="" }
- if ($act eq "enter_stopwatch_mode")
- {
- $sw=1;
- last;
- }
- if ($act eq "exit_stopwatch_mode")
- {
- $sw=0;
- last;
- }
- }
- return "OK|$na|$st|$ca|$di|$bc|$bd|$at|$li|$aw|$as|$sl|$sw|$ak|$lt|$ts|$bt|$dp";
- }
- #Error: API doesn't return any information about errors...
- my $na=$hash->{NAME};
- return "ERR|$na";
- }
- ############ Accept result from background process: ##############
- sub updReading($$$) {
- my ($hash,$name,$val) = @_;
- if (defined($val)) {
- if ($hash->{READINGS}{$name}{VAL} != $val) {
- readingsBulkUpdate($hash,$name,$val,1);
- }
- }
- }
- sub jawboneUp_DoneBackground($)
- {
- my ($string) = @_;
- if (!defined($string)) {
- # Internal error.
- print ("Internal error at DoneBackground (0x001).\n");
- return undef;
- }
- my @a = split("\\|",$string);
- if (@a < 2) {
- print ("Internal error at DoneBackground (0x002).\n");
- return undef;
- }
- my $hash = $defs{$a[1]};
- delete($hash->{helper}{RUNNING_PID});
- if ($a[0] eq "ERR") {
- $hash->{"API_LastError"} = FmtDateTime(gettimeofday());
- $hash->{"API_Status"} = "API Failure. Check credentials and internet connectivity, retrying...";
- $hash->{"API_Success"} = 0;
- $hash->{"API_Failures"} = $hash->{"API_Failures"}+1;
- if ($hash->{"API_Failures"} > 2) {
- $hash->{STATE} = "Disconnected - disabled";
- $attr{$hash->{NAME}}{"disable"} = 1;
- RemoveInternalTimer($hash);
- $hash->{"API_NextSchedule"} = "- - -";
- $hash->{"API_Status"} = "API Failure. Check credentials and internet connectivity, disabled. (Use manual 'get update' to re-enable.)";
- } else {
- $hash->{STATE} = "Connect-failure, retries: ".$hash->{"API_Failures"};
- }
- } else {
- if (@a < 18) {
- print ("Internal error at DoneBackground (0x003).\n");
- $hash->{STATE} = "Disconnected - disabled";
- $attr{$hash->{NAME}}{"disable"} = 1;
- RemoveInternalTimer($hash);
- $hash->{"API_NextSchedule"} = "- - -";
- $hash->{"API_Status"} = "API Failure. Unexpected format of return values: )".$string;
- return undef;
- }
- readingsBeginUpdate($hash);
- updReading($hash,"bg_steps",$a[2]);
- updReading($hash,"calories",$a[3]);
- updReading($hash,"distance",$a[4]);
- updReading($hash,"bmr_calories",$a[5]);
- updReading($hash,"bmr_calories_day",$a[6]);
- updReading($hash,"active_time",$a[7]);
- updReading($hash,"longest_idle",$a[8]);
- updReading($hash,"sleep_awake",$a[9]);
- updReading($hash,"sleep_asleep",$a[10]);
- updReading($hash,"sleep_mode",$a[11]);
- updReading($hash,"stopwatch_mode",$a[12]);
- updReading($hash,"awakenings",$a[13]);
- updReading($hash,"light",$a[14]);
- updReading($hash,"time_to_sleep",$a[15]);
- updReading($hash,"bedtime",$a[16]);
- updReading($hash,"deep",$a[17]);
- readingsEndUpdate($hash, 1);
- $hash->{LAST_POLL} = FmtDateTime( gettimeofday() );
- $hash->{STATE} = "Connected";
- $hash->{"API_Success"} = $hash->{"API_Success"}+1;
- $hash->{"API_Status"} = "API OK Success.";
- $hash->{"API_LastSuccess"} = FmtDateTime(gettimeofday());
- }
- return undef;
- }
- ############ Background Worker timeout #########################
- sub jawboneUp_AbortBackground($)
- {
- my ($hash) = @_;
- delete($hash->{helper}{RUNNING_PID});
- $hash->{"API_Timeouts"} = $hash->{"API_Timeouts"}+1;
- $hash->{STATE} = "Timeout";
- $hash->{"API_Status"} = "Timeout, retrying...";
- $hash->{"API_LastError"} = FmtDateTime(gettimeofday());
- return undef if( AttrVal($hash->{NAME}, "disable", 0 ) == 1 );
- InternalTimer(gettimeofday()+$hash->{INTERVAL}, "jawboneUp_poll", $hash, 0);
- $hash->{"API_NextSchedule"} = FmtDateTime(gettimeofday()+$hash->{INTERVAL});
- return undef;
- }
- # Request update from Jawbone servers by spawning a background task (via BlockingCall)
- sub
- jawboneUp_poll($)
- {
- my ($hash) = @_;
- my $name = $hash->{NAME};
- RemoveInternalTimer($hash);
- $hash->{"API_NextSchedule"} = "- - -";
- return undef if( AttrVal($name, "disable", 0 ) == 1 );
- # Getting values from Jawbone server sometimes takes several seconds - therefore we background the request.
- if (exists($hash->{helper}{RUNNING_PID})) {
- $hash->{"API_ReentranceAvoided"} = $hash->{"API_ReentranceAvoided"}+1;
- if ($hash->{"API_ReentranceAvoided"} > 1) {
- $hash->{"API_ReentranceAvoided"} = 0;
- $hash->{"API_Failures"} = $hash->{"API_Failures"}+1;
- $hash->{"API_Status"} = "Reentrance-Problem, retrying...";
- # This is potentially dangerous, because it cannot be verified if the old process is still running,
- # However there were cases when neither the Abort nor the Done callback were activitated, leading
- # to a stall of the module
- delete($hash->{helper}{RUNNING_PID});
- }
- } else {
- $hash->{helper}{RUNNING_PID} = BlockingCall("jawboneUp_DoBackground",$hash,"jawboneUp_DoneBackground",60,"jawboneUp_AbortBackground",$hash);
- }
- return undef if( AttrVal($hash->{NAME}, "disable", 0 ) == 1 );
- InternalTimer(gettimeofday()+$hash->{INTERVAL}, "jawboneUp_poll", $hash, 0);
- $hash->{"API_NextSchedule"} = FmtDateTime(gettimeofday()+$hash->{INTERVAL});
- return undef;
- }
- sub
- jawboneUp_Get($$@)
- {
- my ($hash, $name, $cmd) = @_;
- my $list = "update:noArg";
- if( $cmd eq "update" ) {
- if ( AttrVal($hash->{NAME}, "disable", 0 ) == 1 ) {
- $attr{$hash->{NAME}}{"disable"} = 0;
- }
- jawboneUp_poll($hash);
- return undef;
- }
- return "Unknown argument $cmd, choose one of $list";
- }
- sub
- jawboneUp_Attr($$$)
- {
- my ($cmd, $name, $attrName, $attrVal) = @_;
- my $orig = $attrVal;
- $attrVal = int($attrVal) if($attrName eq "interval");
- $attrVal = $safe_poll if($attrName eq "interval" && $attrVal < $min_poll && $attrVal != 0);
- if( $attrName eq "interval" ) {
- my $hash = $defs{$name};
- $hash->{INTERVAL} = $attrVal;
- $hash->{INTERVAL} = $safe_poll if( !$attrVal );
- } elsif( $attrName eq "disable" ) {
- my $hash = $defs{$name};
- RemoveInternalTimer($hash);
- $hash->{"API_NextSchedule"} = "- - -";
- if( $cmd eq "set" && $attrVal ne "0" ) {
- } else {
- $attr{$name}{$attrName} = 0;
- jawboneUp_poll($hash);
- }
- }
- if( $cmd eq "set" ) {
- if( $orig ne $attrVal ) {
- $attr{$name}{$attrName} = $attrVal;
- return $attrName ." set to ". $attrVal;
- }
- }
- return;}
- 1;
- =pod
- =begin html
- <a name="JawboneUp"></a>
- <h3>JawboneUp</h3>
- <ul>
- 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>
- All communication with the Jawbone services is handled as background-tasks, in order not to interfere with other FHEM services.
- <br><br>
- <b>Installation</b>
- Among the perl modules required for this module are: LWP::UserAgent, IO::Socket::SSL, WWW::Jawbone::Up.<br>
- At least WWW:Jawbone::Up doesn't seem to have a debian equivalent, so you'll need CPAN to install the modules.<br>
- Example: <code>cpan -i WWW::Jawbone::Up</code> should install the required perl modules for the Jawbone up.<br>
- 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>
- Some dependent modules might fail during self-test, in that case try a forced install: <code>cpan -i -f module-name</code>
- <br><br>
- <b>Error handling</b>
- If there are more than three consecutive API errors, the module disables itself. A "get update" re-enables the module.<br>
- API errors can be caused by wrong credentials or missing internet-connectivity or by a failure of the Jawbone server.<br><br>
- <b>Configuration</b>
- <a name="jawboneUp_Define"></a>
- <b>Define</b>
- <ul>
- <code>define <name> JawboneUp <user> <password> [<interval>] </code><br>
- <br>
- Defines a JawboneUp device.<br>
- <b>Parameters</b>
- <ul>
- <li>name<br>
- A name for your jawbone device.</li>
- <li>user<br>
- Username (email) used as account-name for the jawbone service.</li>
- <li>password<br>
- The password for the jawbone service.</li>
- <li>interval<br>
- Optional polling intervall in seconds. Default is 3600, minimum is 300 (=5min). It is not recommended to go below 900sec.</li>
- </ul><br>
- Example:
- <ul>
- <code>define myJawboneUp JawboneUp me@foo.org myS3cret 3600</code><br>
- <code>attr myJawboneUp room Jawbone</code><br>
- </ul>
- </ul><br>
- <a name="jawboneUp_Readings"></a>
- <b>Readings</b>
- <ul>
- <li>active_time<br>
- (Active time (seconds))</li>
- <li>bg_steps<br>
- (Step count)</li>
- <li>bmr_calories<br>
- (Resting calories)</li>
- <li>bmr_calories_day<br>
- (Average daily calories (without activities))</li>
- <li>calories<br>
- (Activity calories)</li>
- <li>distance<br>
- (Distance in km)</li>
- <li>longest_idle<br>
- (Inactive time in seconds)<br></li>
- <li>sleep_awake<br>
- (Awake time during sleep in seconds)</li>
- <li>sleep_asleep<br>
- (Actual sleep during sleep period, time in seconds)</li>
- <li>awakenings<br>
- (Awakenings)</li>
- <li>light<br>
- (Light sleep during sleep period, time in seconds)</li>
- <li>time_to_sleep<br>
- (Time to fall asleep in seconds)</li>
- <li>bedtime<br>
- (Time in bed)</li>
- <li>deep<br>
- (Deep sleep in seconds)</li>
- <li>awake<br>
- (Time awake in seconds)</li>
- <li>sleep_mode<br>
- (0: sleep mode inactive, 1: sleep mode active) Note: this is not real-time since updates depend on the module's poll-intervall</li>
- <li>stopwatch_mode<br>
- (0: not in stopwatch mode, 1: stopwatch mode active) Note: not suitable for real-time measurements for the reasons above.</li>
- </ul><br>
- <a name="jawboneUp_Get"></a>
- <b>Get</b>
- <ul>
- <li>update<br>
- trigger an update</li>
- </ul><br>
- <a name="jawboneUp_Attr"></a>
- <b>Attributes</b>
- <ul>
- <li>interval<br>
- the interval in seconds for updates. the default ist 3600 (=1h), minimum is 300 (=5min). It is not recommended to go below 900sec.</li>
- <li>disable<br>
- 1 -> disconnect and stop polling</li>
- </ul>
- </ul>
- =end html
- =cut
|