From 51d085dbb031469e0329e551065b42a2d4c8a1e7 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Wed, 31 Mar 2021 23:07:29 +0200 Subject: [PATCH 1/3] Implement Chargeprice API --- app/build.gradle | 2 + .../evmap/api/chargeprice/ChargepriceApi.kt | 76 + .../evmap/api/chargeprice/ChargepriceModel.kt | 280 ++++ .../api/chargeprice/ChargepriceApiTest.kt | 76 + app/src/test/resources/chargeprice/2105.json | 1287 +++++++++++++++++ 5 files changed, 1721 insertions(+) create mode 100644 app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt create mode 100644 app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt create mode 100644 app/src/test/java/net/vonforst/evmap/api/chargeprice/ChargepriceApiTest.kt create mode 100644 app/src/test/resources/chargeprice/2105.json diff --git a/app/build.gradle b/app/build.gradle index 045b3054..ad0b3bb5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -108,6 +108,8 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0' implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' + implementation 'moe.banana:moshi-jsonapi:3.5.0' + implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0' implementation 'io.coil-kt:coil:1.1.0' implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3' implementation "com.mikepenz:aboutlibraries-core:$about_libs_version" diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt new file mode 100644 index 00000000..e1e1d1f5 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt @@ -0,0 +1,76 @@ +package net.vonforst.evmap.api.chargeprice + +import android.content.Context +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import moe.banana.jsonapi2.ArrayDocument +import moe.banana.jsonapi2.JsonApiConverterFactory +import moe.banana.jsonapi2.ResourceAdapterFactory +import net.vonforst.evmap.BuildConfig +import okhttp3.Cache +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface ChargepriceApi { + @POST("charge_prices") + suspend fun getChargePrices( + @Body request: ChargepriceRequest, + @Header("Accept-Language") language: String + ): ArrayDocument + + @GET("vehicles") + suspend fun getVehicles(): ArrayDocument + + companion object { + private val cacheSize = 1L * 1024 * 1024 // 1MB + val supportedLanguages = setOf("de", "en", "fr", "nl") + + private val jsonApiAdapterFactory = ResourceAdapterFactory.builder() + .add(ChargepriceRequest::class.java) + .add(ChargepriceTariff::class.java) + .add(ChargepriceBrand::class.java) + .add(ChargePrice::class.java) + .add(ChargepriceCar::class.java) + .build() + val moshi = Moshi.Builder() + .add(jsonApiAdapterFactory) + .add(KotlinJsonAdapterFactory()) + .build() + + fun create( + apikey: String, + baseurl: String = "https://api.chargeprice.app/v1/", + context: Context? = null + ): ChargepriceApi { + val client = OkHttpClient.Builder().apply { + addInterceptor { chain -> + // add API key to every request + val original = chain.request() + val new = original.newBuilder() + .header("API-Key", apikey) + .header("Content-Type", "application/json") + .build() + chain.proceed(new) + } + if (BuildConfig.DEBUG) { + addNetworkInterceptor(StethoInterceptor()) + } + if (context != null) { + cache(Cache(context.getCacheDir(), cacheSize)) + } + }.build() + + val retrofit = Retrofit.Builder() + .baseUrl(baseurl) + .addConverterFactory(JsonApiConverterFactory.create(moshi)) + .client(client) + .build() + return retrofit.create(ChargepriceApi::class.java) + } + } +} diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt new file mode 100644 index 00000000..0235b596 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt @@ -0,0 +1,280 @@ +package net.vonforst.evmap.api.chargeprice + +import android.content.Context +import com.squareup.moshi.Json +import moe.banana.jsonapi2.HasMany +import moe.banana.jsonapi2.HasOne + +import moe.banana.jsonapi2.JsonApi +import moe.banana.jsonapi2.Resource +import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.Equatable +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.ui.currency +import kotlin.math.ceil +import kotlin.math.floor + + +@JsonApi(type = "charge_price_request") +class ChargepriceRequest : Resource() { + @field:Json(name = "data_adapter") + lateinit var dataAdapter: String + lateinit var station: ChargepriceStation + lateinit var options: ChargepriceOptions + var tariffs: HasMany? = null + var vehicle: HasOne? = null +} + +data class ChargepriceStation( + val longitude: Double, + val latitude: Double, + val country: String?, + val network: String?, + @Json(name = "charge_points") val chargePoints: List +) { + companion object { + fun fromGoingelectric(geCharger: ChargeLocation): ChargepriceStation { + return ChargepriceStation( + geCharger.coordinates.lng, + geCharger.coordinates.lat, + geCharger.address.country, + geCharger.network, + geCharger.chargepoints.map { + ChargepriceChargepoint(it.power, it.type) + } + ) + } + } +} + +data class ChargepriceChargepoint( + val power: Double, + val plug: String +) + +data class ChargepriceOptions( + @Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null, + val energy: Double? = null, + val duration: Int? = null, + @Json(name = "battery_range") val batteryRange: List? = null, + @Json(name = "car_ac_phases") val carAcPhases: Int? = null, + val currency: String? = null, + @Json(name = "start_time") val startTime: Int? = null, + @Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null, + @Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null +) + +@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() + +@JsonApi(type = "car") +class ChargepriceCar : Resource() { + lateinit var name: String + lateinit var brand: String + + @field:Json(name = "dc_charge_ports") + lateinit var dcChargePorts: List + lateinit var manufacturer: HasOne + + 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 ChargepriceCar + + if (name != other.name) return false + if (brand != other.brand) return false + if (dcChargePorts != other.dcChargePorts) return false + if (manufacturer != other.manufacturer) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + brand.hashCode() + result = 31 * result + dcChargePorts.hashCode() + result = 31 * result + manufacturer.hashCode() + return result + } +} + +@JsonApi(type = "brand") +class ChargepriceBrand : Resource() + +@JsonApi(type = "charge_price") +class ChargePrice : Resource(), Equatable, Cloneable { + lateinit var provider: String + + @field:Json(name = "tariff_name") + lateinit var tariffName: String + lateinit var url: String + + @field:Json(name = "monthly_min_sales") + var monthlyMinSales: Double = 0.0 + + @field:Json(name = "total_monthly_fee") + var totalMonthlyFee: Double = 0.0 + + @field:Json(name = "flat_rate") + var flatRate: Boolean = false + + @field:Json(name = "direct_payment") + var directPayment: Boolean = false + + @field:Json(name = "provider_customer_tariff") + var providerCustomerTariff: Boolean = false + lateinit var currency: String + + @field:Json(name = "start_time") + var startTime: Int = 0 + lateinit var tags: List + + @field:Json(name = "charge_point_prices") + lateinit var chargepointPrices: List + + + fun formatMonthlyFees(ctx: Context): String { + return listOfNotNull( + if (totalMonthlyFee > 0) { + ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency)) + } else null, + if (monthlyMinSales > 0) { + ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency)) + } else null + ).joinToString(", ") + } + + 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 ChargePrice + + if (provider != other.provider) return false + if (tariffName != other.tariffName) return false + if (url != other.url) return false + if (monthlyMinSales != other.monthlyMinSales) return false + if (totalMonthlyFee != other.totalMonthlyFee) return false + if (flatRate != other.flatRate) return false + if (directPayment != other.directPayment) return false + if (providerCustomerTariff != other.providerCustomerTariff) return false + if (currency != other.currency) return false + if (startTime != other.startTime) return false + if (tags != other.tags) return false + if (chargepointPrices != other.chargepointPrices) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + provider.hashCode() + result = 31 * result + tariffName.hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + monthlyMinSales.hashCode() + result = 31 * result + totalMonthlyFee.hashCode() + result = 31 * result + flatRate.hashCode() + result = 31 * result + directPayment.hashCode() + result = 31 * result + providerCustomerTariff.hashCode() + result = 31 * result + currency.hashCode() + result = 31 * result + startTime + result = 31 * result + tags.hashCode() + result = 31 * result + chargepointPrices.hashCode() + return result + } + + public override fun clone(): ChargePrice { + return ChargePrice().apply { + chargepointPrices = this@ChargePrice.chargepointPrices + currency = this@ChargePrice.currency + directPayment = this@ChargePrice.directPayment + flatRate = this@ChargePrice.flatRate + monthlyMinSales = this@ChargePrice.monthlyMinSales + provider = this@ChargePrice.provider + providerCustomerTariff = this@ChargePrice.providerCustomerTariff + startTime = this@ChargePrice.startTime + tags = this@ChargePrice.tags + tariffName = this@ChargePrice.tariffName + totalMonthlyFee = this@ChargePrice.totalMonthlyFee + url = this@ChargePrice.url + } + } +} + +data class ChargepointPrice( + val power: Double, + val plug: String, + val price: Double, + @Json(name = "price_distribution") val priceDistribution: PriceDistribution, + @Json(name = "blocking_fee_start") val blockingFeeStart: Int?, + @Json(name = "no_price_reason") var noPriceReason: String? +) { + fun formatDistribution(ctx: Context): String { + fun percent(value: Double): String { + return ctx.getString(R.string.percent_format, value * 100) + "\u00a0" + } + + fun time(value: Int): String { + val h = floor(value.toDouble() / 60).toInt(); + val min = ceil(value.toDouble() % 60).toInt(); + if (h == 0 && min > 0) return "${min}min"; + // be slightly sloppy (3:01 is shown as 3h) to save space + else if (h > 0 && (min == 0 || min == 1)) return "${h}h"; + else return "%d:%02dh".format(h, min); + } + + // based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js + with(priceDistribution) { + return listOfNotNull( + if (session != null && session > 0.0) { + (if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee) + } else null, + if (kwh != null && kwh > 0.0 && !isOnlyKwh) { + (if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh) + } else null, + if (minute != null && minute > 0.0) { + (if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) + + if (blockingFeeStart != null) { + " (${ + ctx.getString( + R.string.chargeprice_blocking_fee, + time(blockingFeeStart) + ) + })" + } else "" + } else null, + if ((minute == null || minute == 0.0) && blockingFeeStart != null) { + ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart)) + } else null + ).joinToString(" +\u00a0") + } + } +} + +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) +} + +data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable + +data class ChargepriceMeta( + @Json(name = "charge_points") val chargePoints: List +) + +data class ChargepriceChargepointMeta( + val power: Double, + val plug: String, + val energy: Double, + val duration: Double +) \ No newline at end of file diff --git a/app/src/test/java/net/vonforst/evmap/api/chargeprice/ChargepriceApiTest.kt b/app/src/test/java/net/vonforst/evmap/api/chargeprice/ChargepriceApiTest.kt new file mode 100644 index 00000000..41b30a01 --- /dev/null +++ b/app/src/test/java/net/vonforst/evmap/api/chargeprice/ChargepriceApiTest.kt @@ -0,0 +1,76 @@ +package net.vonforst.evmap.api.chargeprice + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.GoingElectricApi +import net.vonforst.evmap.okResponse +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.Assert.assertEquals +import org.junit.Test +import java.net.HttpURLConnection + +class ChargepriceApiTest { + val ge: GoingElectricApi + val webServer = MockWebServer() + val chargeprice: ChargepriceApi + + init { + webServer.start() + + val apikey = "" + val baseurl = webServer.url("/ge/").toString() + ge = GoingElectricApi.create(apikey, baseurl) + chargeprice = ChargepriceApi.create( + apikey, + webServer.url("/cp/").toString() + ) + + webServer.dispatcher = object : Dispatcher() { + val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + + override fun dispatch(request: RecordedRequest): MockResponse { + val segments = request.requestUrl!!.pathSegments + val urlHead = segments.subList(0, 2).joinToString("/") + when (urlHead) { + "ge/chargepoints" -> { + val id = request.requestUrl!!.queryParameter("ge_id") + return okResponse("/chargers/$id.json") + } + "cp/charge_prices" -> { + val body = request.body.readUtf8() + return okResponse("/chargeprice/2105.json") + } + else -> return notFoundResponse + } + } + } + } + + private fun readResource(s: String) = + ChargepriceApiTest::class.java.getResource(s)?.readText() + + @ExperimentalCoroutinesApi + @Test + fun apiTest() { + for (chargepoint in listOf(2105L, 18284L)) { + val charger = runBlocking { ge.getChargepointDetail(chargepoint).body()!! } + .chargelocations[0] as ChargeLocation + println(charger) + + runBlocking { + val result = chargeprice.getChargePrices( + ChargepriceRequest().apply { + dataAdapter = "going_electric" + station = ChargepriceStation.fromGoingelectric(charger) + options = ChargepriceOptions(energy = 22.0, duration = 60) + }, "en" + ) + assertEquals(25, result.size) + } + } + } +} diff --git a/app/src/test/resources/chargeprice/2105.json b/app/src/test/resources/chargeprice/2105.json new file mode 100644 index 00000000..497d84e1 --- /dev/null +++ b/app/src/test/resources/chargeprice/2105.json @@ -0,0 +1,1287 @@ +{ + "data": [ + { + "id": "a7a44895-b23a-47b0-9674-f08039a4af9e", + "type": "charge_price", + "attributes": { + "provider": "EnBW ODR", + "tariff_name": "MobilityMe Privat", + "url": "https://www.odr.de/Mobility_Me/mobilityme.html", + "monthly_min_sales": 0.0, + "total_monthly_fee": 2.95, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "New tariffs for new customers!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "0b6d0281-0b2f-484e-8f1a-1aed19ef9a82" + } + } + } + }, + { + "id": "a1aea427-53b6-4fee-9852-6b77359b9f4b", + "type": "charge_price", + "attributes": { + "provider": "eins", + "tariff_name": "E-Mobil", + "url": "https://www.eins.de/privatkunden/elektromobilitaet/oeffentliches-laden/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.8, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.8, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "9a10db7b-3738-4dd6-b040-9f6bf25ac2da" + } + } + } + }, + { + "id": "b19c3df3-bda9-45c6-9c69-ad92acd57381", + "type": "charge_price", + "attributes": { + "provider": "Enel X", + "tariff_name": "JuicePass", + "url": "https://www.enelx.com/it/en/electric-mobility/products/privates/juicepass-app", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 11.0, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 11.0, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "7221b94b-4128-470e-befd-9f96ada009ef" + } + } + } + }, + { + "id": "466090c2-0d6c-4856-85c3-bb2ef4127ca5", + "type": "charge_price", + "attributes": { + "provider": "EV Rodau", + "tariff_name": "Rodaustrom", + "url": "https://www.ev-rodau.de/Navigation/E-Mobilitaet/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "Existing customers (registriert before 15.02.2021): 0.49/kWh", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 20.68, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 20.68, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "6640c45d-0704-4adb-9a7a-a1afea17b3eb" + } + } + } + }, + { + "id": "e87a391f-b8f6-4581-a190-7e4c542928e9", + "type": "charge_price", + "attributes": { + "provider": "MVV eMotion", + "tariff_name": "MVV eMotion", + "url": "https://mvv.chargecloud.de/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.58, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.58, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "091adf0a-ae49-4c53-bb11-2eccb409685c" + } + } + } + }, + { + "id": "dfbf56b7-424a-4f1b-8749-9a4edca3eef3", + "type": "charge_price", + "attributes": { + "provider": "EnBW", + "tariff_name": "mobility+ Viellader", + "url": "https://www.enbw.com/elektromobilitaet/produkte/mobilityplus-app/laden-und-bezahlen", + "monthly_min_sales": 0.0, + "total_monthly_fee": 4.99, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 6.38, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 6.38, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "14037a35-45d8-46ab-b348-187f7e23eba4" + } + } + } + }, + { + "id": "9f6a2795-0319-4200-b871-7af040a27f14", + "type": "charge_price", + "attributes": { + "provider": "EnBW", + "tariff_name": "ADAC e-CHARGE", + "url": "https://www.adac.de/rund-ums-fahrzeug/e-mobilitaet/laden/adac-e-charge/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "lock", + "text": "Club members only", + "url": "https://www.adac.de/mitgliedschaft/mitglied-werden/" + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 6.38, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 6.38, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "36c2017a-097a-4072-9a58-6ef904b6173d" + } + } + } + }, + { + "id": "ba713da6-a97d-48d7-89c8-f49b529faec0", + "type": "charge_price", + "attributes": { + "provider": "EnBW", + "tariff_name": "mobility+ Standard", + "url": "https://www.enbw.com/elektromobilitaet/produkte/mobilityplus-app/laden-und-bezahlen", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "dba260fc-f006-4ea8-881e-6b9f3f4e0528" + } + } + } + }, + { + "id": "3bf34f4c-de37-45e9-83e9-0300cc09b604", + "type": "charge_price", + "attributes": { + "provider": "Lichtblick SE", + "tariff_name": "FahrStrom", + "url": "https://www.lichtblick.de/e-mobilitaet/fahrstrom-unterwegs#fahrstrom-calculator", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 10.78, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 10.78, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "21a10712-bd10-4f91-89b6-15d20890a0d0" + } + } + } + }, + { + "id": "85e5162a-4306-45a7-82a9-f3a9e6d3d5fc", + "type": "charge_price", + "attributes": { + "provider": "Plugsurfing", + "tariff_name": "Plugsurfing", + "url": "https://www.plugsurfing.com/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 10.78, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 10.78, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "88257a81-924b-49ee-945b-304f1660343b" + } + } + } + }, + { + "id": "dc3be456-3167-4d3c-9d43-ec3ec99b8086", + "type": "charge_price", + "attributes": { + "provider": "evpass", + "tariff_name": "evpass explorer", + "url": "https://www.evpass.ch/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 4.441433303221921, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "ea1454d2-5944-4723-9e98-6918ffc14ac0" + } + } + } + }, + { + "id": "880a67b9-df4c-4fd7-8292-c3292f06d38b", + "type": "charge_price", + "attributes": { + "provider": "evpass", + "tariff_name": "evpass Day Flat", + "url": "https://www.evpass.ch/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 81.30081300813008, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "2c361bb5-6caf-48a9-b769-682c0389164c" + } + } + } + }, + { + "id": "a0f464ee-b508-4ef7-962f-54ac61291435", + "type": "charge_price", + "attributes": { + "provider": "evpass", + "tariff_name": "evpass Anytime Flat", + "url": "https://www.evpass.ch/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 108.40108401084011, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "02cd2e4b-0a27-4f67-a680-65be6b2ed8d7" + } + } + } + }, + { + "id": "e82421e5-bb97-4d4f-bfaa-275e6183148a", + "type": "charge_price", + "attributes": { + "provider": "evpass", + "tariff_name": "evpass Night Flat", + "url": "https://www.evpass.ch/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 45.16711833785005, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 24.390243902439025, + "price_distribution": { + "minute": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "a6be130c-ed42-4d09-b4b3-d5ff018ece44" + } + } + } + }, + { + "id": "36d1692a-1eea-4ca9-a753-06fe0a9b74da", + "type": "charge_price", + "attributes": { + "provider": "Stadtwerke Ingolstadt", + "tariff_name": "SWI e-Motion", + "url": "https://sw-i.de/mobilitaet/elektromobilitaet/oeffentliches-laden/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 10.1978, + "price_distribution": { + "session": 0.2451509149032144, + "kwh": 0.7548490850967855 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 10.1978, + "price_distribution": { + "session": 0.2451509149032144, + "kwh": 0.7548490850967855 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "0efd4ffd-cc9d-4ddd-8a74-5dca40728ff4" + } + } + } + }, + { + "id": "2babdda3-c2b9-4d3e-8bdc-794c10d73b6b", + "type": "charge_price", + "attributes": { + "provider": "evway", + "tariff_name": "evway", + "url": "https://evway.net/de/tarif/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "Transparent provider", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.8, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.8, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "c9fa56cc-6365-467c-b46f-eba2d13f9849" + } + } + } + }, + { + "id": "5e0a3e4d-8657-481a-8483-e6cf2053ee50", + "type": "charge_price", + "attributes": { + "provider": "Move", + "tariff_name": "Move light", + "url": "https://www.move.ch/en/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "New prices: No minute fee on AC!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 14.272809394760616, + "price_distribution": { + "kwh": 0.9050632911392406, + "session": 0.09493670886075949 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 14.272809394760616, + "price_distribution": { + "kwh": 0.9050632911392406, + "session": 0.09493670886075949 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "3b564f24-182f-4cbf-8390-7957f4f9eaa5" + } + } + } + }, + { + "id": "c2bc99d2-ea36-44fa-9279-977ced74a309", + "type": "charge_price", + "attributes": { + "provider": "Move", + "tariff_name": "Move comfort", + "url": "https://www.move.ch/en/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 4.441433303221921, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "New prices: No minute fee on AC!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 12.285456187895214, + "price_distribution": { + "kwh": 0.8897058823529412, + "session": 0.1102941176470588 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 12.285456187895214, + "price_distribution": { + "kwh": 0.8897058823529412, + "session": 0.1102941176470588 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "e7cfcc90-5ff7-4821-b315-ab1e9a46cd70" + } + } + } + }, + { + "id": "f413f87c-2b0b-480a-93d0-4c831a367389", + "type": "charge_price", + "attributes": { + "provider": "e-laden", + "tariff_name": "e-laden (Stadtwerke Bruchsal)", + "url": "https://www.e-laden.info/html/page.php?page_id=8", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 7.6, + "price_distribution": { + "session": 0.13157894736842105, + "kwh": 0.868421052631579, + "minute": 0.0 + }, + "blocking_fee_start": 120, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 7.6, + "price_distribution": { + "session": 0.13157894736842105, + "kwh": 0.868421052631579, + "minute": 0.0 + }, + "blocking_fee_start": 120, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "7bc071d3-a170-4755-9b71-bc87642e3a2e" + } + } + } + }, + { + "id": "3bc056aa-dc28-4493-930c-f5ec00aca00b", + "type": "charge_price", + "attributes": { + "provider": "e-laden", + "tariff_name": "e-laden (Stadtwerke Baden-Baden)", + "url": "https://www.e-laden.info/html/page.php?page_id=8", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "New prices with 01.03.!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 60, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.58, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 60, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "b9f932f5-dd09-46f5-862e-3d031963d08f" + } + } + } + }, + { + "id": "8e86ed49-d143-4f6a-9340-a27c7ffc6c70", + "type": "charge_price", + "attributes": { + "provider": "swisscharge", + "tariff_name": "Swisscharge", + "url": "https://www.swisscharge.ch/", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 16.25, + "price_distribution": { + "kwh": 0.6363076923076924, + "minute": 0.3636923076923077 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 16.25, + "price_distribution": { + "kwh": 0.6363076923076924, + "minute": 0.3636923076923077 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "04aab3f1-1b4b-4c6b-abb8-6e5d38f5237e" + } + } + } + }, + { + "id": "8c0ba631-7c48-4d11-80bd-e2bb61e95f48", + "type": "charge_price", + "attributes": { + "provider": "Stadtwerke Tübingen", + "tariff_name": "Lade TüStrom", + "url": "https://www.swtue.de/e-mobilitaet/ladestationen-in-tuebingen.html", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 9.68, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 9.68, + "price_distribution": { + "kwh": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "599dce51-072a-4265-b866-e5941491329a" + } + } + } + }, + { + "id": "e1b6ea8b-8b9b-4b84-9e8a-ed2d978de830", + "type": "charge_price", + "attributes": { + "provider": "E.ON", + "tariff_name": "Drive", + "url": "https://www.eon.de/de/pk/e-mobility/unterwegs.html", + "monthly_min_sales": 0.0, + "total_monthly_fee": 4.95, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "info", + "text": "From 01.01.2021", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 5.95, + "price_distribution": { + "session": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 5.95, + "price_distribution": { + "session": 1.0 + }, + "blocking_fee_start": null, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "ae2e27d5-b74f-418c-b345-b53ac40c241f" + } + } + } + }, + { + "id": "8f221fdb-af5a-4b83-a54a-a176c3f969a4", + "type": "charge_price", + "attributes": { + "provider": "Maingau Energie", + "tariff_name": "EinfachStromLaden (Business)", + "url": "https://www.maingau-energie.de/e-mobilit%C3%A4t/autostrom-tarif", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "alert", + "text": "Higher prices might apply if used often!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 10.559999999999999, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 10.559999999999999, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "82314aed-b2bc-406f-9343-5b0ce9c9ad03" + } + } + }, + "links": { + "open_app_at_station": "https://api.chargeprice.app/v1/companies/f7acbbc7-3247-4298-b3ed-9c61c4860a13/deeplinks?filter[longitude]=9.57108&filter[latitude]=54.5116" + } + }, + { + "id": "60691e0f-1e71-4027-a431-de7543bb2464", + "type": "charge_price", + "attributes": { + "provider": "Maingau Energie", + "tariff_name": "EinfachStromLaden (Privat)", + "url": "https://www.maingau-energie.de/e-mobilit%C3%A4t/autostrom-tarif", + "monthly_min_sales": 0.0, + "total_monthly_fee": 0.0, + "flat_rate": false, + "direct_payment": false, + "provider_customer_tariff": false, + "currency": "EUR", + "start_time": 720, + "tags": [ + { + "kind": "alert", + "text": "Higher prices might apply if used often!", + "url": null + } + ], + "charge_point_prices": [ + { + "power": 22.0, + "plug": "Typ2", + "price": 8.36, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + }, + { + "power": 2.3, + "plug": "Schuko", + "price": 8.36, + "price_distribution": { + "kwh": 1.0, + "minute": 0.0 + }, + "blocking_fee_start": 240, + "no_price_reason": null + } + ] + }, + "relationships": { + "tariff": { + "data": { + "type": "tariff", + "id": "8fdeb6b4-2d72-4cff-87d8-081fa0ac4688" + } + } + }, + "links": { + "open_app_at_station": "https://api.chargeprice.app/v1/companies/f7acbbc7-3247-4298-b3ed-9c61c4860a13/deeplinks?filter[longitude]=9.57108&filter[latitude]=54.5116" + } + } + ], + "meta": { + "charge_points": [ + { + "power": 22.0, + "plug": "Typ2", + "energy": 22, + "duration": 60 + }, + { + "power": 2.3, + "plug": "Schuko", + "energy": 22, + "duration": 60 + } + ] + } +} \ No newline at end of file From 3d30e746a0fc72fb1355221983bab7da6d37c631 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Thu, 8 Apr 2021 22:56:09 +0200 Subject: [PATCH 2/3] Implement Chargeprice GUI --- _img/powered_by_chargeprice.svg | 130 +++++++++++++ .../evmap/adapter/DataBindingAdapters.kt | 77 ++++++++ .../evmap/api/chargeprice/ChargepriceApi.kt | 1 - .../api/goingelectric/GoingElectricApi.kt | 14 +- .../evmap/fragment/ChargepriceFragment.kt | 163 +++++++++++++++++ .../vonforst/evmap/fragment/MapFragment.kt | 5 +- .../evmap/fragment/SettingsFragment.kt | 27 +++ .../evmap/storage/PreferenceDataSource.kt | 18 ++ .../evmap/ui/BalancedBreakingTextView.kt | 33 ++++ .../net/vonforst/evmap/ui/BindingAdapters.kt | 59 ++++++ .../evmap/ui/CheckableConstraintLayout.kt | 48 +++++ .../evmap/viewmodel/ChargepriceViewModel.kt | 152 +++++++++++++++ .../evmap/viewmodel/SettingsViewModel.kt | 33 ++++ .../res/anim/chargeprice_dialog_enter.xml | 17 ++ .../main/res/anim/chargeprice_dialog_exit.xml | 17 ++ app/src/main/res/drawable/button_outline.xml | 15 ++ .../res/drawable/ic_chargeprice_alert.xml | 10 + .../main/res/drawable/ic_chargeprice_info.xml | 10 + .../main/res/drawable/ic_chargeprice_lock.xml | 10 + .../main/res/drawable/ic_chargeprice_star.xml | 10 + app/src/main/res/drawable/ic_close.xml | 10 + .../drawable/ic_powered_by_chargeprice.xml | 149 +++++++++++++++ .../main/res/drawable/rounded_rect_16dp.xml | 6 + .../main/res/layout/fragment_chargeprice.xml | 173 ++++++++++++++++++ app/src/main/res/layout/item_chargeprice.xml | 154 ++++++++++++++++ .../main/res/layout/item_chargeprice_tag.xml | 40 ++++ .../main/res/layout/item_connector_button.xml | 71 +++++++ app/src/main/res/layout/item_detail.xml | 8 +- .../res/layout/item_detail_openinghours.xml | 26 +-- app/src/main/res/menu/chargeprice.xml | 10 + app/src/main/res/navigation/nav_graph.xml | 20 ++ app/src/main/res/values-de/strings.xml | 23 +++ app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 23 +++ app/src/main/res/values/styles.xml | 5 + app/src/main/res/xml/settings.xml | 15 +- 36 files changed, 1559 insertions(+), 28 deletions(-) create mode 100644 _img/powered_by_chargeprice.svg create mode 100644 app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt create mode 100644 app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt create mode 100644 app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt create mode 100644 app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt create mode 100644 app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt create mode 100644 app/src/main/res/anim/chargeprice_dialog_enter.xml create mode 100644 app/src/main/res/anim/chargeprice_dialog_exit.xml create mode 100644 app/src/main/res/drawable/button_outline.xml create mode 100644 app/src/main/res/drawable/ic_chargeprice_alert.xml create mode 100644 app/src/main/res/drawable/ic_chargeprice_info.xml create mode 100644 app/src/main/res/drawable/ic_chargeprice_lock.xml create mode 100644 app/src/main/res/drawable/ic_chargeprice_star.xml create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_powered_by_chargeprice.xml create mode 100644 app/src/main/res/drawable/rounded_rect_16dp.xml create mode 100644 app/src/main/res/layout/fragment_chargeprice.xml create mode 100644 app/src/main/res/layout/item_chargeprice.xml create mode 100644 app/src/main/res/layout/item_chargeprice_tag.xml create mode 100644 app/src/main/res/layout/item_connector_button.xml create mode 100644 app/src/main/res/menu/chargeprice.xml diff --git a/_img/powered_by_chargeprice.svg b/_img/powered_by_chargeprice.svg new file mode 100644 index 00000000..d97f8e43 --- /dev/null +++ b/_img/powered_by_chargeprice.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index 702c7db5..f52334de 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -1,16 +1,24 @@ package net.vonforst.evmap.adapter import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import net.vonforst.evmap.BR import net.vonforst.evmap.R import net.vonforst.evmap.api.availability.ChargepointStatus +import net.vonforst.evmap.api.chargeprice.ChargePrice +import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta +import net.vonforst.evmap.api.chargeprice.ChargepriceTag import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.databinding.ItemChargepriceBinding +import net.vonforst.evmap.databinding.ItemConnectorButtonBinding +import net.vonforst.evmap.ui.CheckableConstraintLayout import net.vonforst.evmap.viewmodel.FavoritesViewModel interface Equatable { @@ -88,4 +96,73 @@ class FavoritesAdapter(val vm: FavoritesViewModel) : override fun getItemViewType(position: Int): Int = R.layout.item_favorite override fun getItemId(position: Int): Long = getItem(position).charger.id +} + +class ChargepriceAdapter() : + DataBindingAdapter() { + + val viewPool = RecyclerView.RecycledViewPool(); + var meta: ChargepriceChargepointMeta? = null + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val holder = super.onCreateViewHolder(parent, viewType) + val binding = holder.binding as ItemChargepriceBinding + binding.rvTags.apply { + adapter = ChargepriceTagsAdapter() + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply { + recycleChildrenOnDetach = true + } + itemAnimator = null + setRecycledViewPool(viewPool) + } + return holder + } + + override fun bind(holder: ViewHolder, item: ChargePrice) { + super.bind(holder, item) + (holder.binding as ItemChargepriceBinding).meta = meta + } +} + +class CheckableConnectorAdapter : DataBindingAdapter() { + private var checkedItem: Int = 0 + + override fun getItemViewType(position: Int): Int = R.layout.item_connector_button + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + super.bind(holder, getItem(position)) + val binding = holder.binding as ItemConnectorButtonBinding + val root = binding.root as CheckableConstraintLayout + root.isChecked = checkedItem == position + root.setOnClickListener { + root.isChecked = true + } + root.setOnCheckedChangeListener { v: View, checked: Boolean -> + if (checked) { + checkedItem = position + notifyDataSetChanged() + onCheckedItemChangedListener?.invoke(getCheckedItem()) + } + } + } + + fun getCheckedItem(): Chargepoint = getItem(checkedItem) + + fun setCheckedItem(item: Chargepoint) { + checkedItem = currentList.indexOf(item) + } + + var onCheckedItemChangedListener: ((Chargepoint) -> Unit)? = null +} + +class ChargepriceTagsAdapter() : + DataBindingAdapter() { + override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt index e1e1d1f5..24c410b9 100644 --- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt @@ -41,7 +41,6 @@ interface ChargepriceApi { .add(jsonApiAdapterFactory) .add(KotlinJsonAdapterFactory()) .build() - fun create( apikey: String, baseurl: String = "https://api.chargeprice.app/v1/", diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt index f6333be6..6c3f502f 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt @@ -69,6 +69,13 @@ interface GoingElectricApi { companion object { private val cacheSize = 10L * 1024 * 1024 // 10MB + val moshi = Moshi.Builder() + .add(ChargepointListItemJsonAdapterFactory()) + .add(JsonObjectOrFalseAdapter.Factory()) + .add(HoursAdapter()) + .add(InstantAdapter()) + .build() + fun create( apikey: String, baseurl: String = "https://api.goingelectric.de", @@ -90,13 +97,6 @@ interface GoingElectricApi { } }.build() - val moshi = Moshi.Builder() - .add(ChargepointListItemJsonAdapterFactory()) - .add(JsonObjectOrFalseAdapter.Factory()) - .add(HoursAdapter()) - .add(InstantAdapter()) - .build() - val retrofit = Retrofit.Builder() .baseUrl(baseurl) .addConverterFactory(MoshiConverterFactory.create(moshi)) diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt new file mode 100644 index 00000000..28937a0f --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt @@ -0,0 +1,163 @@ +package net.vonforst.evmap.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import net.vonforst.evmap.MapsActivity +import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.ChargepriceAdapter +import net.vonforst.evmap.adapter.CheckableConnectorAdapter +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.api.goingelectric.GoingElectricApi +import net.vonforst.evmap.databinding.FragmentChargepriceBinding +import net.vonforst.evmap.viewmodel.ChargepriceViewModel +import net.vonforst.evmap.viewmodel.viewModelFactory +import java.text.NumberFormat + +class ChargepriceFragment : DialogFragment() { + private lateinit var binding: FragmentChargepriceBinding + + private val vm: ChargepriceViewModel by viewModels(factoryProducer = { + viewModelFactory { + ChargepriceViewModel( + requireActivity().application, + getString(R.string.chargeprice_key) + ) + } + }) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + dialog?.window?.attributes?.windowAnimations = R.style.ChargepriceDialogAnimation + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_chargeprice, container, false + ) + binding.lifecycleOwner = this + binding.vm = vm + + binding.toolbar.inflateMenu(R.menu.chargeprice) + binding.toolbar.setTitle(R.string.chargeprice_title) + + return binding.root + } + + override fun onStart() { + super.onStart() + + // dialog with 95% screen height + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + (resources.displayMetrics.heightPixels * 0.95).toInt() + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val toolbar = view.findViewById(R.id.toolbar) as Toolbar + + val navController = findNavController() + toolbar.setupWithNavController( + navController, + (requireActivity() as MapsActivity).appBarConfiguration + ) + + val jsonAdapter = GoingElectricApi.moshi.adapter(ChargeLocation::class.java) + val charger = jsonAdapter.fromJson(requireArguments().getString(ARG_CHARGER)!!)!! + vm.charger.value = charger + if (vm.chargepoint.value == null) { + vm.chargepoint.value = charger.chargepointsMerged.get(0) + } + + val chargepriceAdapter = ChargepriceAdapter().apply { + onClickListener = { + (requireActivity() as MapsActivity).openUrl(it.url) + } + } + binding.chargePricesList.apply { + adapter = chargepriceAdapter + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + addItemDecoration( + DividerItemDecoration( + context, LinearLayoutManager.VERTICAL + ) + ) + } + vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) { + chargepriceAdapter.meta = it.data + } + + val connectorsAdapter = CheckableConnectorAdapter() + binding.connectorsList.apply { + adapter = connectorsAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + + val observer: Observer = Observer { + connectorsAdapter.setCheckedItem(it) + } + vm.chargepoint.observe(viewLifecycleOwner, observer) + connectorsAdapter.onCheckedItemChangedListener = { + vm.chargepoint.removeObserver(observer) + vm.chargepoint.value = it + vm.chargepoint.observe(viewLifecycleOwner, observer) + } + + binding.imgChargepriceLogo.setOnClickListener { + (requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric") + } + + binding.btnSettings.setOnClickListener { + navController.navigate(R.id.action_chargeprice_to_settingsFragment) + } + + binding.batteryRange.setLabelFormatter { value: Float -> + val fmt = NumberFormat.getNumberInstance() + fmt.maximumFractionDigits = 0 + fmt.format(value.toDouble()) + } + + binding.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_close -> { + dismiss() + true + } + else -> false + } + } + } + + companion object { + val ARG_CHARGER = "charger" + + fun showCharger(charger: ChargeLocation): Bundle { + return Bundle().apply { + putString( + ARG_CHARGER, + GoingElectricApi.moshi.adapter(ChargeLocation::class.java).toJson(charger) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index 4f1a2278..5525ce96 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -296,8 +296,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } binding.detailView.btnChargeprice.setOnClickListener { val charger = vm.charger.value?.data ?: return@setOnClickListener - (activity as? MapsActivity)?.openUrl( - "https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric" + findNavController().navigate( + R.id.action_map_to_chargepriceFragment, + ChargepriceFragment.showCharger(charger) ) } binding.detailView.topPart.setOnClickListener { diff --git a/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt index 148dbb37..7d85892c 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt @@ -4,20 +4,33 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import net.vonforst.evmap.MapsActivity import net.vonforst.evmap.R import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.updateNightMode +import net.vonforst.evmap.viewmodel.SettingsViewModel +import net.vonforst.evmap.viewmodel.viewModelFactory class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { private lateinit var prefs: PreferenceDataSource + private val vm: SettingsViewModel by viewModels(factoryProducer = { + viewModelFactory { + SettingsViewModel( + requireActivity().application, + getString(R.string.chargeprice_key) + ) + } + }) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById(R.id.toolbar) as Toolbar @@ -28,6 +41,20 @@ class SettingsFragment : PreferenceFragmentCompat(), navController, (requireActivity() as MapsActivity).appBarConfiguration ) + + val myVehiclePreference = findPreference("chargeprice_my_vehicle")!! + myVehiclePreference.isEnabled = false + vm.vehicles.observe(viewLifecycleOwner) { res -> + res.data?.let { cars -> + val sortedCars = cars.sortedBy { it.brand } + myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray() + myVehiclePreference.entries = + sortedCars.map { "${it.brand} ${it.name}" }.toTypedArray() + myVehiclePreference.isEnabled = true + myVehiclePreference.summary = cars.find { it.id == prefs.chargepriceMyVehicle } + ?.let { "${it.brand} ${it.name}" } + } + } } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt index 782aba1a..486b7dc7 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt @@ -97,4 +97,22 @@ class PreferenceDataSource(val context: Context) { set(value) { sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply() } + + var chargepriceMyVehicle: String? + get() = sp.getString("chargeprice_my_vehicle", null) + set(value) { + sp.edit().putString("chargeprice_my_vehicle", value).apply() + } + + var chargepriceNoBaseFee: Boolean + get() = sp.getBoolean("chargeprice_no_base_fee", false) + set(value) { + sp.edit().putBoolean("chargeprice_no_base_fee", value).apply() + } + + var chargepriceShowProviderCustomerTariffs: Boolean + get() = sp.getBoolean("chargeprice_show_provider_customer_tariffs", false) + set(value) { + sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply() + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt b/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt new file mode 100644 index 00000000..844f84ec --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt @@ -0,0 +1,33 @@ +package net.vonforst.evmap.ui + +import android.content.Context +import android.text.Layout +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import kotlin.math.ceil + +class BalancedBreakingTextView(context: Context, attrs: AttributeSet) : + AppCompatTextView(context, attrs) { + + @Override + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + if (layout != null) { + val width = + ceil(getMaxLineWidth(layout)).toInt() + compoundPaddingLeft + compoundPaddingRight + val height = measuredHeight + setMeasuredDimension(width, height) + } + } + + private fun getMaxLineWidth(layout: Layout): Float { + var maxWidth = 0.0f + for (i in 0 until layout.lineCount) { + if (layout.getLineWidth(i) > maxWidth) { + maxWidth = layout.getLineWidth(i) + } + } + return maxWidth + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt index 68f6d60c..9d3cc87c 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt @@ -11,10 +11,13 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.use import androidx.core.text.HtmlCompat import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import androidx.databinding.InverseBindingListener import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.slider.RangeSlider import net.vonforst.evmap.R import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.api.iconForPlugType @@ -177,6 +180,35 @@ fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) { } } +@BindingAdapter("chargepriceTagColor") +fun setChargepriceTagColor(view: TextView, kind: String) { + view.backgroundTintList = ColorStateList.valueOf( + ContextCompat.getColor( + view.context, + when (kind) { + "star" -> R.color.chargeprice_star + "alert" -> R.color.chargeprice_alert + "info" -> R.color.chargeprice_info + "lock" -> R.color.chargeprice_lock + else -> R.color.chip_background + } + ) + ) +} + +@BindingAdapter("chargepriceTagIcon") +fun setChargepriceTagIcon(view: TextView, kind: String) { + view.setCompoundDrawablesRelativeWithIntrinsicBounds( + when (kind) { + "star" -> R.drawable.ic_chargeprice_star + "alert" -> R.drawable.ic_chargeprice_alert + "info" -> R.drawable.ic_chargeprice_info + "lock" -> R.drawable.ic_chargeprice_lock + else -> 0 + }, 0, 0, 0 + ) +} + private fun availabilityColor( status: List?, context: Context @@ -210,4 +242,31 @@ fun availabilityText(status: List?): String? { fun flatten(it: Iterable>?): List? { return it?.flatten() +} + +fun currency(currency: String): String { + // shorthands for currencies + return when (currency) { + "EUR" -> "€" + "USD" -> "$" + "DKK", "SEK", "NOK" -> "kr." + "PLN" -> "zł" + "CHF" -> "Fr." + "CZK" -> "Kč" + "GBP" -> "£" + "HRK" -> "kn" + "HUF" -> "Ft" + "ISK" -> "Kr" + else -> currency + } +} + +@InverseBindingAdapter(attribute = "app:values") +fun getRangeSliderValue(slider: RangeSlider) = slider.values + +@BindingAdapter("app:valuesAttrChanged") +fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) { + slider.addOnChangeListener { _, _, _ -> + attrChange.onChange() + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt b/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt new file mode 100644 index 00000000..92443f65 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt @@ -0,0 +1,48 @@ +package net.vonforst.evmap.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.Checkable +import androidx.constraintlayout.widget.ConstraintLayout + + +class CheckableConstraintLayout(ctx: Context, attrs: AttributeSet) : ConstraintLayout(ctx, attrs), + Checkable { + private var onCheckedChangeListener: ((View, Boolean) -> Unit)? = null + private var checked = false + private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked) + + override fun setChecked(b: Boolean) { + if (b != checked) { + checked = b; + refreshDrawableState(); + onCheckedChangeListener?.invoke(this, checked); + } + } + + override fun isChecked(): Boolean { + return checked + } + + override fun toggle() { + checked = !checked + } + + override fun onCreateDrawableState(extraSpace: Int): IntArray? { + val drawableState = super.onCreateDrawableState(extraSpace + 1) + if (isChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET) + } + return drawableState + } + + /** + * Register a callback to be invoked when the checked state of this view changes. + * + * @param listener the callback to call on checked state change + */ + fun setOnCheckedChangeListener(listener: (View, Boolean) -> Unit) { + onCheckedChangeListener = listener + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt new file mode 100644 index 00000000..e3d423bb --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt @@ -0,0 +1,152 @@ +package net.vonforst.evmap.viewmodel + +import android.app.Application +import androidx.lifecycle.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.banana.jsonapi2.HasOne +import net.vonforst.evmap.api.chargeprice.* +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.storage.PreferenceDataSource +import java.io.IOException +import java.util.* + +class ChargepriceViewModel(application: Application, chargepriceApiKey: String) : + AndroidViewModel(application) { + private var api = ChargepriceApi.create(chargepriceApiKey) + private var prefs = PreferenceDataSource(application) + + val charger: MutableLiveData by lazy { + MutableLiveData() + } + + val chargepoint: MutableLiveData by lazy { + MutableLiveData() + } + + val vehicle: LiveData by lazy { + MutableLiveData().apply { + value = prefs.chargepriceMyVehicle?.let { ChargepriceCar().apply { id = it } } + } + } + + val batteryRange: MutableLiveData> by lazy { + MutableLiveData>().apply { + value = listOf(20f, 80f) + } + } + + val chargePrices: MediatorLiveData>> by lazy { + MediatorLiveData>>().apply { + value = Resource.loading(null) + listOf(charger, vehicle, batteryRange).forEach { + addSource(it) { + loadPrices() + } + } + } + } + + val chargePriceMeta: MutableLiveData> by lazy { + MutableLiveData>().apply { + value = Resource.loading(null) + } + } + + val chargePricesForChargepoint: MediatorLiveData>> by lazy { + MediatorLiveData>>().apply { + listOf(chargePrices, chargepoint).forEach { + addSource(it) { + val cps = chargePrices.value + val chargepoint = chargepoint.value + if (cps == null || chargepoint == null) { + value = null + } else if (cps.status == Status.ERROR) { + value = Resource.error(cps.message, null) + } else if (cps.status == Status.LOADING) { + value = Resource.loading(null) + } else { + value = Resource.success(cps.data!!.map { cp -> + val filteredPrices = + cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power } + if (filteredPrices.isEmpty()) { + null + } else { + cp.clone().apply { + chargepointPrices = filteredPrices + } + } + }.filterNotNull().sortedBy { it.chargepointPrices.first().price }) + } + } + } + } + } + + val chargepriceMetaForChargepoint: MediatorLiveData> by lazy { + MediatorLiveData>().apply { + listOf(chargePriceMeta, chargepoint).forEach { + addSource(it) { + val cpMeta = chargePriceMeta.value + val chargepoint = chargepoint.value + if (cpMeta == null || chargepoint == null) { + value = null + } else if (cpMeta.status == Status.ERROR) { + value = Resource.error(cpMeta.message, null) + } else if (cpMeta.status == Status.LOADING) { + value = Resource.loading(null) + } else { + value = + Resource.success(cpMeta.data!!.chargePoints.filter { it.plug == chargepoint.type && it.power == chargepoint.power }[0]) + } + } + } + } + } + + private var loadPricesJob: Job? = null + private fun loadPrices() { + chargePrices.value = Resource.loading(null) + val geCharger = charger.value + val car = vehicle.value + if (geCharger == null || car == null) { + chargePrices.value = Resource.error(null, null) + return + } + + loadPricesJob?.cancel() + loadPricesJob = viewModelScope.launch { + delay(800) + try { + val result = api.getChargePrices(ChargepriceRequest().apply { + dataAdapter = "going_electric" + station = ChargepriceStation.fromGoingelectric(geCharger) + vehicle = HasOne(car) + options = ChargepriceOptions( + batteryRange = batteryRange.value!!.map { it.toDouble() }, + providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, + maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null + ) + }, getChargepriceLanguage()) + val meta = + result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta + chargePrices.value = Resource.success(result) + chargePriceMeta.value = Resource.success(meta) + } catch (e: IOException) { + chargePrices.value = Resource.error(e.message, null) + chargePriceMeta.value = Resource.error(e.message, null) + } + } + } + + private fun getChargepriceLanguage(): String { + val locale = Locale.getDefault().language + return if (ChargepriceApi.supportedLanguages.contains(locale)) { + locale + } else { + "en" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..e5123e72 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt @@ -0,0 +1,33 @@ +package net.vonforst.evmap.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import net.vonforst.evmap.api.chargeprice.ChargepriceApi +import net.vonforst.evmap.api.chargeprice.ChargepriceCar +import java.io.IOException + +class SettingsViewModel(application: Application, chargepriceApiKey: String) : + AndroidViewModel(application) { + private var api = ChargepriceApi.create(chargepriceApiKey) + + val vehicles: MutableLiveData>> by lazy { + MutableLiveData>>().apply { + value = Resource.loading(null) + loadVehicles() + } + } + + private fun loadVehicles() { + viewModelScope.launch { + try { + val result = api.getVehicles() + vehicles.value = Resource.success(result) + } catch (e: IOException) { + vehicles.value = Resource.error(e.message, null) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/chargeprice_dialog_enter.xml b/app/src/main/res/anim/chargeprice_dialog_enter.xml new file mode 100644 index 00000000..1c6531f0 --- /dev/null +++ b/app/src/main/res/anim/chargeprice_dialog_enter.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/chargeprice_dialog_exit.xml b/app/src/main/res/anim/chargeprice_dialog_exit.xml new file mode 100644 index 00000000..a30075e0 --- /dev/null +++ b/app/src/main/res/anim/chargeprice_dialog_exit.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_outline.xml b/app/src/main/res/drawable/button_outline.xml new file mode 100644 index 00000000..bb4c2ade --- /dev/null +++ b/app/src/main/res/drawable/button_outline.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_alert.xml b/app/src/main/res/drawable/ic_chargeprice_alert.xml new file mode 100644 index 00000000..2484c15a --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_alert.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_info.xml b/app/src/main/res/drawable/ic_chargeprice_info.xml new file mode 100644 index 00000000..ddb2b894 --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_lock.xml b/app/src/main/res/drawable/ic_chargeprice_lock.xml new file mode 100644 index 00000000..fb4d8fbe --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_lock.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_star.xml b/app/src/main/res/drawable/ic_chargeprice_star.xml new file mode 100644 index 00000000..c1ee35ac --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_star.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..95cc170f --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_powered_by_chargeprice.xml b/app/src/main/res/drawable/ic_powered_by_chargeprice.xml new file mode 100644 index 00000000..51606907 --- /dev/null +++ b/app/src/main/res/drawable/ic_powered_by_chargeprice.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/rounded_rect_16dp.xml b/app/src/main/res/drawable/rounded_rect_16dp.xml new file mode 100644 index 00000000..ac4e6385 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rect_16dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chargeprice.xml b/app/src/main/res/layout/fragment_chargeprice.xml new file mode 100644 index 00000000..51c1846a --- /dev/null +++ b/app/src/main/res/layout/fragment_chargeprice.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +