diff --git a/app/build.gradle b/app/build.gradle index 757a0d76..7ddb7c77 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,8 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.5.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' testImplementation 'junit:junit:4.12' + //noinspection GradleDependency + testImplementation 'org.json:json:20080701' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/app/src/main/java/com/johan/evmap/api/AvailabilityDetector.kt b/app/src/main/java/com/johan/evmap/api/AvailabilityDetector.kt new file mode 100644 index 00000000..5841410f --- /dev/null +++ b/app/src/main/java/com/johan/evmap/api/AvailabilityDetector.kt @@ -0,0 +1,86 @@ +package com.johan.evmap.api + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import okhttp3.* +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.TimeUnit + +private const val radius = 100 // max radius in meters + +interface AvailabilityDetector { + suspend fun getAvailability(location: ChargeLocation): Map> +} + +enum class ChargepointStatus { + AVAILABLE, UNKNOWN, CHARGING +} + +class ChargecloudAvailabilityDetector(private val client: OkHttpClient, + private val operatorId: String): AvailabilityDetector { + @ExperimentalCoroutinesApi + override suspend fun getAvailability(location: ChargeLocation): Map> { + val url = "https://app.chargecloud.de/emobility:ocpi/$operatorId/app/2.0/locations?latitude=${location.coordinates.lat}&longitude=${location.coordinates.lng}&radius=$radius&offset=0&limit=10" + val request = Request.Builder().url(url).build() + val response = client.newCall(request).await() + + if (!response.isSuccessful) throw IOException(response.message()) + + val json = JSONObject(response.body()!!.string()) + + val statusMessage = json.getString("status_message") + if (statusMessage != "Success") throw IOException(statusMessage) + + val data = json.getJSONArray("data") + if (data.length() > 1) throw IOException("found multiple candidates.") + if (data.length() == 0) throw IOException("no candidates found.") + + val evses = data.getJSONObject(0).getJSONArray("evses") + val chargepointStatus = mutableMapOf>() + evses.iterator().forEach { evse -> + evse.getJSONArray("connectors").iterator().forEach connector@{ connector -> + val type = getType(connector.getString("standard")) + val power = connector.getDouble("max_power") + val status = ChargepointStatus.valueOf(connector.getString("status")) + if (type == null) return@connector + + var chargepoint = chargepointStatus.keys.filter { + it.type == type + it.power == power + }.getOrNull(0) + val statusList: List + if (chargepoint == null) { + chargepoint = Chargepoint(type, power, 1) + statusList = listOf(status) + } else { + val previousStatus = chargepointStatus[chargepoint]!! + statusList = previousStatus + listOf(status) + chargepointStatus.remove(chargepoint) + chargepoint = + Chargepoint(chargepoint.type, chargepoint.power, chargepoint.count + 1) + } + + chargepointStatus[chargepoint] = statusList + } + } + return chargepointStatus + } + + private fun getType(string: String): String? { + return when (string) { + "IEC_62196_T2" -> Chargepoint.TYPE_2 + "DOMESTIC_F" -> Chargepoint.SCHUKO + "IEC_62196_T2_COMBO" -> Chargepoint.CCS + "CHADEMO" -> Chargepoint.CHADEMO + else -> null + } + } +} + +private val okhttp = OkHttpClient.Builder() + .readTimeout(10, TimeUnit.SECONDS) + .connectTimeout(10, TimeUnit.SECONDS) + .build() +val availabilityDetectors = listOf( + ChargecloudAvailabilityDetector(okhttp, "6336fe713f2eb7fa04b97ff6651b76f8") +) \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt b/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt index e1ac12b1..fade54e9 100644 --- a/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt +++ b/app/src/main/java/com/johan/evmap/api/GoingElectricModel.kt @@ -179,4 +179,12 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq } return "$powerFmt kW" } + + companion object { + const val TYPE_2 = "Type2" + const val CCS = "CCS" + const val SCHUKO = "Schuko" + const val CHADEMO = "CHAdeMO" + const val SUPERCHARGER = "Tesla Supercharger" + } } \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/api/Utils.kt b/app/src/main/java/com/johan/evmap/api/Utils.kt new file mode 100644 index 00000000..315aa201 --- /dev/null +++ b/app/src/main/java/com/johan/evmap/api/Utils.kt @@ -0,0 +1,38 @@ +package com.johan.evmap.api + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import java.io.IOException +import kotlin.coroutines.resumeWithException + +operator fun JSONArray.iterator(): Iterator + = (0 until length()).asSequence().map { get(it) as T }.iterator() + +@ExperimentalCoroutinesApi +suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + continuation.resume(response) {} + } + + override fun onFailure(call: Call, e: IOException) { + if (continuation.isCancelled) return + continuation.resumeWithException(e) + } + }) + + continuation.invokeOnCancellation { + try { + cancel() + } catch (ex: Throwable) { + //Ignore cancel exception + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt b/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt index 27db5aef..a71c5c8f 100644 --- a/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/com/johan/evmap/ui/BindingAdapters.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.floatingactionbutton.FloatingActionButton import com.johan.evmap.R +import com.johan.evmap.api.Chargepoint @BindingAdapter("goneUnless") @@ -51,11 +52,11 @@ fun setRecyclerViewData(recyclerView: ViewPager2, items: List?) { fun getConnectorItem(view: ImageView, type: String) { view.setImageResource( when (type) { - "CCS" -> R.drawable.ic_connector_ccs - "CHAdeMO" -> R.drawable.ic_connector_chademo - "Schuko" -> R.drawable.ic_connector_schuko - "Tesla Supercharger" -> R.drawable.ic_connector_supercharger - "Typ2" -> R.drawable.ic_connector_typ2 + Chargepoint.CCS -> R.drawable.ic_connector_ccs + Chargepoint.CHADEMO -> R.drawable.ic_connector_chademo + Chargepoint.SCHUKO -> R.drawable.ic_connector_schuko + Chargepoint.SUPERCHARGER -> R.drawable.ic_connector_supercharger + Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2 // TODO: add other connectors else -> 0 } diff --git a/app/src/test/java/com/johan/evmap/ApiTests.kt b/app/src/test/java/com/johan/evmap/ApiTests.kt index f163e944..c858f273 100644 --- a/app/src/test/java/com/johan/evmap/ApiTests.kt +++ b/app/src/test/java/com/johan/evmap/ApiTests.kt @@ -1,7 +1,8 @@ package com.johan.evmap -import com.johan.evmap.api.ChargeLocation -import com.johan.evmap.api.GoingElectricApi +import com.johan.evmap.api.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import org.junit.Test class ApiTests { @@ -13,11 +14,17 @@ class ApiTests { api = GoingElectricApi.create(apikey) } + @ExperimentalCoroutinesApi @Test fun apiTest() { val charger = api.getChargepointDetail(2105) .execute().body()!! .chargelocations[0] as ChargeLocation print(charger) + + runBlocking { + val result = availabilityDetectors[0].getAvailability(charger) + print(result) + } } }