mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 23:57:45 -05:00
Compare commits
1 Commits
master
...
tesla-offi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67b29917c0 |
1
.github/workflows/apikeys-ci.xml
vendored
1
.github/workflows/apikeys-ci.xml
vendored
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-----------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user