Compare commits

...

9 Commits
1.4.2 ... 1.4.3

Author SHA1 Message Date
johan12345
fb5da76834 fix changelogs 2022-11-30 19:59:48 +01:00
johan12345
ad922f0667 Release 1.4.3 2022-11-30 19:46:20 +01:00
johan12345
773b35d9ce Android Auto Place search: fix clickability when distance is not available 2022-11-30 19:26:34 +01:00
johan12345
a3347c9d62 ChargepriceScreen: use sectioned list instead of disabled state to separate own plans from others 2022-11-30 19:18:46 +01:00
johan12345
da671b8dd3 German string: fix informal form 2022-11-30 18:54:59 +01:00
johan12345
6d877e13e4 re-enable refresh button on AAOS
this is a workaround for https://issuetracker.google.com/issues/260112181
2022-11-30 18:45:23 +01:00
johan12345
8ab1d7170c update CustomBottomSheetBehavior
fixes #260
2022-11-26 21:15:44 +01:00
johan12345
1f75d722cd Implement multi-EVSEID request for fronyx API 2022-11-21 08:49:37 +01:00
johan12345
11bd4b2cec fix NPE in ChargepriceFragment 2022-11-20 20:30:23 +01:00
16 changed files with 381 additions and 49 deletions

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 142
versionName "1.4.2"
versionCode 148
versionName "1.4.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
@@ -171,7 +171,7 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:3529a5a9f1'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f4f641aab5'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'

View File

@@ -77,34 +77,44 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
carContext.stringProvider(),
chargepoint.type
)
} ${chargepoint.formatPower()} " + carContext.getString(
R.string.chargeprice_stats,
meta.energy,
time(meta.duration.roundToInt()),
meta.energy / meta.duration * 60
)
} ${chargepoint.formatPower()} ${
carContext.getString(
R.string.chargeprice_stats,
meta.energy,
time(meta.duration.roundToInt()),
meta.energy / meta.duration * 60
)
}"
}
}
val myTariffs = prefs.chargepriceMyTariffs
val myTariffsAll = prefs.chargepriceMyTariffsAll
val list = ItemList.Builder().apply {
setNoItemsMessage(
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
val prices = prices?.take(maxRows)
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
val myPricesList = buildPricesList(myPrices)
val otherPricesList = buildPricesList(otherPrices)
addSectionedList(
SectionedItemList.create(
myPricesList,
(header?.let { it + "\n" } ?: "") +
carContext.getString(R.string.chargeprice_header_my_tariffs)
)
)
addSectionedList(
SectionedItemList.create(
otherPricesList,
carContext.getString(R.string.chargeprice_header_other_tariffs)
)
)
prices?.take(maxRows)?.forEach { price ->
addItem(Row.Builder().apply {
setTitle(formatProvider(price))
addText(formatPrice(price))
if (carContext.carAppApiLevel >= 5) {
setEnabled(myTariffsAll || myTariffs != null && price.tariffId in myTariffs)
}
}.build())
}
}.build()
if (header != null && list.items.isNotEmpty()) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
val list = buildPricesList(prices)
if (header != null) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}
}
setActionStrip(
@@ -155,6 +165,21 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}.build()
}
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
return ItemList.Builder().apply {
setNoItemsMessage(
errorMessage
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
)
prices?.forEach { price ->
addItem(Row.Builder().apply {
setTitle(formatProvider(price))
addText(formatPrice(price))
}.build())
}
}.build()
}
private fun formatProvider(price: ChargePrice): String {
if (!price.tariffName.startsWith(price.provider)) {
return price.provider + " " + price.tariffName

View File

@@ -263,7 +263,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
.build())
.build())
if (carContext.carAppApiLevel >= 5) {
if (carContext.carAppApiLevel >= 5 ||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
) {
setOnContentRefreshListener(this@MapScreen)
}
}.build()

View File

