Compare commits

..

24 Commits
1.1.1 ... 1.1.3

Author SHA1 Message Date
johan12345
9df24081d4 Release 1.1.3 2021-11-16 21:24:16 +01:00
johan12345
255001b768 fix Chargeprice when "my plans" have not yet been selected 2021-11-16 21:20:07 +01:00
johan12345
55af84b7de fix detection of GoingElectric opening hours "24:00" and "around the clock" 2021-11-16 21:08:53 +01:00
johan12345
4f6f09dc83 Release 1.1.2 2021-11-14 18:17:39 +01:00
johan12345
7f6d0c1391 update dependencies 2021-11-14 17:59:41 +01:00
johan12345
96b60d0f49 GoingElectric API: do not show "paid parking" / "paid charging"
because that may also mean that no information is available
see #13
2021-11-14 17:33:36 +01:00
johan12345
2824f0b5c3 handle saved state for MapViewModel 2021-11-14 17:19:11 +01:00
johan12345
af0921ed20 implement a93bacd9b3 for Android Auto 2021-11-14 16:58:44 +01:00
johan12345
a5b55479cb Detail view: show opening hours description also if open 24/7 2021-11-14 16:40:06 +01:00
johan12345
a93bacd9b3 Chargeprice: show provider-exclusive plans when included in "my plans"
fixes #147
2021-11-14 16:26:43 +01:00
johan12345
9d7278e0e2 AvailabilityDetector: support missing household plugs in NewMotion data
fixes #146
2021-11-14 15:58:08 +01:00
johan12345
f6d9c615a0 AvailabilityDetectorTest: use proper socket type constants 2021-11-14 15:40:13 +01:00
johan12345
a8ee3f5b7d Change semantics of opening hours in model
to fix incompatibility with Room that caused a NullPointerException
2021-11-14 15:27:49 +01:00
johan12345
826b4f89f1 fix crash in light mode
introduced in 5d7d881729
2021-11-06 22:51:16 +01:00
johan12345
5675d065e3 update Car App Library to 1.1.0-rc01 2021-11-06 22:34:42 +01:00
johan12345
3e3531551d add link to Chargeprice FAQ page 2021-11-06 22:30:31 +01:00
johan12345
5d7d881729 Chargeprice: add branding 2021-11-06 21:29:17 +01:00
johan12345
23c73e3d7e Android Auto: add error message when Chargeprice data fails to load 2021-11-06 20:02:02 +01:00
johan12345
7835aa8d78 initialize Google map with correct locale 2021-11-04 21:47:33 +01:00
johan12345
f06b712090 disable NullSafeMutableLiveData lint error
seems to give false positives
2021-11-01 19:22:34 +01:00
johan12345
317695954d remove unneeded maven repositories 2021-11-01 15:27:56 +01:00
johan12345
24cfd1c10b upgrade dependencies 2021-11-01 15:16:21 +01:00
johan12345
775faa2f55 update AboutLibraries 2021-11-01 15:00:45 +01:00
johan12345
08bd2bdf5a update build tools 2021-11-01 15:00:30 +01:00
30 changed files with 876 additions and 186 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 31
versionCode 65
versionName "1.1.1"
versionCode 67
versionName "1.1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -103,21 +103,25 @@ android {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
}
lintOptions {
disable 'NullSafeMutableLiveData'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.browser:browser:1.4.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
@@ -139,8 +143,8 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
googleImplementation 'androidx.car.app:app:1.1.0-beta01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
googleImplementation 'androidx.car.app:app:1.1.0-rc01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-rc01'
// AnyMaps
def anyMapsVersion = '751daec281'
@@ -161,7 +165,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.3.1"
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
@@ -185,7 +189,7 @@ dependencies {
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'

View File

@@ -7,10 +7,16 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.maps.MapsInitializer
import com.google.android.libraries.places.api.Places
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
fun init(context: Context) {
Places.initialize(context, context.getString(R.string.google_maps_key))
MapsInitializer.initialize(context.applicationContext, MapsInitializer.Renderer.LATEST, null)
val localeContext = LocaleContextWrapper.wrap(
context.applicationContext, PreferenceDataSource(context).language
)
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
}
fun checkPlayServices(activity: Activity): Boolean {

View File

@@ -14,14 +14,20 @@ import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.*
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
@@ -164,94 +170,120 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val manufacturer = model?.manufacturer?.value
val modelName = model?.name?.value
lifecycleScope.launch {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
try {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
invalidate()
return@launch
}
} else {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
invalidate()
return@launch
}
} else {
}
val car = vehicles[0]
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
tariffs = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
}
val car = vehicles[0]
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage = carContext.getString(R.string.chargeprice_no_compatible_connectors)
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
return@launch
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.chargeprice_connection_error,
CarToast.LENGTH_LONG
)
.show()
}
}
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
}
}

