Compare commits

...

11 Commits
0.7.1 ... 0.7.2

Author SHA1 Message Date
johan12345
d335d7cab0 Release 0.7.2 2021-05-09 15:25:03 +02:00
johan12345
f7c3faa7bd Chargeprice: show my tariffs first in overview 2021-05-09 15:18:35 +02:00
johan12345
1338e2306e Chargeprice: show currently selected currency as summary 2021-05-09 14:45:31 +02:00
johan12345
83a2b42408 Chargeprice: add "my plans" selection preference 2021-05-09 14:44:18 +02:00
johan12345
0ce5938f5b Chargeprice: show provider name only if tariff name doesn't start with it 2021-05-09 14:38:57 +02:00
johan12345
5ab50e04ae README.md: add info about necessary Chargeprice API key 2021-04-28 22:51:05 +02:00
johan12345
ee0fd4e8d8 Chargeprice: store charging range (#86) 2021-04-28 22:47:42 +02:00
johan12345
369b7d9410 Chargeprice: implement currency selection (#86) 2021-04-28 22:41:08 +02:00
johan12345
c9a0b270cd Chargeprice: do not show two error messages if car was not yet selected 2021-04-28 22:38:56 +02:00
johan12345
c8aa64fa7c fix missing German translation 2021-04-24 19:41:39 +02:00
Johan von Forstner
d5b18bd6fb README: Update Features 2021-04-23 08:12:19 +02:00
20 changed files with 317 additions and 19 deletions

View File

@@ -18,6 +18,8 @@ Features
- Realtime availability information (beta)
- Search places
- Favorites list, also with availability information
- Charging price comparison, powered by [Chargeprice.app](https://chargeprice.app)
- Android Auto integration
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
@@ -33,7 +35,8 @@ Development setup
The App is developed using Android Studio.
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs)
as well as for [Google APIs](https://console.developers.google.com/)
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These APIs need to be put into the
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
@@ -50,5 +53,8 @@ following content:
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
</resources>
```

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 45
versionName "0.7.1"
versionCode 46
versionName "0.7.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -107,6 +107,16 @@ class ChargepriceAdapter() :
field = value
notifyDataSetChanged()
}
var myTariffs: Set<String>? = null
set(value) {
field = value
notifyDataSetChanged()
}
var myTariffsAll: Boolean? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
@@ -127,7 +137,11 @@ class ChargepriceAdapter() :
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
super.bind(holder, item)
(holder.binding as ItemChargepriceBinding).meta = meta
(holder.binding as ItemChargepriceBinding).apply {
this.meta = this@ChargepriceAdapter.meta
this.myTariffs = this@ChargepriceAdapter.myTariffs
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
}
}
}

View File

@@ -26,6 +26,9 @@ interface ChargepriceApi {
@GET("vehicles")
suspend fun getVehicles(): ArrayDocument<ChargepriceCar>
@GET("tariffs")
suspend fun getTariffs(): ArrayDocument<ChargepriceTariff>
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")

View File

@@ -70,13 +70,46 @@ data class ChargepriceOptions(
)
@JsonApi(type = "tariff")
data class ChargepriceTariff(
val provider: String,
val name: String,
@field:Json(name = "direct_payment") val directPayment: Boolean,
@field:Json(name = "provider_customer_tariff") val providerCustomerTariff: Boolean,
@field:Json(name = "charge_card_id") val chargeCardId: String // GE charge card ID
) : Resource()
class ChargepriceTariff() : Resource() {
lateinit var provider: String
lateinit var name: String
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
@field:Json(name = "supported_cuntries")
lateinit var supportedCountries: Set<String>
@field:Json(name = "charge_card_id")
lateinit var chargeCardId: String // GE charge card ID
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceTariff
if (provider != other.provider) return false
if (name != other.name) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (supportedCountries != other.supportedCountries) return false
if (chargeCardId != other.chargeCardId) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + supportedCountries.hashCode()
result = 31 * result + chargeCardId.hashCode()
return result
}
}
@JsonApi(type = "car")
class ChargepriceCar : Resource() {
@@ -146,6 +179,8 @@ class ChargePrice : Resource(), Equatable, Cloneable {
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
var tariff: HasOne<ChargepriceTariff>? = null
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
@@ -212,6 +247,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
tariffName = this@ChargePrice.tariffName
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
}
}
}

View File

@@ -112,6 +112,12 @@ class ChargepriceFragment : DialogFragment() {
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
chargepriceAdapter.meta = it?.data
}
vm.myTariffs.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffs = it
}
vm.myTariffsAll.observe(viewLifecycleOwner) {
chargepriceAdapter.myTariffsAll = it
}
val connectorsAdapter = CheckableConnectorAdapter()
@@ -168,6 +174,7 @@ class ChargepriceFragment : DialogFragment() {
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
when (res?.status) {
Status.ERROR -> {
if (vm.vehicle.value == null) return@observe
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(

View File

@@ -8,6 +8,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
@@ -32,6 +33,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
})
private lateinit var myVehiclePreference: ListPreference
private lateinit var myTariffsPreference: MultiSelectListPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -44,7 +46,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
(requireActivity() as MapsActivity).appBarConfiguration
)
myVehiclePreference = findPreference<ListPreference>("chargeprice_my_vehicle")!!
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
vm.vehicles.observe(viewLifecycleOwner) { res ->
res.data?.let { cars ->
@@ -57,6 +59,32 @@ class SettingsFragment : PreferenceFragmentCompat(),
?.let { "${it.brand} ${it.name}" }
}
}
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
vm.tariffs.observe(viewLifecycleOwner) { res ->
res.data?.let { tariffs ->
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
myTariffsPreference.entries = tariffs.map {
if (!it.name.startsWith(it.provider)) {
"${it.provider} ${it.name}"
} else {
it.name
}
}.toTypedArray()
myTariffsPreference.isEnabled = true
updateMyTariffsSummary()
}
}
}
private fun updateMyTariffsSummary() {
myTariffsPreference.summary = if (prefs.chargepriceMyTariffsAll) {
getString(R.string.chargeprice_all_tariffs_selected)
} else {
val n = prefs.chargepriceMyTariffs?.size ?: 0
requireContext().resources
.getQuantityString(R.plurals.chargeprice_some_tariffs_selected, n, n)
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@@ -89,6 +117,9 @@ class SettingsFragment : PreferenceFragmentCompat(),
}
}
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
}
}

View File

@@ -111,6 +111,18 @@ class PreferenceDataSource(val context: Context) {
.apply()
}
var chargepriceMyTariffs: Set<String>?
get() = sp.getStringSet("chargeprice_my_tariffs", null)
set(value) {
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
}
var chargepriceMyTariffsAll: Boolean
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
set(value) {
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
}
var chargepriceNoBaseFee: Boolean
get() = sp.getBoolean("chargeprice_no_base_fee", false)
set(value) {
@@ -122,4 +134,21 @@ class PreferenceDataSource(val context: Context) {
set(value) {
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
}
var chargepriceCurrency: String
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
set(value) {
sp.edit().putString("chargeprice_currency", value).apply()
}
var chargepriceBatteryRange: List<Float>
get() = listOf(
sp.getFloat("chargeprice_battery_range_min", 20f),
sp.getFloat("chargeprice_battery_range_max", 80f),
)
set(value) {
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
.putFloat("chargeprice_battery_range_max", value[1])
.apply()
}
}

View File

@@ -255,7 +255,7 @@ fun currency(currency: String): String {
"USD" -> "$"
"DKK", "SEK", "NOK" -> "kr."
"PLN" -> ""
"CHF" -> "Fr."
"CHF" -> "Fr. "
"CZK" -> ""
"GBP" -> "£"
"HRK" -> "kn"
@@ -291,4 +291,15 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
@BindingAdapter("app:tint")
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}
@BindingAdapter("myTariffsBackground")
fun myTariffsBackground(view: View, myTariff: Boolean) {
if (myTariff) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
} else {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
}
}
}

View File

@@ -0,0 +1,31 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.MultiSelectListPreference
import net.vonforst.evmap.fragment.MultiSelectDialog
class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
MultiSelectListPreference(ctx, attrs) {
override fun onClick() {
val dialog =
MultiSelectDialog.getInstance(
title.toString(),
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
if (all) entryValues.map { it.toString() }.toSet() else values,
emptySet()
)
dialog.okListener = { selected ->
all = selected == entryValues.toSet()
values = selected
}
dialog.show((context as AppCompatActivity).supportFragmentManager, null)
}
var all: Boolean
get() = sharedPreferences.getBoolean(key + "_all", true)
set(value) {
sharedPreferences.edit().putBoolean(key + "_all", value).apply()
}
}

View File

@@ -67,7 +67,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val batteryRange: MutableLiveData<List<Float>> by lazy {
MutableLiveData<List<Float>>().apply {
value = listOf(20f, 80f)
value = prefs.chargepriceBatteryRange
observeForever {
prefs.chargepriceBatteryRange = it
}
}
}
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
@@ -112,6 +115,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
} else if (cps.status == Status.LOADING) {
value = Resource.loading(null)
} else {
val myTariffs = prefs.chargepriceMyTariffs
value = Resource.success(cps.data!!.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power }
@@ -122,13 +126,30 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
chargepointPrices = filteredPrices
}
}
}.filterNotNull().sortedBy { it.chargepointPrices.first().price })
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
)
}
}
}
}
}
val myTariffs: LiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyTariffs
}
}
val myTariffsAll: LiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = prefs.chargepriceMyTariffsAll
}
}
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
listOf(chargePriceMeta, chargepoint).forEach {
@@ -173,7 +194,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, getChargepriceLanguage())
val meta =

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import java.io.IOException
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
@@ -20,6 +21,13 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
}
}
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
value = Resource.loading(null)
loadTariffs()
}
}
private fun loadVehicles() {
viewModelScope.launch {
try {
@@ -30,4 +38,15 @@ class SettingsViewModel(application: Application, chargepriceApiKey: String) :
}
}
}
private fun loadTariffs() {
viewModelScope.launch {
try {
val result = api.getTariffs()
tariffs.value = Resource.success(result)
} catch (e: IOException) {
tariffs.value = Resource.error(e.message, null)
}
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -11,6 +11,8 @@
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="java.util.Set" />
<variable
name="item"
type="ChargePrice" />
@@ -18,6 +20,14 @@
<variable
name="meta"
type="ChargepriceChargepointMeta" />
<variable
name="myTariffs"
type="Set&lt;String>" />
<variable
name="myTariffsAll"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -27,7 +37,7 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:background="?selectableItemBackground">
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
<TextView
android:id="@+id/txtTariff"
@@ -50,7 +60,7 @@
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.provider.equals(item.tariffName)}"
app:goneUnless="@{!item.tariffName.startsWith(item.provider)}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintStart_toStartOf="@+id/txtTariff"

View File

@@ -186,6 +186,27 @@
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_title">Preisvergleich</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="pref_chargeprice_currency">Währung</string>
<string name="pref_my_tariffs">Meine Tarife</string>
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
<string-array name="pref_chargeprice_currency_names">
<item>Schweizer Franken (CHF)</item>
<item>Tschechische Krone (CZK)</item>
<item>Dänische Krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Britisches Pfund (GBP)</item>
<item>Kroatische Kuna (HRK)</item>
<item>Ungarischer Forint (HUF)</item>
<item>Isländische Krone (ISK)</item>
<item>Norwegische Krone (NOK)</item>
<item>Polnischer Złoty (PLN)</item>
<item>Schwedische Krone (SEK)</item>
<item>US-Dollar (USD)</item>
</string-array>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d Tarif ausgewählt</item>
<item quantity="other">%d Tarife ausgewählt</item>
</plurals>
</resources>

View File

@@ -20,4 +20,32 @@
<item>on</item>
<item>off</item>
</string-array>
<string-array name="pref_chargeprice_currency_names">
<item>Swiss franc (CHF)</item>
<item>Czech koruna (CZK)</item>
<item>Danish krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Pound sterling (GBP)</item>
<item>Croatian kuna (HRK)</item>
<item>Hungarian forint (HUF)</item>
<item>Icelandic króna (ISK)</item>
<item>Norwegian krone (NOK)</item>
<item>Polish złoty (PLN)</item>
<item>Swedish krona (SEK)</item>
<item>US dollar (USD)</item>
</string-array>
<string-array name="pref_chargeprice_currency_values" donottranslate="true">
<item>CHF</item>
<item>CZK</item>
<item>DKK</item>
<item>EUR</item>
<item>GBP</item>
<item>HRK</item>
<item>HUF</item>
<item>ISK</item>
<item>NOK</item>
<item>PLN</item>
<item>SEK</item>
<item>USD</item>
</string-array>
</resources>

View File

@@ -187,4 +187,11 @@
<string name="chargeprice_title">Prices</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
<string name="pref_chargeprice_currency">Currency</string>
<string name="pref_my_tariffs">My charging plans</string>
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan selected</item>
<item quantity="other">%d plans selected</item>
</plurals>
</resources>

View File

@@ -42,6 +42,16 @@
<ListPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"

View File

@@ -0,0 +1,4 @@
Verbesserungen für Chargeprice.app-Integration:
- Währung kann in den Einstellungen gewählt werden
- Ausgewählter Ladebereich wird gespeichert
- Eigene Tarife können in den Einstellungen ausgewählt werden

View File

@@ -0,0 +1,4 @@
Improvements for Chargeprice.app integration:
- Currency selection in settings
- Save selected charging range
- Own charging plans can be selected in settings