diff --git a/fhem/FHEM/38_JawboneUp.pm b/fhem/FHEM/38_JawboneUp.pm new file mode 100644 index 000000000..a9fff7cf8 --- /dev/null +++ b/fhem/FHEM/38_JawboneUp.pm @@ -0,0 +1,403 @@ +# $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; + +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; +} + +##################################### + +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.1"; + + $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} < 900) { + $hash->{INTERVAL} = 900; + } + + 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"}; + return "OK|$na|$st|$ca|$di|$bc|$bd|$at|$li"; + } + #Error: API doesn't return any information about errors... + my $na=$hash->{NAME}; + return "ERR|$na"; +} + +############ Accept result from background process: ############## +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 < 9) { + 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; + } + readingsSingleUpdate($hash,"bg_steps",$a[2],1); + readingsSingleUpdate($hash,"calories",$a[3],1); + readingsSingleUpdate($hash,"distance",$a[4],1); + readingsSingleUpdate($hash,"bmr_calories",$a[5],1); + readingsSingleUpdate($hash,"bmr_calories_day",$a[6],1); + readingsSingleUpdate($hash,"active_time",$a[7],1); + readingsSingleUpdate($hash,"longest_idle",$a[8],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 = 900 if($attrName eq "interval" && $attrVal < 900 && $attrVal != 0); + + if( $attrName eq "interval" ) { + my $hash = $defs{$name}; + $hash->{INTERVAL} = $attrVal; + $hash->{INTERVAL} = 900 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 900 (=5min).
      • +

      + + 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)
    • +

    + + + Get +
      +
    • update
      + trigger an update
    • +

    + + + Attributes +
      +
    • interval
      + the interval in seconds for updates. the default ist 3600 (=1h), minimum is 900 (=15min).
    • +
    • disable
      + 1 -> disconnect and stop polling
    • +
    +
+ +=end html +=cut