View File

@@ -84,10 +84,10 @@ fun buildDetails(
loc.openinghours.getStatusText(ctx)
else
loc.openinghours.description ?: "",
if (loc.openinghours.days != null) loc.openinghours.description else null,
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven) loc.openinghours.description else null,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailsAdapter.Detail(
if (loc.cost != null && !loc.cost.isEmpty) DetailsAdapter.Detail(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),

View File

@@ -65,10 +65,20 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
var chargepoints = chargepoints
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
val geTypes = chargepoints.map { it.type }.distinct().toSet()
var geTypes = chargepoints.map { it.type }.distinct().toSet()
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
Chargepoint.SCHUKO
)) {
// If charger has household plugs and other plugs, try removing the household plugs
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
}
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
// find connectors of this type
@@ -92,7 +102,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
chargepoint to ids
}
} else if (powers.size == 1 && gePowers.size == 2
&& chargepoints.sumBy { it.count } == connsOfType.size
&& chargepoints.sumOf { it.count } == connsOfType.size
) {
// special case: dual charger(s) with load balancing
// GoingElectric shows 2 different powers, NewMotion just one

View File

@@ -205,6 +205,9 @@ class ChargePrice : Resource(), Equatable, Cloneable {
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
@field:Json(name = "branding")
var branding: ChargepriceBranding? = null
var tariff: HasOne<ChargepriceTariff>? = null
@@ -238,6 +241,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
if (startTime != other.startTime) return false
if (tags != other.tags) return false
if (chargepointPrices != other.chargepointPrices) return false
if (branding != other.branding) return false
return true
}
@@ -256,6 +260,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
result = 31 * result + startTime
result = 31 * result + tags.hashCode()
result = 31 * result + chargepointPrices.hashCode()
result = 31 * result + branding.hashCode()
return result
}
@@ -274,6 +279,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
branding = this@ChargePrice.branding
}
}
}
@@ -328,6 +334,12 @@ data class ChargepointPrice(
}
}
data class ChargepriceBranding(
@Json(name = "background_color") val backgroundColor: String,
@Json(name = "text_color") val textColor: String,
@Json(name = "logo_url") val logoUrl: String
)
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
val isOnlyKwh =
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
@@ -339,6 +351,19 @@ data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
enum class ChargepriceInclude {
@Json(name = "filter")
FILTER,
@Json(name = "always")
ALWAYS,
@Json(name = "exclusive")
EXCLUSIVE
}
data class ChargepriceRequestTariffMeta(
val include: ChargepriceInclude
)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,

View File

