Skip to content

RFC: Basic NOBIL implementation #363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: openstreetmap
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ android {
if (goingelectricKey != null) {
resValue("string", "goingelectric_key", goingelectricKey)
}
var nobilKey =
System.getenv("NOBIL_API_KEY") ?: project.findProperty("NOBIL_API_KEY")?.toString()
if (nobilKey == null && project.hasProperty("NOBIL_API_KEY_ENCRYPTED")) {
nobilKey = decode(
project.findProperty("NOBIL_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (nobilKey != null) {
resValue("string", "nobil_key", nobilKey)
}
var openchargemapKey =
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
?.toString()
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.nobil.NobilApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
import net.vonforst.evmap.model.*
Expand Down Expand Up @@ -94,6 +95,13 @@ fun Context.stringProvider() = object : StringProvider {

fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
return when (type) {
"nobil" -> {
NobilApiWrapper(
ctx.getString(
R.string.nobil_key
)
)
}
"openchargemap" -> {
OpenChargeMapApiWrapper(
ctx.getString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"Spanien",
"Tschechien"
) && charger.network != "Tesla Supercharger"
"nobil" -> charger.network != "Tesla"
"openchargemap" -> country in listOf(
"DE",
"AT",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,41 @@ class TeslaGuestAvailabilityDetector(
}
val details = detailsA.await()

if (location.dataSource == "nobil") {
// TODO: Lots of copy & paste here. The main difference for nobil data
// is that V2 chargers don't have duplicated connectors.
var detailsSorted = details.chargerList
.sortedBy { c -> c.labelLetter }
.sortedBy { c -> c.labelNumber }

if (detailsSorted.size != location.chargepoints.size) {
// TODO: Nobil data is outdated and connectors are missing
// TODO: Tesla data could also be missing for connectors
throw AvailabilityDetectorException("charger has unknown connectors")
}

val detailsMap =
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
var i = 0
for (connector in location.chargepointsMerged) {
detailsMap[connector] =
detailsSorted.subList(i, i + connector.count)
i += connector.count
}

val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }

val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)

return ChargeLocationStatus(
statusMap,
"Tesla",
labels = labelsMap,
extraData = pricing
)
}

val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
val scV2CCSConnectors = location.chargepoints.filter {
it.type in listOf(
Expand Down Expand Up @@ -166,6 +201,7 @@ class TeslaGuestAvailabilityDetector(
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"nobil" -> charger.network == "Tesla"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,55 @@ class TeslaOwnerAvailabilityDetector(
)
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")

if (location.dataSource == "nobil") {
// TODO: Lots of copy & paste here. The main difference for nobil data
// is that V2 chargers don't have duplicated connectors.s
val chargerDetails = details.siteDynamic.chargerDetails
val chargers = details.siteStatic.chargers.associateBy { it.id }
var detailsSorted = chargerDetails
.sortedBy { c -> c.charger.labelLetter ?: chargers[c.charger.id]?.labelLetter }
.sortedBy { c -> c.charger.labelNumber ?: chargers[c.charger.id]?.labelNumber }
if (detailsSorted.size != location.chargepoints.size) {
// TODO: Nobil data is outdated and connectors are missing
// TODO: Code below suggests tesla data could also be missing for
// connectors
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
}

val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = cph.data.toMutableList()
Collections.rotate(data, -index)
data
}
}

val detailsMap =
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
var i = 0
for (connector in location.chargepointsMerged) {
detailsMap[connector] =
detailsSorted.subList(i, i + connector.count)
i += connector.count
}

val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }

val labelsMap = detailsMap.mapValues {
it.value.map {
it.charger.label?.value ?: chargers[it.charger.id]?.label?.value
}
}

return ChargeLocationStatus(
statusMap,
"Tesla",
labels = labelsMap,
congestionHistogram = congestionHistogram,
extraData = details.pricing
)
}
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
val scV2CCSConnectors = location.chargepoints.filter {
it.type in listOf(
Expand Down Expand Up @@ -165,6 +213,7 @@ class TeslaOwnerAvailabilityDetector(
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"nobil" -> charger.network == "Tesla"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ data class GEChargeLocation(
address.convert(),
chargepoints.map { it.convert() },
network,
"https://www.goingelectric.de/",
"https:${url}",
"https:${url}edit/",
faultReport?.convert(),
Expand All @@ -88,6 +89,7 @@ data class GEChargeLocation(
locationDescription,
photos?.map { it.convert(apikey) },
chargecards?.map { it.convert() },
null,
openinghours?.convert(),
cost?.convert(),
null,
Expand Down
84 changes: 84 additions & 0 deletions app/src/main/java/net/vonforst/evmap/api/nobil/NobilAdapters.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.vonforst.evmap.api.nobil

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.rawType
import net.vonforst.evmap.model.Coordinate
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

internal class CoordinateAdapter {
@FromJson
fun fromJson(position: String): Coordinate {
val pattern = """\((?<lat>\d+(\.\d+)?), *(?<long>-?\d+(\.\d+)?)\)"""
val match = Regex(pattern).matchEntire(position)
?: throw JsonDataException("Unexpected coordinate format: '$position'")

val groups = match.groups
val latitude : String = groups["lat"]?.value?: "0.0"
val longitude : String = groups["long"]?.value?: "0.0"
return Coordinate(latitude.toDouble(), longitude.toDouble())
}
@ToJson
fun toJson(value: Coordinate): String = "(" + value.lat + ", " + value.lng + ")"
}

internal class LocalDateTimeAdapter {
@FromJson
fun fromJson(value: String?): LocalDateTime? = value?.let {
LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
}

@ToJson
fun toJson(value: LocalDateTime?): String? = value?.toString()
}

internal class NobilConverterFactory(val moshi: Moshi) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
if (type.rawType != NobilDynamicResponseData::class.java) return null

val stringAdapter = moshi.adapter(String::class.java)
val nobilChargerStationAdapter = moshi.adapter(NobilChargerStation::class.java)
return Converter<ResponseBody, NobilDynamicResponseData> { body ->
val reader = JsonReader.of(body.source())
reader.beginObject()

var error: String? = null
var provider: String? = null
var rights: String? = null
var apiver: String? = null
var doc: Sequence<NobilChargerStation>? = null
while (reader.hasNext()) {
when (reader.nextName()) {
"error" -> error = stringAdapter.fromJson(reader)!!
"Provider" -> provider = stringAdapter.fromJson(reader)!!
"Rights" -> rights = stringAdapter.fromJson(reader)!!
"apiver" -> apiver = stringAdapter.fromJson(reader)!!
"chargerstations" -> {
doc = sequence {
reader.beginArray()
while (reader.hasNext()) {
yield(nobilChargerStationAdapter.fromJson(reader)!!)
}
reader.endArray()
reader.close()
}
break
}
}
}
NobilDynamicResponseData(error, provider, rights, apiver, doc)
}
}
}
Loading