Compare commits

...

1 Commits

Author SHA1 Message Date
johan12345
67b29917c0 Use official Tesla API for availability data
does not work yet
2023-10-16 19:14:32 +02:00
7 changed files with 80 additions and 63 deletions

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
-----------------