@@ -126,16 +126,21 @@ internal class HoursAdapter {
private val regex = Regex("from (.*) till (.*)")
@FromJson
fun fromJson(str: String): GEHours? {
fun fromJson(str: String): GEHours {
if (str == "closed") {
return GEHours(null, null)
} else if (str == "around the clock") {
return GEHours(LocalTime.MIN, LocalTime.MAX)
} else {
val match = regex.find(str)
if (match != null) {
return GEHours(
LocalTime.parse(match.groupValues[1]),
val start = LocalTime.parse(match.groupValues[1])
val end = if (match.groupValues[2] == "24:00") {
LocalTime.MAX
} else {
LocalTime.parse(match.groupValues[2])
)
}
return GEHours(start, end)
} else {
// I cannot reproduce this case, but it seems to occur once in a while
Log.e("GoingElectricApi", "invalid hours value: " + str)

View File

@@ -87,7 +87,14 @@ data class GECost(
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
) {
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
fun convert() = Cost(
// In GE, freecharging = false can either mean "paid charging" or "no information
// available", only freecharging = true provides useful information. Therefore convert
// false to null. Same for freeparking.
if (freecharging) freecharging else null,
if (freeparking) freeparking else null,
descriptionShort, descriptionLong
)
}
@JsonClass(generateAdapter = true)
@@ -126,7 +133,7 @@ data class GEHours(
val start: LocalTime?,
val end: LocalTime?
) {
fun convert() = Hours(start, end)
fun convert() = if (start != null && end != null) Hours(start, end) else null
}
@JsonClass(generateAdapter = true)

View File

@@ -1,9 +1,11 @@
package net.vonforst.evmap.autocomplete
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.parcelize.Parcelize
interface AutocompleteProvider {
val id: String
@@ -183,4 +185,5 @@ enum class AutocompletePlaceType {
}
}
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
@Parcelize
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?) : Parcelable

View File

@@ -175,6 +175,10 @@ class ChargepriceFragment : DialogFragment() {
dismiss()
true
}
R.id.menu_help -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
else -> false
}
}

View File

@@ -910,6 +910,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
if (vm.searchResult.value != null) {
// show search result (after configuration change)
vm.searchResult.postValue(vm.searchResult.value)
}
}
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])

View File

