Skip to content

Commit

Permalink
Use official Tesla API for availability data
Browse files Browse the repository at this point in the history
does not work yet
  • Loading branch information
johan12345 committed Oct 16, 2023
1 parent bc91c05 commit 67b2991
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 63 deletions.
1 change: 1 addition & 0 deletions .github/workflows/apikeys-ci.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
<string name="openchargemap_key" translatable="false">ci</string>
<string name="fronyx_key" translatable="false">ci</string>
<string name="acra_credentials" translatable="false">ci:ci</string>
<string name="tesla_credentials" translatable="false">ci:ci</string>
</resources>
7 changes: 7 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ android {
if (acraKey != null) {
variant.resValue "string", "acra_credentials", acraKey
}
def teslaKey = env.TESLA_CREDENTIALS ?: project.findProperty("TESLA_CREDENTIALS")
if (teslaKey == null && project.hasProperty("TESLA_CREDENTIALS_ENCRYPTED")) {
teslaKey = decode(project.findProperty("TESLA_CREDENTIALS_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (teslaKey != null) {
variant.resValue "string", "tesla_credentials", teslaKey
}
}

packagingOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.vonforst.evmap.api.availability
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
Expand Down Expand Up @@ -170,8 +171,15 @@ class AvailabilityRepository(context: Context) {
.connectTimeout(10, TimeUnit.SECONDS)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
private val teslaAvailabilityDetector =
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context))
private val teslaAvailabilityDetector = run {
val (clientId, clientSecret) = context.getString(R.string.tesla_credentials).split(":")
TeslaAvailabilityDetector(
okhttp,
EncryptedPreferenceDataStore(context),
clientId,
clientSecret
)
}
private val availabilityDetectors = listOf(
RheinenergieAvailabilityDetector(okhttp),
teslaAvailabilityDetector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@ interface TeslaAuthenticationApi {
@JsonClass(generateAdapter = true)
class AuthCodeRequest(
val code: String,
@Json(name = "code_verifier") val codeVerifier: String,
@Json(name = "redirect_uri") val redirectUri: String = "https://auth.tesla.com/void/callback",
scope: String = "openid email offline_access",
@Json(name = "client_id") clientId: String = "ownerapi"
) : OAuth2Request(scope, clientId)
@Json(name = "redirect_uri") val redirectUri: String = "https://ev-map.app/void/callback",
scope: String = "openid offline_access vehicle_device_data",
@Json(name = "client_id") clientId: String,
@Json(name = "client_secret") clientSecret: String
) : OAuth2Request(scope, clientId, clientSecret)

@JsonClass(generateAdapter = true)
class RefreshTokenRequest(
@Json(name = "refresh_token") val refreshToken: String,
scope: String = "openid email offline_access",
@Json(name = "client_id") clientId: String = "ownerapi"
) : OAuth2Request(scope, clientId)
scope: String = "openid offline_access vehicle_device_data",
@Json(name = "client_id") clientId: String,
@Json(name = "client_secret") clientSecret: String,
) : OAuth2Request(scope, clientId, clientSecret)

sealed class OAuth2Request(
val scope: String,
val clientId: String
val clientId: String,
val clientSecret: String
)

@JsonClass(generateAdapter = true)
Expand Down Expand Up @@ -85,36 +87,15 @@ interface TeslaAuthenticationApi {
return retrofit.create(TeslaAuthenticationApi::class.java)
}

fun generateCodeVerifier(): String {
val code = ByteArray(64)
SecureRandom().nextBytes(code)
return Base64.encodeToString(
code,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}

fun generateCodeChallenge(codeVerifier: String): String {
val bytes = codeVerifier.toByteArray()
val messageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(bytes, 0, bytes.size)
return Base64.encodeToString(
messageDigest.digest(),
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}

fun buildSignInUri(codeChallenge: String): Uri =
fun buildSignInUri(clientId: String): Uri =
Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
.appendQueryParameter("client_id", "ownerapi")
.appendQueryParameter("code_challenge", codeChallenge)
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", "https://ev-map.app/void/callback")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid email offline_access")
.appendQueryParameter("scope", "openid offline_access vehicle_device_data")
.appendQueryParameter("state", "123").build()

val resultUrlPrefix = "https://auth.tesla.com/void/callback"
val resultUrlPrefix = "https://ev-map.app/void/callback"
}
}

Expand Down Expand Up @@ -500,6 +481,8 @@ fun Coordinate.asTeslaCoord() =
class TeslaAvailabilityDetector(
private val client: OkHttpClient,
private val tokenStore: TokenStore,
private val clientId: String,
private val clientSecret: String,
private val baseUrl: String? = null
) :
BaseAvailabilityDetector(client) {
Expand Down Expand Up @@ -644,7 +627,9 @@ class TeslaAvailabilityDetector(
val response =
authApi.getToken(
TeslaAuthenticationApi.RefreshTokenRequest(
refreshToken
refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
)
tokenStore.teslaAccessToken = response.accessToken
Expand Down
28 changes: 13 additions & 15 deletions app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.hardware.Sensor
import android.hardware.SensorManager
import android.net.Uri
import android.os.Bundle
import android.os.IInterface
import android.text.Html
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
Expand All @@ -23,7 +20,6 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
Expand Down Expand Up @@ -240,12 +236,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
}

private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)

val (clientId, _) = carContext.getString(R.string.tesla_credentials).split(":")
val args = OAuthLoginFragmentArgs(
uri.toString(),
TeslaAuthenticationApi.buildSignInUri(clientId = clientId).toString(),
TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
).toBundle()
Expand All @@ -261,7 +254,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
OAuthLoginFragment.EXTRA_URL,
Uri::class.java
)
teslaGetAccessToken(url!!, codeVerifier)
teslaGetAccessToken(url!!)
}
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))

