# $Id: $ # # 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; ############# 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 JawboneUp []" if(@a < 4); my $name = $a[0]; my $user = $a[2]; my $password = $a[3]; $hash->{"module_version"} = "0.1.3"; $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=$score->{"move"}{"bg_steps"}; my $ca=$score->{"move"}{"calories"}; my $di=$score->{"move"}{"distance"}; my $bc=$score->{"move"}{"bmr_calories"}; my $bd=$score->{"move"}{"bmr_calories_day"}; my $at=$score->{"move"}{"active_time"}; my $li=$score->{"move"}{"longest_idle"}; my $aw=$score->{"sleep"}{"awake"}; my $ak=$score->{"sleep"}{"awakenings"}; my $lt=$score->{"sleep"}{"light"}; my $ts=$score->{"sleep"}{"time_to_sleep"}; my $bt=$score->{"sleep"}{"goals"}{"bedtime"}[0]; my $dp=$score->{"sleep"}{"goals"}{"deep"}[0]; my $as=$score->{"sleep"}{"goals"}{"total"}[0]; # Second expensive call for band events my $json=jawboneGetBandEvents($up); my $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 = $json->{"data"}->{"items"}[$i]->{"action"}; 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 = $json->{"data"}->{"items"}[$i]->{"action"}; 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 ($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

JawboneUp

    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.

    All communication with the Jawbone services is handled as background-tasks, in order not to interfere with other FHEM services.

    Installation Among the perl modules required for this module are: LWP::UserAgent, IO::Socket::SSL, WWW::Jawbone::Up.
    At least WWW:Jawbone::Up doesn't seem to have a debian equivalent, so you'll need CPAN to install the modules.
    Example: cpan -i WWW::Jawbone::Up should install the required perl modules for the Jawbone up.
    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.
    Some dependent modules might fail during self-test, in that case try a forced install: cpan -i -f module-name

    Error handling If there are more than three consecutive API errors, the module disables itself. A "get update" re-enables the module.
    API errors can be caused by wrong credentials or missing internet-connectivity or by a failure of the Jawbone server.

    Configuration Define
      define <name> JawboneUp <user> <password> [<interval>]

      Defines a JawboneUp device.
      Parameters
      • name
        A name for your jawbone device.
      • user
        Username (email) used as account-name for the jawbone service.
      • password
        The password for the jawbone service.
      • interval
        Optional polling intervall in seconds. Default is 3600, minimum is 300 (=5min). It is not recommended to go below 900sec.

      Example:
        define myJawboneUp JawboneUp me@foo.org myS3cret 3600
        attr myJawboneUp room Jawbone

    Readings
    • active_time
      (Active time (seconds))
    • bg_steps
      (Step count)
    • bmr_calories
      (Resting calories)
    • bmr_calories_day
      (Average daily calories (without activities))
    • calories
      (Activity calories)
    • distance
      (Distance in km)
    • longest_idle
      (Inactive time in seconds)
    • sleep_awake
      (Awake time during sleep in seconds)
    • sleep_asleep
      (Actual sleep during sleep period, time in seconds)
    • awakenings
      (Awakenings)
    • light
      (Light sleep during sleep period, time in seconds)
    • time_to_sleep
      (Time to fall asleep in seconds)
    • bedtime
      (Time in bed)
    • deep
      (Deep sleep in seconds)
    • awake
      (Time awake in seconds)
    • sleep_mode
      (0: sleep mode inactive, 1: sleep mode active) Note: this is not real-time since updates depend on the module's poll-intervall
    • stopwatch_mode
      (0: not in stopwatch mode, 1: stopwatch mode active) Note: not suitable for real-time measurements for the reasons above.

    Get
    • update
      trigger an update

    Attributes
    • interval
      the interval in seconds for updates. the default ist 3600 (=1h), minimum is 300 (=5min). It is not recommended to go below 900sec.
    • disable
      1 -> disconnect and stop polling
=end html =cut