@@ -126,6 +126,9 @@ data class Cost(
val descriptionShort: String? = null,
val descriptionLong: String? = null
) : Parcelable {
val isEmpty: Boolean
get() = descriptionLong == null && descriptionShort == null && freecharging == null && freeparking == null
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
if (freecharging != null && freeparking != null) {
val charging =
@@ -137,6 +140,22 @@ data class Cost(
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
}
} else if (freecharging != null) {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$charging"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_charging, charging), 0)
}
} else if (freeparking != null) {
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$parking"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_parking, parking), 0)
}
} else if (descriptionShort != null) {
return descriptionShort
} else if (descriptionLong != null) {
@@ -162,9 +181,7 @@ data class OpeningHours(
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
} else if (days != null) {
val hours = days.getHoursForDate(LocalDate.now())
if (hours.start == null || hours.end == null) {
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
}
?: return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
val now = LocalTime.now()
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
@@ -192,21 +209,21 @@ data class OpeningHours(
@Parcelize
data class OpeningHoursDays(
@Embedded(prefix = "mo") val monday: Hours,
@Embedded(prefix = "tu") val tuesday: Hours,
@Embedded(prefix = "we") val wednesday: Hours,
@Embedded(prefix = "th") val thursday: Hours,
@Embedded(prefix = "fr") val friday: Hours,
@Embedded(prefix = "sa") val saturday: Hours,
@Embedded(prefix = "su") val sunday: Hours,
@Embedded(prefix = "ho") val holiday: Hours
@Embedded(prefix = "mo") val monday: Hours?,
@Embedded(prefix = "tu") val tuesday: Hours?,
@Embedded(prefix = "we") val wednesday: Hours?,
@Embedded(prefix = "th") val thursday: Hours?,
@Embedded(prefix = "fr") val friday: Hours?,
@Embedded(prefix = "sa") val saturday: Hours?,
@Embedded(prefix = "su") val sunday: Hours?,
@Embedded(prefix = "ho") val holiday: Hours?
) : Parcelable {
fun getHoursForDate(date: LocalDate): Hours {
fun getHoursForDate(date: LocalDate): Hours? {
// TODO: check for holidays
return getHoursForDayOfWeek(date.dayOfWeek)
}
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours? {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
return when (dayOfWeek) {
DayOfWeek.MONDAY -> monday
@@ -223,16 +240,12 @@ data class OpeningHoursDays(
@Parcelize
data class Hours(
val start: LocalTime?,
val end: LocalTime?
val start: LocalTime,
val end: LocalTime
) : Parcelable {
override fun toString(): String {
if (start != null && end != null) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
} else {
return "closed"
}
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
}
}

View File

@@ -2,6 +2,10 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@@ -18,14 +22,12 @@ import androidx.databinding.InverseBindingListener
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import coil.load
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.R
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.meterPerFt
import net.vonforst.evmap.shouldUseImperialUnits
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -344,18 +346,68 @@ 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)
fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?): Drawable? {
when {
myTariff -> {
return ContextCompat.getDrawable(context, R.drawable.my_tariff_background)
}
brandingColor != null -> {
val drawable = ContextCompat.getDrawable(context, R.drawable.branded_tariff_background)
val color = colorToTransparent(Color.parseColor(brandingColor))
(drawable as LayerDrawable).setDrawableByLayerId(
R.id.background, ColorDrawable(
color
)
)
return drawable
}
else -> {
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
return it.getDrawable(0)
}
}
}
}
fun isDarkMode(context: Context) = context.isDarkMode()
/**
* Converts an opaque color to a transparent color, assuming it was on a white background
* with a certain opacity targetAlpha.
*/
private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int {
if (Color.alpha(color) != 255) return color
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
}
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
if (url != null) {
view.load(url)
} else {
view.setImageDrawable(null)
}
}
@BindingAdapter("tooltipTextCompat")
fun setTooltipTextCompat(view: View, text: String) {
TooltipCompat.setTooltipText(view, text)
}
@BindingAdapter("tintNullable")
fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
if (tint != null) {
view.imageTintList = ColorStateList.valueOf(tint)
} else {
view.imageTintList = null
}
}

View File

@@ -4,7 +4,10 @@ import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
@@ -100,7 +103,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataSource,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors
vehicleCompatibleConnectors,
myTariffs, myTariffsAll
).forEach {
addSource(it) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
@@ -208,7 +212,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
val dataSource = dataSource.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null) {
val myTariffs = myTariffs.value
val myTariffsAll = myTariffsAll.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
chargePrices.value = Resource.error(null, null)
return
}
@@ -222,6 +228,19 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataAdapter = dataSource
station = cpStation
vehicle = HasOne(car)
tariffs = if (!myTariffsAll) {
HasMany<ChargepriceTariff>(*myTariffs!!.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,

View File

@@ -1,12 +1,14 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import android.os.Parcelable
import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
@@ -27,7 +29,8 @@ import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
@@ -39,7 +42,8 @@ internal fun getClusterDistance(zoom: Float): Int? {
}
}
class MapViewModel(application: Application) : AndroidViewModel(application) {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.javaClass
val apiName: String
@@ -50,11 +54,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
val bottomSheetState: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
state.getLiveData("bottomSheetState")
}
val mapPosition: MutableLiveData<MapPosition> by lazy {
MutableLiveData<MapPosition>()
state.getLiveData("mapPosition")
}
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
@@ -125,7 +129,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
state.getLiveData("chargerSparse")
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
@@ -223,7 +227,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
MutableLiveData<PlaceWithBounds>()
state.getLiveData("searchResult")
}
val mapType: MutableLiveData<AnyMap.Type> by lazy {

View File

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

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" />
</vector>

View File

@@ -37,17 +37,16 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
<TextView
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.tariffName}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
@@ -57,12 +56,11 @@
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Cheap Charging Co." />
@@ -71,11 +69,10 @@
android:id="@+id/rvTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:nestedScrollingEnabled="false"
app:data="@{item.tags}"
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
tools:itemCount="1"
@@ -85,12 +82,12 @@
android:id="@+id/txtProviderCustomerTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/chargeprice_provider_customer_tariff"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.providerCustomerTariff}"
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
@@ -98,12 +95,11 @@
android:id="@+id/txtMonthlyFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.formatMonthlyFees(context)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
tools:text="Base fee 1 €/month" />
@@ -160,5 +156,19 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.65" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:scaleType="fitCenter"
app:goneUnless="@{item.branding.logoUrl != null}"
app:imageUrl="@{item.branding.logoUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -37,7 +37,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals(&quot;closed&quot;) ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek) == null ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -2,6 +2,12 @@
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
<item
android:id="@+id/menu_close"
android:icon="@drawable/ic_close"

View File

