diff --git a/.github/workflows/apikeys-ci.xml b/.github/workflows/apikeys-ci.xml
index 841112cbe..64ce8350c 100644
--- a/.github/workflows/apikeys-ci.xml
+++ b/.github/workflows/apikeys-ci.xml
@@ -6,4 +6,5 @@
ci
ci
ci:ci
+ ci:ci
diff --git a/app/build.gradle b/app/build.gradle
index 5e24858e0..fe940a246 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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 {
diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
index f3d95f331..b09daef87 100644
--- a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt
@@ -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
@@ -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,
diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt
index 3bc8c1ddb..cb25a0e0f 100644
--- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt
@@ -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)
@@ -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"
}
}
@@ -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) {
@@ -644,7 +627,9 @@ class TeslaAvailabilityDetector(
val response =
authApi.getToken(
TeslaAuthenticationApi.RefreshTokenRequest(
- refreshToken
+ refreshToken,
+ clientId = clientId,
+ clientSecret = clientSecret
)
)
tokenStore.teslaAccessToken = response.accessToken
diff --git a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
index 55d7c83b9..b27975118 100644
--- a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
+++ b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt
@@ -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
@@ -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
@@ -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()
@@ -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))
@@ -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
diff --git a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
index 311d75b7e..ed7cc4dd2 100644
--- a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
+++ b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt
@@ -139,10 +139,8 @@ 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,
@@ -150,28 +148,33 @@ class DataSettingsFragment : BaseSettingsFragment() {
).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
diff --git a/doc/api_keys.md b/doc/api_keys.md
index 273399d04..9470b7cfe 100644
--- a/doc/api_keys.md
+++ b/doc/api_keys.md
@@ -149,6 +149,21 @@ in German.
+### **Tesla**
+
+[API documentation](https://developer.tesla.com/docs/fleet-api)
+
+
+How to obtain an API key
+
+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 (`:`).
+
+
+
Pricing providers
-----------------