mirror of
https://github.com/ev-map/EVMap.git
synced 2026-02-18 23:26:18 -05:00
Compare commits
12 Commits
1.7.0
...
tesla-offi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67b29917c0 | ||
|
|
bc91c0571b | ||
|
|
a83102a97e | ||
|
|
f52a98540c | ||
|
|
e0d97e7219 | ||
|
|
3bbd20a57e | ||
|
|
3279c5eceb | ||
|
|
03d958ac2c | ||
|
|
b1fd370101 | ||
|
|
bdc96fcd57 | ||
|
|
0d54e17eb4 | ||
|
|
b1d0081fb7 |
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>
|
||||
|
||||
@@ -19,7 +19,7 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 202
|
||||
versionCode 204
|
||||
versionName "1.7.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -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 {
|
||||
|
||||
@@ -209,8 +209,8 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +268,7 @@ interface TeslaGraphQlApi {
|
||||
data class GetChargingSiteInformationResponseData(val charging: GetChargingSiteInformationResponseDataCharging)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation)
|
||||
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
@@ -445,6 +426,9 @@ interface TeslaGraphQlApi {
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_20_MINUTES")
|
||||
APPROXIMATELY_20_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_GREATER_THAN_25_MINUTES")
|
||||
GREATER_THAN_25_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_UNKNOWN")
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -497,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) {
|
||||
@@ -544,7 +530,7 @@ class TeslaAvailabilityDetector(
|
||||
TeslaGraphQlApi.VehicleMakeType.NON_TESLA
|
||||
)
|
||||
)
|
||||
).data.charging.site
|
||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
@@ -641,7 +627,9 @@ class TeslaAvailabilityDetector(
|
||||
val response =
|
||||
authApi.getToken(
|
||||
TeslaAuthenticationApi.RefreshTokenRequest(
|
||||
refreshToken
|
||||
refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
)
|
||||
tokenStore.teslaAccessToken = response.accessToken
|
||||
|
||||
@@ -64,8 +64,8 @@ data class OCMChargepoint(
|
||||
addressInfo.toAddress(refData),
|
||||
connections.map { it.convert(refData) },
|
||||
operatorInfo?.title,
|
||||
"https://openchargemap.org/site/poi/details/$id",
|
||||
"https://openchargemap.org/site/poi/edit/$id",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
convertFaultReport(),
|
||||
recentlyVerified,
|
||||
null,
|
||||
|
||||
@@ -520,7 +520,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class PlaceSearchScreen(
|
||||
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
val placeDetails = getDetails(place.id) ?: return@launch
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
@@ -226,9 +226,9 @@ class PlaceSearchScreen(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDetails(id: String): PlaceWithBounds {
|
||||
suspend fun getDetails(id: String): PlaceWithBounds? {
|
||||
val provider = currentProvider!!
|
||||
val result = resultList!!.find { it.id == id }!!
|
||||
val result = resultList?.find { it.id == id } ?: return null
|
||||
|
||||
val recentPlace = recentResults.find { it.id == id }
|
||||
if (recentPlace != null) return recentPlace.asPlaceWithBounds()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -168,7 +168,7 @@ class ChargepriceFragment : Fragment() {
|
||||
chargepriceAdapter.myTariffsAll = it
|
||||
}
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
|
||||
it?.data?.let { chargepriceAdapter.submitList(it) }
|
||||
chargepriceAdapter.submitList(it?.data ?: emptyList())
|
||||
}
|
||||
|
||||
val connectorsAdapter = CheckableConnectorAdapter()
|
||||
|
||||
@@ -1224,26 +1224,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.setForceShowIcon(true)
|
||||
popup.setOnMenuItemClickListener {
|
||||
val navController = requireView().findNavController()
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
|
||||
lifecycleScope.launch {
|
||||
vm.copyFiltersToCustom()
|
||||
requireView().findNavController().safeNavigate(
|
||||
navController.safeNavigate(
|
||||
MapFragmentDirections.actionMapToFilterFragment()
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_manage_filter_profiles -> {
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
|
||||
requireView().findNavController().safeNavigate(
|
||||
navController.safeNavigate(
|
||||
MapFragmentDirections.actionMapToFilterProfilesFragment()
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
val profileId = profilesMap.inverse[it]
|
||||
if (profileId != null) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -116,6 +116,7 @@ class ChargepriceViewModel(
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
vehicle,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<string name="rename">Renomear</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Você faz grande uso da comparação de preços. Ajude a cobrir os custos de acesso à informação apoiando o EVMap com uma doação.</string>
|
||||
<string name="verified">verificado</string>
|
||||
<string name="chargeprice_select_connector">Escolhe o conector</string>
|
||||
<string name="chargeprice_select_connector">Escolha o conector</string>
|
||||
<string name="verified_desc">O carregador foi marcado como funcional por um membro da comunidade %s</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
@@ -58,7 +58,7 @@
|
||||
<string name="fault_report_date">Com problemas (atualizado: %s)</string>
|
||||
<string name="filter_chargecards">Formas de pagamento</string>
|
||||
<string name="pref_language">Língua da app</string>
|
||||
<string name="all_selected">Todas selecionadas</string>
|
||||
<string name="all_selected">Todos selecionados</string>
|
||||
<string name="edit">editar</string>
|
||||
<string name="pref_darkmode">Modo escuro</string>
|
||||
<string name="connection_error">Não foi possível carregar a lista de carregadores</string>
|
||||
@@ -76,7 +76,7 @@
|
||||
<string name="category_public_authorities">Autoridades públicas</string>
|
||||
<string name="category_private_charger">Carregador privado</string>
|
||||
<string name="category_rest_area">Área de descanso</string>
|
||||
<string name="edit_at_datasource">Editado em %s</string>
|
||||
<string name="edit_at_datasource">Editar em %s</string>
|
||||
<string name="categories">Categorias</string>
|
||||
<string name="category_service_on_motorway">Área de serviço (autoestrada)</string>
|
||||
<string name="category_service_off_motorway">Área de serviço (fora da autoestrada)</string>
|
||||
@@ -96,7 +96,7 @@
|
||||
<string name="save_profile_enter_name">Insira o nome do perfil com este filtro:</string>
|
||||
<string name="save_as_profile">Guardar como perfil</string>
|
||||
<string name="filterprofiles_empty_state">Não existem filtros guardados</string>
|
||||
<string name="welcome_2">Cada cor corresponde a potência máxima do carregador</string>
|
||||
<string name="welcome_2">Cada cor corresponde à potência máxima do carregador</string>
|
||||
<string name="welcome_to_evmap">Bem-vindo(a) ao EVMap</string>
|
||||
<string name="pref_darkmode_always_off">Sempre desligado</string>
|
||||
<string name="welcome_2_title">Escolha a potência</string>
|
||||
@@ -153,14 +153,14 @@
|
||||
<string name="lets_go">Vamos lá</string>
|
||||
<string name="crash_report_text">O EVMap encontrou um problema. Por favor envie um relatório do erro para o criador da app.</string>
|
||||
<string name="crash_report_comment_prompt">Pode adicionar um comentário abaixo:</string>
|
||||
<string name="pref_search_provider">Fornecedor da pesquisa</string>
|
||||
<string name="pref_search_provider">Provedor da pesquisa</string>
|
||||
<string name="powered_by_mapbox">via Mapbox</string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Apoie o desenvolvimento do EVMap com uma única doação</string>
|
||||
<string name="pref_map_rotate_gestures_on">Use dois dedos para girar o mapa</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotação desligada (norte sempre para cima)</string>
|
||||
<string name="refresh_live_data">atualizar estado em tempo real</string>
|
||||
<string name="pref_search_provider_info">As pesquisas são caras, especialmente quando o Google Maps é usado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
|
||||
<string name="pref_search_provider_info">As pesquisas são caras, especialmente se o Google Maps for utilizado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
|
||||
<string name="github_sponsors_desc">Apoie o EVMap através do GitHub</string>
|
||||
<string name="unnamed_filter_profile">Filtro sem nome</string>
|
||||
<string name="deleted_recent_search_results">As pesquisas recentes foram eliminadas</string>
|
||||
@@ -325,7 +325,7 @@
|
||||
<string name="settings_cache_clear">Limpar cache</string>
|
||||
<string name="settings_cache_count_summary">%d carregadores na base de dados, %.1f MB</string>
|
||||
<string name="settings_caching">Caching (base de dados local)</string>
|
||||
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados na base de dados local, com a exceção dos seus favoritos</string>
|
||||
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados localmente, com a exceção dos seus favoritos</string>
|
||||
<string name="auto_no_chargers_found">Não foram encontrados carregadores próximo de si</string>
|
||||
<string name="auto_no_favorites_found">Nenhum favorito encontrado</string>
|
||||
<string name="opened_on_phone">Aberto no telefone</string>
|
||||
@@ -371,5 +371,5 @@
|
||||
<string name="pref_units_imperial">Imperial</string>
|
||||
<string name="referrals_info">Também pode usar um dos seguintes links de afiliado para apoiar o desenvolvedor da app com a sua compra.</string>
|
||||
<string name="generic_connection_error">Não foi possível carregar a informação</string>
|
||||
<string name="powered_by_fronyx">previsão feita pelo fronyx</string>
|
||||
<string name="powered_by_fronyx">previsão feita por fronyx</string>
|
||||
</resources>
|
||||
@@ -15,11 +15,12 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.internal.DoNotInstrument
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@DoNotInstrument
|
||||
@Ignore("Disabled because Robolectric does not yet support API 34")
|
||||
@Config(sdk = [33]) // Robolectric does not yet support SDK 34
|
||||
class CarAppTest {
|
||||
private val testCarContext =
|
||||
TestCarContext.createCarContext(ApplicationProvider.getApplicationContext()).apply {
|
||||
|
||||
@@ -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