@@ -17,6 +17,8 @@
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
<string name="cost">Kosten</string>
<string name="cost_detail"><![CDATA[<b>Laden:</b> %s · <b>Parken:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
<string name="free">Kostenlos</string>
<string name="paid">Kostenpflichtig</string>
<string name="amenities">Ladeweile</string>
@@ -192,7 +194,7 @@
<string name="chargeprice_vehicle">Fahrzeug</string>
<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_title">Preise</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>
@@ -241,9 +243,11 @@
<string name="unnamed_filter_profile">Unbenanntes Filterprofil</string>
<string name="privacy_link">https://evmap.vonforst.net/de/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/de/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/de/chargeprice_faq.html</string>
<string name="required">erforderlich</string>
<string name="edit_filter_profile">„%s“ bearbeiten</string>
<string name="pref_search_delete_recent">Suchverlauf löschen</string>
<string name="deleted_recent_search_results">Suchverlauf wurde gelöscht</string>
<string name="settings_data_sources">Datenquellen</string>
<string name="help">Hilfe</string>
</resources>

View File

@@ -16,6 +16,8 @@
<string name="holiday">Holiday</string>
<string name="cost">Cost</string>
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s charging</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parking</b>]]></string>
<string name="free">Free</string>
<string name="paid">Paid</string>
<string name="amenities">Amenities</string>
@@ -226,9 +228,11 @@
<string name="unnamed_filter_profile">Unnamed filter profile</string>
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="required">required</string>
<string name="edit_filter_profile">Edit “%s”</string>
<string name="pref_search_delete_recent">Delete recent search results</string>
<string name="deleted_recent_search_results">Recent search results have been deleted</string>
<string name="settings_data_sources">Data sources</string>
<string name="help">Help</string>
</resources>

View File