Expand All @@ -276,22 +269,27 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
}
}

private fun teslaGetAccessToken(url: Uri, codeVerifier: String) {
private fun teslaGetAccessToken(url: Uri) {
teslaLoggingIn = true
invalidate()

val code = url.getQueryParameter("code") ?: return
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
val (clientId, clientSecret) = carContext.getString(R.string.tesla_credentials).split(":")
val request = TeslaAuthenticationApi.AuthCodeRequest(
code,
clientId = clientId,
clientSecret = clientSecret
)
lifecycleScope.launch {
try {
val time = Instant.now().epochSecond
val response =
TeslaAuthenticationApi.create(okhttp).getToken(request)
val userResponse =
TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()
// val userResponse =
// TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()

encryptedPrefs.teslaEmail = userResponse.response.email
encryptedPrefs.teslaEmail = "user@example.com"
encryptedPrefs.teslaAccessToken = response.accessToken
encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn
encryptedPrefs.teslaRefreshToken = response.refreshToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,39 +139,42 @@ class DataSettingsFragment : BaseSettingsFragment() {
}

private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge)

val (clientId, _) = getString(R.string.tesla_credentials).split(":")
val uri = TeslaAuthenticationApi.buildSignInUri(clientId = clientId)
val args = OAuthLoginFragmentArgs(
uri.toString(),
TeslaAuthenticationApi.resultUrlPrefix,
"#000000"
).toBundle()

setFragmentResultListener(uri.toString()) { _, result ->
teslaGetAccessToken(result, codeVerifier)
teslaGetAccessToken(result)
}

findNavController().navigate(R.id.oauth_login, args)
}

private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
private fun teslaGetAccessToken(result: Bundle) {
teslaAccountPreference.summary = getString(R.string.logging_in)

val url = Uri.parse(result.getString("url"))
val code = url.getQueryParameter("code") ?: return
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
val (clientId, clientSecret) = getString(R.string.tesla_credentials).split(":")
val request = TeslaAuthenticationApi.AuthCodeRequest(
code,
clientId = clientId,
clientSecret = clientSecret
)
lifecycleScope.launch {
try {
val time = Instant.now().epochSecond
val response =
TeslaAuthenticationApi.create(okhttp).getToken(request)
val userResponse =
TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()
// val userResponse =
// TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()

encryptedPrefs.teslaEmail = userResponse.response.email
encryptedPrefs.teslaEmail = "user@example.com"
encryptedPrefs.teslaAccessToken = response.accessToken
encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn
encryptedPrefs.teslaRefreshToken = response.refreshToken
Expand Down
15 changes: 15 additions & 0 deletions doc/api_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ in German.

</details>

### **Tesla**

[API documentation](https://developer.tesla.com/docs/fleet-api)

<details>
<summary>How to obtain an API key</summary>

1. [Sign up](https://www.tesla.com/teslaaccount) for a Tesla account
2. In the [Tesla Developer Portal](https://developer.tesla.com/), click on "Request app access"
3. Enter the details of your app
4. You will receive a *Client ID* and *Client Secret*. Enter them both into `tesla_credentials`,
separated by a colon (`:`).

</details>

Pricing providers
-----------------

Expand Down

0 comments on commit 67b2991

Please sign in to comment.