implement availability detector with NewMotion API

This commit is contained in:
Johan von Forstner
2020-04-13 13:10:06 +02:00
parent 80edbe66ce
commit c800cca21e
15 changed files with 1566 additions and 84 deletions

View File

@@ -1,6 +1,5 @@
package com.johan.evmap.api
import android.location.Location
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
@@ -37,11 +36,22 @@ suspend fun Call.await(): Response {
}
}
const val earthRadiusKm: Double = 6372.8
fun distanceBetween(
startLatitude: Double, startLongitude: Double,
endLatitude: Double, endLongitude: Double
): Float {
val distance = floatArrayOf(0f)
Location.distanceBetween(startLatitude, startLongitude, endLatitude, endLongitude, distance)
return distance[0]
): Double {
// see https://rosettacode.org/wiki/Haversine_formula#Java
val dLat = Math.toRadians(endLatitude - startLatitude);
val dLon = Math.toRadians(endLongitude - endLongitude);
val originLat = Math.toRadians(startLatitude);
val destinationLat = Math.toRadians(endLatitude);
val a = Math.pow(Math.sin(dLat / 2), 2.toDouble()) + Math.pow(
Math.sin(dLon / 2),
2.toDouble()
) * Math.cos(originLat) * Math.cos(destinationLat);
val c = 2 * Math.asin(Math.sqrt(a));
return earthRadiusKm * c * 1000;
}

View File

@@ -45,6 +45,37 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
}
return filter.getOrNull(0)
}
protected fun matchChargepoints(
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val geTypes = chargepoints.map { it.type }.distinct().toSet()
if (types != geTypes) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
// find connectors of this type
val connsOfType = connectors.filter { it.value.second == type }
// find powers this connector is available as
val powers = connsOfType.map { it.value.first }.distinct().sorted()
// find corresponding powers in GE data
val gePowers =
chargepoints.filter { it.type == type }.map { it.power }.distinct().sorted()
// if the distinct number of powers is the same, try to match.
if (powers.size == gePowers.size) {
gePowers.zip(powers).map { (gePower, power) ->
val chargepoint = chargepoints.find { it.type == type && it.power == gePower }!!
val ids = connsOfType.filter { it.value.first == power }.keys
chargepoint to ids
}
} else {
throw AvailabilityDetectorException("chargepoints do not match")
}
}.toMap()
}
}
data class ChargeLocationStatus(

View File

@@ -44,6 +44,7 @@ interface NewMotionApi {
@JsonClass(generateAdapter = true)
data class NMConnector(
val uid: Long,
val connectorType: String,
val electricalProperties: NMElectricalProperties
)
@@ -72,9 +73,9 @@ interface NewMotionApi {
}
companion object {
fun create(client: OkHttpClient): NewMotionApi {
fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://my.newmotion.com/api/map/v2/")
.baseUrl(baseUrl ?: "https://my.newmotion.com/api/map/v2/")
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
@@ -83,8 +84,9 @@ interface NewMotionApi {
}
}
class NewMotionAvailabilityDetector(client: OkHttpClient) : BaseAvailabilityDetector(client) {
val api = NewMotionApi.create(client)
class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
BaseAvailabilityDetector(client) {
val api = NewMotionApi.create(client, baseUrl)
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
val lat = location.coordinates.lat
@@ -121,8 +123,10 @@ class NewMotionAvailabilityDetector(client: OkHttpClient) : BaseAvailabilityDete
}
}
val chargepointStatus = mutableMapOf<Chargepoint, List<ChargepointStatus>>()
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
connectorStatus.forEach { (connector, statusStr) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType) {
"Type2" -> Chargepoint.TYPE_2
@@ -138,47 +142,18 @@ class NewMotionAvailabilityDetector(client: OkHttpClient) : BaseAvailabilityDete
"Unspecified" -> ChargepointStatus.UNKNOWN
else -> ChargepointStatus.UNKNOWN
}
var chargepoint = getCorrespondingChargepoint(chargepointStatus.keys, type, power)
val statusList: List<ChargepointStatus>
if (chargepoint == null) {
// find corresponding chargepoint from goingelectric to get correct power
val geChargepoint =
getCorrespondingChargepoint(location.chargepoints, type, power)
?: throw AvailabilityDetectorException(
"Chargepoints from NewMotion API and goingelectric do not match."
)
chargepoint = Chargepoint(
type,
geChargepoint.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
nmConnectors.put(id, power to type)
nmStatus.put(id, status)
}
if (chargepointStatus.keys == location.chargepoints.toSet()) {
return ChargeLocationStatus(
chargepointStatus,
"NewMotion"
)
} else {
throw AvailabilityDetectorException(
"Chargepoints from NewMotion API and goingelectric do not match."
)
val match = matchChargepoints(nmConnectors, location.chargepoints)
val chargepointStatus = match.mapValues { entry ->
entry.value.map { nmStatus[it]!! }
}
return ChargeLocationStatus(
chargepointStatus,
"NewMotion"
)
}
}

View File

@@ -10,7 +10,7 @@ import retrofit2.http.GET
import retrofit2.http.Query
interface GoingElectricApi {
@GET("/chargepoints/")
@GET("chargepoints/")
fun getChargepoints(
@Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@@ -19,11 +19,14 @@ interface GoingElectricApi {
@Query("cluster_distance") clusterDistance: Int
): Call<ChargepointList>
@GET("/chargepoints/")
@GET("chargepoints/")
fun getChargepointDetail(@Query("ge_id") id: Long): Call<ChargepointList>
companion object {
fun create(apikey: String): GoingElectricApi {
fun create(
apikey: String,
baseurl: String = "https://api.goingelectric.de"
): GoingElectricApi {
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
// add API key to every request
@@ -42,7 +45,7 @@ interface GoingElectricApi {
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.goingelectric.de")
.baseUrl(baseurl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(client)
.build()