@@ -8,13 +8,16 @@ class AvailabilityDetectorTest {
@Test
fun testMatchChargepointsSingleCorrect() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// correct data in NewMotion
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (22.0 to "Typ2"), 1L to (22.0 to "Typ2")),
mapOf(
0L to (22.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (22.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -23,13 +26,16 @@ class AvailabilityDetectorTest {
@Test
fun testMatchChargepointsSingleWrongPower() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// wrong power in NewMotion
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -38,11 +44,15 @@ class AvailabilityDetectorTest {
@Test(expected = AvailabilityDetectorException::class)
fun testMatchChargepointsSingleWrong() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// non-matching data in NewMotion
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2"), 2L to (50.0 to "CCS")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
2L to (50.0 to Chargepoint.CCS_UNKNOWN)
),
chargepoints
)
}
@@ -51,11 +61,11 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsComplex() {
// charger with many different connectors
val chargepoints = listOf(
Chargepoint("Typ2", 43.0, 1),
Chargepoint("CCS", 50.0, 1),
Chargepoint("CHAdeMO", 50.0, 2),
Chargepoint("CCS", 160.0, 1),
Chargepoint("CCS", 320.0, 2)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 43.0, 1),
Chargepoint(Chargepoint.CCS_UNKNOWN, 50.0, 1),
Chargepoint(Chargepoint.CHADEMO, 50.0, 2),
Chargepoint(Chargepoint.CCS_UNKNOWN, 160.0, 1),
Chargepoint(Chargepoint.CCS_UNKNOWN, 320.0, 2)
)
// partly wrong power in NewMotion
@@ -70,15 +80,15 @@ class AvailabilityDetectorTest {
BaseAvailabilityDetector.matchChargepoints(
mapOf(
// CHAdeMO + CCS HPC
0L to (50.0 to "CHAdeMO"),
1L to (200.0 to "CCS"),
0L to (50.0 to Chargepoint.CHADEMO),
1L to (200.0 to Chargepoint.CCS_UNKNOWN),
// dual CCS HPC
2L to (80.0 to "CCS"),
3L to (200.0 to "CCS"),
2L to (80.0 to Chargepoint.CCS_UNKNOWN),
3L to (200.0 to Chargepoint.CCS_UNKNOWN),
// 50kW triple charger
4L to (50.0 to "CCS"),
5L to (50.0 to "CHAdeMO"),
6L to (43.0 to "Typ2")
4L to (50.0 to Chargepoint.CCS_UNKNOWN),
5L to (50.0 to Chargepoint.CHADEMO),
6L to (43.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
@@ -89,15 +99,18 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsDifferentPower() {
// single charger with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
val chargepoints = listOf(
Chargepoint("Typ2", 22.0, 1),
Chargepoint("Typ2", 11.0, 1)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1)
)
// both have 27 kW power in NewMotion
assertEquals(
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -107,8 +120,8 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsDifferentPower2() {
// two chargers with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
val chargepoints = listOf(
Chargepoint("Typ2", 22.0, 2),
Chargepoint("Typ2", 11.0, 2)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 2)
)
// both have 27 kW power in NewMotion
@@ -116,10 +129,54 @@ class AvailabilityDetectorTest {
mapOf(chargepoints[1] to setOf(0L, 1L), chargepoints[0] to setOf(2L, 3L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to "Typ2"),
1L to (27.0 to "Typ2"),
2L to (27.0 to "Typ2"),
3L to (27.0 to "Typ2")
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
2L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
3L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
}
@Test
fun testMatchChargepointsMissingSchuko() {
// single charger with 2 22kw chargepoints and two Schuko sockets
val chargepoints = listOf(
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
)
// NewMotion only includes the Type 2 sockets
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
}
@Test
fun testMatchChargepointsMissingSchukoDifferentPower() {
// single charger with 2 22kw chargepoints with load balancing and two Schuko sockets
val chargepoints = listOf(
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1),
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
)
// NewMotion only includes the Type 2 sockets
assertEquals(
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)

View File

@@ -10,6 +10,7 @@ import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalTime
class GoingElectricApiTest {
val api: GoingElectricApi
@@ -63,6 +64,21 @@ class GoingElectricApiTest {
assertEquals(2105, charger.id)
}
@Test
fun testLoadChargepointDetail2() {
val response = runBlocking { api.getChargepointDetail(34210) }
assertTrue(response.isSuccessful)
val body = response.body()!!
assertEquals("ok", body.status)
assertEquals(null, body.startkey)
assertEquals(1, body.chargelocations.size)
val charger = body.chargelocations[0] as GEChargeLocation
assertEquals(34210, charger.id)
assertEquals(LocalTime.MIN, charger.openinghours!!.days!!.monday.start)
assertEquals(LocalTime.MAX, charger.openinghours!!.days!!.monday.end)
assertEquals(LocalTime.MAX, charger.openinghours!!.days!!.tuesday.end)
}
@Test
fun testLoadChargepointList() {
val response = runBlocking {

View File

@@ -0,0 +1,362 @@
{
"status": "ok",
"chargelocations": [
{
"ge_id": 34210,
"name": "Langendreer Marktplatz",
"address": {
"city": "Bochum",
"country": "Deutschland",
"postcode": "44892",
"street": "Oberstraße 2"
},
"coordinates": {
"lat": 51.473454,
"lng": 7.325153
},
"chargepoints": [
{
"type": "typ2_socket",
"voltage": "400",
"amperage": "32",
"current": "acthree",
"power": 22,
"count": 2
}
],
"network": "be.energised",
"operator": "Stadtwerke Bochum GmbH",
"cost": {
"freecharging": false,
"freeparking": true,
"description_short": false,
"description_long": false
},
"fault_report": false,
"verified": true,
"barrierfree": true,
"openinghours": {
"24/7": false,
"description": "Markttage: Dienstag und Freitag",
"days": {
"monday": "around the clock",
"tuesday": "from 15:00 till 24:00",
"wednesday": "around the clock",
"thursday": "around the clock",
"friday": "from 15:00 till 24:00",
"saturday": "around the clock",
"sunday": "around the clock",
"holiday": "closed"
}
},
"url": "//www.goingelectric.de/stromtankstellen/Deutschland/Bochum/Langendreer-Marktplatz-Oberstrasse-2/34210/",
"ladeweile": false,
"location_description": false,
"general_information": false,
"photos": [
{
"id": 77283
},
{
"id": 77284
},
{
"id": 77285
},
{
"id": 77286
}
],
"chargecards": [
{
"id": 274
},
{
"id": 11
},
{
"id": 7
},
{
"id": 349
},
{
"id": 9
},
{
"id": 75
},
{
"id": 12
},
{
"id": 319
},
{
"id": 36
},
{
"id": 368
},
{
"id": 207
},
{
"id": 124
},
{
"id": 43
},
{
"id": 251
},
{
"id": 233
},
{
"id": 200
},
{
"id": 13
},
{
"id": 44
},
{
"id": 201
},
{
"id": 242
},
{
"id": 332
},
{
"id": 338
},
{
"id": 216
},
{
"id": 312
},
{
"id": 248
},
{
"id": 139
},
{
"id": 402
},
{
"id": 249
},
{
"id": 120
},
{
"id": 223
},
{
"id": 188
},
{
"id": 326
},
{
"id": 151
},
{
"id": 288
},
{
"id": 355
},
{
"id": 336
},
{
"id": 195
},
{
"id": 297
},
{
"id": 358
},
{
"id": 333
},
{
"id": 356
},
{
"id": 257
},
{
"id": 351
},
{
"id": 343
},
{
"id": 103
},
{
"id": 346
},
{
"id": 386
},
{
"id": 256
},
{
"id": 408
},
{
"id": 392
},
{
"id": 330
},
{
"id": 347
},
{
"id": 503
},
{
"id": 511
},
{
"id": 518
},
{
"id": 512
},
{
"id": 513
},
{
"id": 514
},
{
"id": 515
},
{
"id": 516
},
{
"id": 517
},
{
"id": 535
},
{
"id": 519
},
{
"id": 520
},
{
"id": 524
},
{
"id": 525
},
{
"id": 528
},
{
"id": 533
},
{
"id": 498
},
{
"id": 541
},
{
"id": 543
},
{
"id": 544
},
{
"id": 499
},
{
"id": 482
},
{
"id": 497
},
{
"id": 433
},
{
"id": 299
},
{
"id": 366
},
{
"id": 414
},
{
"id": 419
},
{
"id": 428
},
{
"id": 429
},
{
"id": 430
},
{
"id": 432
},
{
"id": 456
},
{
"id": 492
},
{
"id": 464
},
{
"id": 469
},
{
"id": 474
},
{
"id": 479
},
{
"id": 342
},
{
"id": 486
},
{
"id": 487
},
{
"id": 488
},
{
"id": 489
},
{
"id": 545
}
]
}
]
}

View File

@@ -2,15 +2,15 @@
buildscript {
ext.kotlin_version = '1.5.31'
ext.about_libs_version = '8.8.5'
ext.nav_version = '2.4.0-alpha10'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.4.0-beta02'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
@@ -27,10 +27,6 @@ allprojects {
//noinspection JcenterRepositoryObsolete
jcenter() // still required for https://github.com/kamikat/moshi-jsonapi
maven { url 'https://jitpack.io' }
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
flatDir {
dirs 'libs'
}
}
}

View File

@@ -0,0 +1,11 @@
Verbesserungen:
- Echtzeitdaten für weitere AC-Ladestationen
- FAQ-Seite zum Preisvergleich
- GoingElectric: irreführendes "Laden/Parken: kostenpflichtig" entfernt
Fehlerbehebungen:
- GoingElectric: Beschreibung zu Öffnungszeiten wurde nicht immer angezeigt
- Leere Detailansicht, nachdem die App längere Zeit im Hintergrund war
- Preisvergleich: Exklusive Energiekunden-Tarife unter "Meine Tarife" wurden nicht angezeigt
- Eingestellte Sprache wurde nicht für Google Maps genutzt
- Abstürze behoben

View File

@@ -0,0 +1,3 @@
Fehlerbehebungen:
- Preisvergleich funktionierte nicht, wenn "meine Tarife" noch nicht ausgewählt waren
- Abstürze behoben

View File

@@ -0,0 +1,11 @@
Improvements:
- Realtime data for additional AC chargers
- FAQ page about price comparison
- GoingElectric: removed misleading "charging/parking: paid"
Bug fixes:
- GoingElectric: Description text for opening hours was not always shown
- Empty detail view after app was in background for a longer time
- Price comparison: provider exclusive plans were not shown even if selected under "my plans"
- Selected language was not used for Google Maps
- Fixed various crashes

View File

@@ -0,0 +1,3 @@
Bug fixes:
- Price comparison was not working when "my charging plans" had not yet been selected
- Fixed crashes