@@ -105,15 +105,15 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
addText(text)
}
setOnClickListener {
lifecycleScope.launch {
val placeDetails = getDetails(place.id)
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
prefs.placeSearchResultAndroidAutoName =
place.primaryText.toString()
screenManager.popTo(MapScreen.MARKER)
}
setOnClickListener {
lifecycleScope.launch {
val placeDetails = getDetails(place.id)
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
prefs.placeSearchResultAndroidAutoName =
place.primaryText.toString()
screenManager.popTo(MapScreen.MARKER)
}
}
}.build())

View File

@@ -14,13 +14,18 @@ import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface FronyxApi {
private interface FronyxApiRetrofit {
@GET("predictions/evse-id/{evseId}")
suspend fun getPredictionsForEvseId(
@Path("evseId") evseId: String,
@Query("timeframe") timeframe: Int? = null
): FronyxEvseIdResponse
@GET("predictions/evses")
suspend fun getPredictionsForEvseIds(
@Query("evseIds", encoded = true) evseIds: String // comma-separated
): List<FronyxEvseIdResponse>
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
@@ -32,7 +37,7 @@ interface FronyxApi {
apikey: String,
baseurl: String = "https://api.fronyx.io/api/",
context: Context? = null
): FronyxApi {
): FronyxApiRetrofit {
val client = OkHttpClient.Builder().apply {
addInterceptor { chain ->
// add API key to every request
@@ -56,9 +61,28 @@ interface FronyxApi {
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(client)
.build()
return retrofit.create(FronyxApi::class.java)
return retrofit.create(FronyxApiRetrofit::class.java)
}
}
}
class FronyxApi(
apikey: String,
baseurl: String = "https://api.fronyx.io/api/",
context: Context? = null
) {
private val api = FronyxApiRetrofit.create(apikey, baseurl, context)
suspend fun getPredictionsForEvseId(
evseId: String,
timeframe: Int? = null
): FronyxEvseIdResponse = api.getPredictionsForEvseId(evseId, timeframe)
suspend fun getPredictionsForEvseIds(
evseIds: List<String>
): List<FronyxEvseIdResponse> = api.getPredictionsForEvseIds(evseIds.joinToString(","))
companion object {
/**
* Checks if a chargepoint is supported by Fronyx.
*

View File

@@ -6,7 +6,8 @@ import java.time.ZonedDateTime
@JsonClass(generateAdapter = true)
data class FronyxEvseIdResponse(
val evseId: String,
val predictions: List<FronyxPrediction>
val predictions: List<FronyxPrediction>,
val locationId: String?
)
@JsonClass(generateAdapter = true)

View File

@@ -167,7 +167,7 @@ class ChargepriceFragment : Fragment() {
chargepriceAdapter.myTariffsAll = it
}
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.submitList(it.data)
it?.data?.let { chargepriceAdapter.submitList(it) }
}
val connectorsAdapter = CheckableConnectorAdapter()

View File

@@ -229,7 +229,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val predictionApi = FronyxApi.create(application.getString(R.string.fronyx_key))
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
availability.switchMap { av ->
@@ -249,14 +249,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
try {
val result = allEvseIds.map {
predictionApi.getPredictionsForEvseId(it)
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
if (result.size == allEvseIds.size) {
emit(Resource.success(result))
} else {
emit(Resource.error("not all EVSEIDs found", null))
}
emit(Resource.success(result))
println(result)
} catch (e: IOException) {
emit(Resource.error(e.message, null))
e.printStackTrace()

View File

@@ -149,7 +149,7 @@
<string name="reorder">Reihenfolge ändern</string>
<string name="delete">Löschen</string>
<string name="save_as_profile">Als Profil speichern</string>
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
<string name="save_profile_enter_name">Gib den Namen des Filterprofils ein:</string>
<string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
<string name="welcome_to_evmap">Willkommen bei EVMap</string>
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
@@ -286,4 +286,6 @@
<string name="data_source_switched_to">Datenquelle zu %s umgeschaltet</string>
<string name="pref_applink_associate">Unterstützte Links öffnen</string>
<string name="pref_applink_associate_summary">von goingelectric.de und openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Meine Tarife</string>
<string name="chargeprice_header_other_tariffs">Andere Tarife</string>
</resources>

View File

@@ -285,4 +285,6 @@
<string name="data_source_switched_to">Data source switched to %s</string>
<string name="pref_applink_associate">Open supported links</string>
<string name="pref_applink_associate_summary">from goingelectric.de and openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">My plans</string>
<string name="chargeprice_header_other_tariffs">Other plans</string>
</resources>

View File

@@ -20,7 +20,7 @@ class FronyxApiTest {
webServer.start()
val apikey = ""
fronyx = FronyxApi.create(
fronyx = FronyxApi(
apikey,
webServer.url("/").toString()
)
@@ -36,6 +36,14 @@ class FronyxApiTest {
val id = segments[2]
return okResponse("/fronyx/${id.replace("*", "_")}.json")
}
"predictions/evses" -> {
val ids = request.requestUrl!!.queryParameter("evseIds")!!.split(",")
return okResponse(
"/fronyx/${
ids.map { it.replace("*", "_") }.joinToString(",")
}.json"
)
}
else -> return notFoundResponse
}
}
@@ -43,7 +51,7 @@ class FronyxApiTest {
}
@Test
fun apiTest() {
fun apiTestSingle() {
val evseId = "DE*ION*E202102"
runBlocking {
@@ -57,4 +65,25 @@ class FronyxApiTest {
assertEquals(FronyxStatus.AVAILABLE, result.predictions[0].status)
}
}
@Test
fun apiTestMultiple() {
val evseIds = listOf("DE*ION*E202101", "DE*ION*E202102")
runBlocking {
val results = fronyx.getPredictionsForEvseIds(evseIds)
results.forEachIndexed { i, result ->
assertEquals(result.evseId, evseIds[i])
assertEquals(25, result.predictions.size)
assertEquals(
ZonedDateTime.of(2022, 11, 16, 18, 0, 0, 0, ZoneOffset.UTC),
result.predictions[0].timestamp
)
assertEquals(
if (i == 0) FronyxStatus.UNAVAILABLE else FronyxStatus.AVAILABLE,
result.predictions[0].status
)
}
}
}
}

View File

@@ -0,0 +1,214 @@
[
{
"evseId": "DE*ION*E202101",
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
"predictions": [
{
"timestamp": "2022-11-16T18:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T18:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T18:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T18:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T22:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T22:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T22:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T22:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T23:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T23:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T23:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T23:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-17T00:00:00.000Z",
"status": "UNAVAILABLE"
}
]
},
{
"evseId": "DE*ION*E202102",
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
"predictions": [
{
"timestamp": "2022-11-16T18:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T18:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T18:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T18:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T19:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T20:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-11-16T21:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T21:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T21:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T22:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T22:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T22:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T22:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T23:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T23:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T23:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-16T23:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-11-17T00:00:00.000Z",
"status": "AVAILABLE"
}
]
}
]

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Laden der Verfügbarkeitsprognosen beschleunigt
- Android Auto: "Meine Tarife" im Preisvergleich hervorheben
Fehler behoben:
- Android Automotive: Aktualisieren-Button fehlte
- Darstellungsfehler nach dem Scrollen der Detailansicht behoben
- Abstürze behoben

View File

@@ -0,0 +1,9 @@
Verbesserungen:
- Laden der Verfügbarkeitsprognosen beschleunigt
- Android Auto: "Meine Tarife" im Preisvergleich hervorheben
Fehler behoben:
- Android Automotive: Aktualisieren-Button fehlte
- Android Auto: Klick auf Suchergebnis funktioniert manchmal nicht
- Darstellungsfehler nach dem Scrollen der Detailansicht behoben
- Abstürze behoben

View File

@@ -0,0 +1,8 @@
Improvements:
- Faster loading of availability prediction
- Android Auto: highlight "my plans" in price comparison
Bugfixes:
- Android Automotive: refresh button was missing
- fixed visual bug after scrolling detail view
- fixed crashes

View File

@@ -0,0 +1,9 @@
Improvements:
- Faster loading of availability prediction
- Android Auto: highlight "my plans" in price comparison
Bugfixes:
- Android Automotive: refresh button was missing
- Android Auto: Clicking search result sometimes not working
- fixed visual bug after scrolling detail view
- fixed crashes