diff --git a/fhem/CHANGED b/fhem/CHANGED index 3e2533786..378f51419 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,7 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 70_BOTVAC: add oauth2 workflow, + support MyKobold app - bugfix: 76_SMAPortal: fix problem that after call consumerMasterdata delivers an error if haven't any consumer available - bugfix: 72_FB_CALLMONITOR: fix reverse search for dasoertliche.de diff --git a/fhem/FHEM/70_BOTVAC.pm b/fhem/FHEM/70_BOTVAC.pm index 9a12ef8eb..671455374 100755 --- a/fhem/FHEM/70_BOTVAC.pm +++ b/fhem/FHEM/70_BOTVAC.pm @@ -95,6 +95,8 @@ my %opcode = ( # Opcode interpretation of the ws "Payload data 'pong' => 0x0A ); +my $clientId = '298e186d502fb6007b969f2f21b590ef32e4de1b2c665851'; + ################################### sub Initialize { my ($hash) = @_; @@ -198,12 +200,19 @@ sub GetStatus { or ReadingsVal( $name, "pollingMode", 1 ) == 0 ); # check device availability - if ( !$update ) { + if ( !$update && $::init_done) { my @time = localtime(); my $secs = ( $time[2] * 3600 ) + ( $time[1] * 60 ) + $time[0]; # update once per day - push( @successor, [ "dashboard", undef ] ) if ( $secs <= $interval ); + if ( $secs <= $interval ) { + if (ReadingsVal($name, '.idToken', '') eq '') { + push( @successor, [ 'dashboard', undef ] ); + } + else { + push( @successor, [ 'dashboard2', undef ] ); + } + } push( @successor, [ "messages", "getSchedule" ] ); push( @successor, [ "messages", "getGeneralInfo" ] ) @@ -251,9 +260,28 @@ sub Get { return "maps for $what are not available yet"; } } + elsif ( $what =~ /^(securityTokens)$/x ) { + my $rowCount = 1; + my $return = ''; + foreach my $token ( '.accessToken', '.secretKey', '.idToken', '.authToken' ) { + my $value = ReadingsVal($name, $token, ''); + if ( $value ne '' ) { + $return .= '', ( $value =~ /.{1,50}/gsxms )); + $return .= ''; + $rowCount++; + } + } + $return .= '
'; + return $return; + } else { return -"Unknown argument $what, choose one of batteryPercent:noArg statistics:noArg"; +"Unknown argument $what, choose one of batteryPercent:noArg statistics:noArg securityTokens:noArg"; } } @@ -276,6 +304,8 @@ sub Set { my $usage = "Unknown argument " . $a[1] . ", choose one of"; $usage .= " password"; + $usage .= " requestVerification:noArg sendVerification" + if ($hash->{VENDOR} eq 'vorwerk'); if ( ReadingsVal( $name, ".start", "0" ) ) { $usage .= " startCleaning:"; if ( $houseCleaningSrv eq "basic-4" ) { @@ -496,7 +526,12 @@ sub Set { elsif ( $a[1] eq "syncRobots" ) { Log3( $name, 2, "BOTVAC set $name $arg" ); - SendCommand( $hash, "dashboard" ); + if (ReadingsVal($name, '.idToken', '') eq '') { + SendCommand( $hash, "dashboard" ); + } + else { + SendCommand( $hash, "dashboard2" ); + } } # statusRequest @@ -608,6 +643,29 @@ sub Set { StorePassword( $hash, $a[2] ); } + # requestVerification + elsif ( $a[1] eq 'requestVerification' ) { + Log3( $name, 2, "BOTVAC set $name " . $a[1] ); + + SendCommand( $hash, 'requestVerification' ); + } + + # sendVerification + elsif ( $a[1] eq 'sendVerification' ) { + Log3( $name, 2, "BOTVAC set $name $arg" ); + + return 'No verification code given' if ( !defined( $a[2] ) ); + + SendCommand( $hash, 'sendVerification', $a[2] ); + } + + # getProducts + elsif ( $a[1] eq 'getProducts' ) { + Log3( $name, 2, "BOTVAC set $name " . $a[1] ); + + SendCommand( $hash, 'getProducts' ); + } + # pollingMode elsif ( $a[1] eq "pollingMode" ) { Log3( $name, 4, "BOTVAC set $name $arg" ); @@ -757,7 +815,7 @@ sub SendCommand { my ( $hash, $service, $cmd, $option, @successor ) = @_; my $name = $hash->{NAME}; my $email = $hash->{EMAIL}; - my $password = ReadPassword($hash); + my $password = ReadingsVal($name, '.idToken', '') eq '' ? ReadPassword($hash) : ''; my $timestamp = gettimeofday(); my $timeout = 180; my $keepalive = 0; @@ -765,13 +823,24 @@ sub SendCommand { my $URL = "https://"; my %sslArgs = (); my %header; + my $method; my $data; my $response; my $return; Log3( $name, 5, "BOTVAC $name: called function SendCommand()" ); - if ( $service ne "sessions" && $service ne "dashboard" ) { + my @registrationServices = qw( + sessions + dashboard.* + firmwares + .*Verification + profileLogin + getProducts + ); + my $re = join( '|', @registrationServices ); + $re = qr/($re)/xms; + if ( $service !~ /$re/xms ) { return if ( CheckRegistration( $hash, $service, $cmd, $option, @successor ) ); @@ -811,7 +880,7 @@ sub SendCommand { return; } my $token = createUniqueId() . createUniqueId(); - $URL .= GetBeehiveHost( $hash->{VENDOR} ); + $URL .= GetBeehiveHost($hash); $URL .= "/sessions"; $data = "{\"platform\": \"ios\", \"email\": \"$email\", \"token\": \"$token\", \"password\": \"$password\"}"; @@ -820,19 +889,35 @@ sub SendCommand { elsif ( $service eq "dashboard" ) { $header{Authorization} = "Token token=" . ReadingsVal( $name, ".accessToken", "" ); - $URL .= GetBeehiveHost( $hash->{VENDOR} ); + $URL .= GetBeehiveHost($hash); $URL .= "/dashboard"; + } + elsif ( $service eq 'dashboard2' ) { + $header{Authorization} = + 'Auth0Bearer ' . ReadingsVal( $name, '.idToken', '' ); + $URL .= GetBeehiveHost($hash); + $URL .= '/users/me/robots'; + + } + elsif ( $service eq 'firmwares' ) { + $header{Authorization} = + 'Auth0Bearer ' . ReadingsVal( $name, '.idToken', '' ); + $URL .= GetBeehiveHost($hash); + $URL .= '/firmwares/recent'; + } elsif ( $service eq "robots" ) { - my $serial = ReadingsVal( $name, "serial", "" ); + my $serial = ReadingsVal( $name, 'serial', '' ); return if ( $serial eq "" ); - $header{Authorization} = - "Token token=" . ReadingsVal( $name, ".accessToken", "" ); - $URL .= GetBeehiveHost( $hash->{VENDOR} ); + my $idToken = ReadingsVal( $name, '.idToken', '' ); + $header{Authorization} = $idToken eq '' ? + 'Token token=' . ReadingsVal( $name, '.accessToken', '' ): + "Auth0Bearer $idToken"; + $URL .= GetBeehiveHost($hash); $URL .= "/users/me/robots/$serial/"; - $URL .= ( defined($cmd) ? $cmd : "maps" ); + $URL .= ( defined($cmd) ? $cmd : 'maps' ); } elsif ( $service eq "messages" ) { @@ -968,10 +1053,50 @@ sub SendCommand { elsif ( $service eq "loadmap" ) { $URL = $cmd; } + elsif ( $service eq 'requestVerification' ) { + $URL = 'https://mykobold.eu.auth0.com/passwordless/start'; + $data = '{'; + $data .= '"client_id": "' . encode_base64(pack('H*', $clientId), '') . '",'; + $data .= '"connection": "email",'; + $data .= "\"email\": \"$email\","; + $data .= '"send": "code"'; + $data .= '}'; + } + elsif ( $service eq 'sendVerification' ) { + $URL = 'https://mykobold.eu.auth0.com/oauth/token'; + $data = '{'; + $data .= '"audience": "https://mykobold.eu.auth0.com/userinfo",'; + $data .= '"client_id": "' . encode_base64(pack('H*', $clientId), '') . '",'; + $data .= '"country_code": "DE",'; + $data .= '"grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp",'; + $data .= '"locale": "de",'; + $data .= "\"otp\": \"$cmd\","; + $data .= '"platform": "ios",'; + $data .= '"prompt": "login",'; + $data .= '"realm": "email",'; + $data .= '"scope": "openid email profile read:current_user",'; + $data .= '"source": "vorwerk_auth0",'; + $data .= "\"username\": \"$email\""; + $data .= '}'; + } + elsif ( $service eq 'profileLogin' ) { + $method = 'POST'; + $URL = 'https://api-2-prod.companion.kobold.vorwerk.com/api/v1/profile/login'; + $header{Authorization} = 'Bearer ' . ReadingsVal($name, '.idToken', ''); + delete( $header{Accept} ); + } + elsif ( $service eq 'getProducts' ) { + $URL = 'https://api-2-prod.companion.kobold.vorwerk.com/api/v1/profile/products'; + $header{Authorization} = ReadingsVal($name, '.authToken', ''); + $header{'Accept-Language'} = 'de_DE'; + delete( $header{Accept} ); + } # send request via HTTP-POST method Log3( $name, 5, "BOTVAC $name: POST $URL (" . ::urlDecode($data) . ")" ) if ( defined($data) ); + Log3( $name, 5, "BOTVAC $name: POST $URL" ) + if ( defined($method) && $method eq 'POST' ); Log3( $name, 5, "BOTVAC $name: GET $URL" ) if ( !defined($data) ); Log3( $name, 5, @@ -985,6 +1110,7 @@ sub SendCommand { noshutdown => 1, keepalive => $keepalive, header => \%header, + method => $method, data => $data, hash => $hash, service => $service, @@ -1093,7 +1219,7 @@ sub ReceiveCommand { readingsDelete( $hash, ".accessToken" ); readingsEndUpdate( $hash, 1 ); - if ( $service ne "sessions" ) { + if ( $service ne "sessions" && ReadingsVal($name, '.idToken', '') eq '') { # put last command back into queue unshift( @successor, [ $service, $cmd ] ); @@ -1547,6 +1673,51 @@ sub ReceiveCommand { } } + #dashboard vorwerk + elsif ( $service eq 'dashboard2' ) { + if ( ref($return) eq 'ARRAY' ) { + my @robotList = (); + my @robots = @{$return}; + for ( my $i = 0 ; $i < @robots ; $i++ ) { + my $r = { + 'name' => $robots[$i]->{name}, + 'model' => $robots[$i]->{model}, + 'serial' => $robots[$i]->{serial}, + 'secretKey' => $robots[$i]->{secret_key}, + 'macAddr' => $robots[$i]->{mac_address}, + 'nucleoUrl' => $robots[$i]->{nucleo_url} + }; + push( @robotList, $r ); + } + $hash->{helper}{ROBOTS} = \@robotList; + if (@robotList) { + # follow registration procedure first + unshift( @successor, [ 'firmwares' ] ); + } + else { + Log3( $name, 3, "BOTVAC $name: no robots found" ); + Log3( $name, 4, "BOTVAC $name: drop successors" ); + LogSuccessors( $hash, @successor ); + @successor = (); + } + } + } + + #firmwares + elsif ( $service eq 'firmwares' ) { + if ( ref($return) eq 'HASH' ) { + my @robotList = @{ $hash->{helper}{ROBOTS} }; + foreach my $r ( @robotList ) { + my $firmware = $return->{ $r->{model} }; + $r->{recentFirmware} = $firmware->{version} if defined($firmware); + } + if (@robotList) { + SetRobot( $hash, ReadingsNum( $name, 'robot', 0 ) ); + push( @successor, [ 'robots', 'maps' ] ); + } + } + } + # robots elsif ( $service eq "robots" ) { if ( $cmd eq "maps" ) { @@ -1656,6 +1827,43 @@ sub ReceiveCommand { readingsBulkUpdate( $hash, ".map_cache", $data ); } + # requestVerification + elsif ( $service eq 'requestVerification' ) { + # nothing to do + } + + # sendVerification + elsif ( $service eq 'sendVerification' ) { + if ( ref($return) eq 'HASH' ) { + readingsBulkUpdateIfChanged( $hash, '.accessToken', + $return->{access_token} ) + if ( defined( $return->{access_token} ) ); + if ( defined( $return->{id_token} ) ) { + readingsBulkUpdateIfChanged( $hash, '.idToken', + $return->{id_token} ); + push( @successor, [ 'profileLogin' ] ); + } + } + } + + # profileLogin + elsif ( $service eq 'profileLogin' ) { + if ( $respHeader =~ /Authorization:\s(Bearer\s[^\s]*)/xms ) { + readingsBulkUpdateIfChanged( $hash, '.authToken', $1 ); + # follow registration procedure first + unshift( @successor, [ 'getProducts' ] ); + } + } + + # getProdcts + elsif ( $service eq 'getProducts' ) { + if ( ref($return) eq 'ARRAY' ) { + # take data from dashboard + # follow registration procedure first + unshift( @successor, [ 'dashboard2' ] ); + } + } + # all other command results else { Log3( $name, 2, @@ -1854,17 +2062,25 @@ sub CheckRegistration { my ( $hash, $service, $cmd, $option, @successor ) = @_; my $name = $hash->{NAME}; - if ( ReadingsVal( $name, ".secretKey", "" ) eq "" ) { + if ( ReadingsVal( $name, '.secretKey', '' ) eq '' ) { my @nextCmd = ( $service, $cmd, $option ); unshift( @successor, [ $service, $cmd, $option ] ); Log3( $name, 4, "BOTVAC $name: register account" ); LogSuccessors( $hash, @successor ); - SendCommand( $hash, "sessions", undef, undef, @successor ) - if ( ReadingsVal( $name, ".accessToken", "" ) eq "" ); - SendCommand( $hash, "dashboard", undef, undef, @successor ) - if ( ReadingsVal( $name, ".accessToken", "" ) ne "" ); + if ( ReadingsVal( $name, '.idToken', '' ) eq '' ) { + SendCommand( $hash, 'sessions', undef, undef, @successor ) + if ( ReadingsVal( $name, '.accessToken', '' ) eq '' ); + SendCommand( $hash, 'dashboard', undef, undef, @successor ) + if ( ReadingsVal( $name, '.accessToken', '' ) ne '' ); + } + else { + SendCommand( $hash, 'profileLogin', undef, undef, @successor ) + if ( ReadingsVal( $name, '.authToken', '' ) eq '' ); + SendCommand( $hash, 'dashboard2', undef, undef, @successor ) + if ( ReadingsVal( $name, '.authToken', '' ) ne '' ); + } return 1; } @@ -2088,10 +2304,15 @@ sub GetAuthStatusText { } sub GetBeehiveHost { - my ($vendor) = @_; + my ($hash) = @_; + my $name = $hash->{NAME}; + my $vendor = $hash->{VENDOR}; my $vendors = { 'neato' => 'beehive.neatocloud.com', - 'vorwerk' => 'vorwerk-beehive-production.herokuapp.com', + 'vorwerk' => + ReadingsVal($name, '.idToken', '') eq '' ? + 'vorwerk-beehive-production.herokuapp.com' : + 'beehive.ksecosys.com', }; if ( defined( $vendors->{$vendor} ) ) { @@ -2657,334 +2878,149 @@ sub wsMasking { =begin html - +

BOTVAC

+

Attributes

+ - =end html