From 4b38c0de2d609e501bf1bfdd58f8ba47b4b473c6 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 17 Dec 2023 20:38:43 +0100 Subject: [PATCH] Realtime data: add time of last change shown in connector details dialog fixes #275 --- .../evmap/adapter/DataBindingAdapters.kt | 5 +- .../api/availability/AvailabilityDetector.kt | 3 + .../availability/EnBwAvailabilityDetector.kt | 48 ++++++++++++++-- .../NewMotionAvailabilityDetector.kt | 53 +++++++++++++++--- .../TeslaGuestAvailabilityDetector.kt | 47 +++++++++------- .../TeslaOwnerAvailabilityDetector.kt | 56 ++++++++++++------- .../evmap/fragment/ConnectorDetailsDialog.kt | 19 ++++++- .../vonforst/evmap/fragment/MapFragment.kt | 4 +- .../net/vonforst/evmap/ui/BindingAdapters.kt | 17 +++++- .../layout/dialog_connector_details_item.xml | 4 +- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 12 files changed, 196 insertions(+), 62 deletions(-) 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 15d1306c..1a2885c1 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -21,6 +21,7 @@ import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding import net.vonforst.evmap.databinding.ItemConnectorButtonBinding import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.ui.CheckableConstraintLayout +import java.time.Instant interface Equatable { override fun equals(other: Any?): Boolean @@ -98,7 +99,9 @@ class ConnectorDetailsAdapter : DataBindingAdapter>, val source: String, val evseIds: Map>? = null, + val labels: Map>? = null, val congestionHistogram: List? = null, + val lastChange: Map>? = null, val extraData: Any? = null // API-specific data ) { fun applyFilters(connectors: Set?, minPower: Int?): ChargeLocationStatus { diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt index 57738baa..61e55cab 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt @@ -1,6 +1,10 @@ package net.vonforst.evmap.api.availability +import com.squareup.moshi.FromJson import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson +import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.utils.distanceBetween @@ -10,6 +14,8 @@ import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query +import java.time.Instant +import java.time.LocalTime private const val coordRange = 0.005 // range of latitude and longitude for loading the map private const val maxDistance = 60 // max distance between reported positions in meters @@ -53,7 +59,8 @@ interface EnBwApi { data class EnBwChargePoint( val evseId: String?, val status: String, - val connectors: List + val connectors: List, + val state: EnBwState? ) @JsonClass(generateAdapter = true) @@ -70,6 +77,11 @@ interface EnBwApi { val upperRightLon: Double ) + @JsonClass(generateAdapter = true) + data class EnBwState( + val updatedAt: Instant? + ) + companion object { fun create(client: OkHttpClient, baseUrl: String? = null): EnBwApi { val clientWithInterceptor = client.newBuilder() @@ -85,7 +97,11 @@ interface EnBwApi { }.build() val retrofit = Retrofit.Builder() .baseUrl(baseUrl ?: "https://enbw-emp.azure-api.net/emobility-public-api/api/v1/") - .addConverterFactory(MoshiConverterFactory.create()) + .addConverterFactory( + MoshiConverterFactory.create( + Moshi.Builder().add(InstantAdapter()).build() + ) + ) .client(clientWithInterceptor) .build() return retrofit.create(EnBwApi::class.java) @@ -93,6 +109,23 @@ interface EnBwApi { } } +internal class InstantAdapter { + @FromJson + fun fromJson(value: Long?): Instant? = value?.let { + Instant.ofEpochMilli(it) + } + + @ToJson + fun toJson(value: Instant?): Long? = value?.toEpochMilli() +} + +data class EnBwStatus( + val conn: EnBwApi.EnBwConnector, + val status: String, + val evseId: String?, + val lastChange: Instant? +) + class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : BaseAvailabilityDetector(client) { val api = EnBwApi.create(client, baseUrl) @@ -157,14 +190,15 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : val connectorStatus = details.flatMap { it.chargePoints }.flatMap { cp -> cp.connectors.map { connector -> - Triple(connector, cp.status, cp.evseId) + EnBwStatus(connector, cp.status, cp.evseId, cp.state?.updatedAt) } } val enbwConnectors = mutableMapOf>() val enbwStatus = mutableMapOf() val enbwEvseId = mutableMapOf() - connectorStatus.forEachIndexed { index, (connector, statusStr, evseId) -> + val enbwLastChange = mutableMapOf() + connectorStatus.forEachIndexed { index, (connector, statusStr, evseId, updatedAt) -> val id = index.toLong() val power = connector.maxPowerInKw ?: 0.0 val type = when (connector.plugTypeName) { @@ -187,6 +221,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : } enbwConnectors[id] = power to type enbwStatus[id] = status + enbwLastChange[id] = updatedAt evseId?.let { enbwEvseId[id] = it } } @@ -197,10 +232,13 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : val evseIds = if (enbwEvseId.size == enbwStatus.size) match.mapValues { entry -> entry.value.map { enbwEvseId[it]!! } } else null + val lastChange = + if (enbwLastChange.size == enbwStatus.size) match.mapValues { entry -> entry.value.map { enbwLastChange[it] } } else null return ChargeLocationStatus( chargepointStatus, "EnBW", - evseIds + evseIds, + lastChange = lastChange ) } diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt index f1d608c4..b31c7104 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt @@ -1,6 +1,10 @@ package net.vonforst.evmap.api.availability +import androidx.car.app.model.DateTimeWithZone +import com.squareup.moshi.FromJson import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.utils.distanceBetween @@ -9,6 +13,11 @@ import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.GET import retrofit2.http.Path +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeParseException import java.util.* private const val coordRange = 0.005 // range of latitude and longitude for loading the map @@ -42,7 +51,12 @@ interface NewMotionApi { ) @JsonClass(generateAdapter = true) - data class NMEvse(val evseId: String?, val status: String, val connectors: List) + data class NMEvse( + val evseId: String?, + val status: String, + val connectors: List, + val updated: ZonedDateTime? + ) @JsonClass(generateAdapter = true) data class NMConnector( @@ -78,7 +92,11 @@ interface NewMotionApi { fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi { val retrofit = Retrofit.Builder() .baseUrl(baseUrl ?: "https://ui-map.shellrecharge.com/api/map/v2/") - .addConverterFactory(MoshiConverterFactory.create()) + .addConverterFactory( + MoshiConverterFactory.create( + Moshi.Builder().add(ZonedDateTimeAdapter()).build() + ) + ) .client(client) .build() return retrofit.create(NewMotionApi::class.java) @@ -86,6 +104,21 @@ interface NewMotionApi { } } +internal class ZonedDateTimeAdapter { + @FromJson + fun fromJson(value: String): ZonedDateTime? = ZonedDateTime.parse(value) + + @ToJson + fun toJson(value: ZonedDateTime): String = value.toString() +} + +data class NmStatus( + val conn: NewMotionApi.NMConnector, + val status: String, + val evseId: String?, + val updated: ZonedDateTime? +) + class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : BaseAvailabilityDetector(client) { val api = NewMotionApi.create(client, baseUrl) @@ -111,9 +144,9 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul throw AvailabilityDetectorException("no candidates found") } - if (nearest.evseCount < location.totalChargepoints) { + markers = if (nearest.evseCount < location.totalChargepoints) { // combine related stations - markers = markers.filter { marker -> + markers.filter { marker -> distanceBetween( marker.coordinates.latitude, marker.coordinates.longitude, @@ -122,7 +155,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul ) < maxDistance } } else { - markers = listOf(nearest) + listOf(nearest) } // load details @@ -135,14 +168,15 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul } val connectorStatus = details.flatMap { it.evses }.flatMap { evse -> evse.connectors.map { connector -> - Triple(connector, evse.status, evse.evseId) + NmStatus(connector, evse.status, evse.evseId, evse.updated) } } val nmConnectors = mutableMapOf>() val nmStatus = mutableMapOf() val nmEvseId = mutableMapOf() - connectorStatus.forEach { (connector, statusStr, evseId) -> + val nmUpdated = mutableMapOf() + connectorStatus.forEach { (connector, statusStr, evseId, updated) -> val id = connector.uid val power = connector.electricalProperties.getPower() val type = when (connector.connectorType.lowercase(Locale.ROOT)) { @@ -168,6 +202,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul nmConnectors.put(id, power to type) nmStatus.put(id, status) evseId?.let { nmEvseId[id] = it } + updated?.let { nmUpdated[id] = it } } val match = matchChargepoints(nmConnectors, location.chargepointsMerged) @@ -177,10 +212,12 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry -> entry.value.map { nmEvseId[it]!! } } else null + val updated = match.mapValues { entry -> entry.value.map { nmUpdated[it]?.toInstant() } } return ChargeLocationStatus( chargepointStatus, "NewMotion", - evseIds + evseIds, + lastChange = updated ) } diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt index 828bc9cd..fe038497 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt @@ -103,53 +103,62 @@ class TeslaGuestAvailabilityDetector( "charger has unknown connectors" ) - var statusSorted = details.chargersAvailable.chargerDetails - .sortedBy { c -> - details.chargers.find { it.id == c.id }?.labelLetter - } - .sortedBy { c -> - details.chargers.find { it.id == c.id }?.labelNumber - } - .map { it.availability } - if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) { + val chargerDetails = details.chargersAvailable.chargerDetails + val chargers = details.chargers.associateBy { it.id } + var detailsSorted = chargerDetails + .sortedBy { chargers[it.id]?.labelLetter } + .sortedBy { chargers[it.id]?.labelNumber } + + + if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) { // apparently some connectors are missing in Tesla data // If we have just one type of charger, we can still match val numMissing = - scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - statusSorted.size + scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) { - statusSorted = - statusSorted + List(numMissing) { ChargerAvailability.UNKNOWN } + detailsSorted = + detailsSorted + List(numMissing) { + TeslaChargingGuestGraphQlApi.ChargerDetail( + ChargerAvailability.UNKNOWN, + "" + ) + } } else { throw AvailabilityDetectorException("Tesla API chargepoints do not match data source") } } - val statusMap = emptyMap>().toMutableMap() + val detailsMap = + mutableMapOf>() var i = 0 for (connector in scV2Connectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + detailsMap[connector] = + detailsSorted.subList(i, i + connector.count) i += connector.count } if (scV2CCSConnectors.isNotEmpty()) { i = 0 for (connector in scV2CCSConnectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + detailsMap[connector] = + detailsSorted.subList(i, i + connector.count) i += connector.count } } for (connector in scV3Connectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + 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 { chargers[it.id]?.label } } + val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates) return ChargeLocationStatus( statusMap, "Tesla", + labels = labelsMap, extraData = pricing ) } diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt index ed6624ad..df2fb27d 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt @@ -2,6 +2,7 @@ package net.vonforst.evmap.api.availability import net.vonforst.evmap.api.availability.tesla.ChargerAvailability import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi +import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi import net.vonforst.evmap.api.availability.tesla.asTeslaCoord import net.vonforst.evmap.model.ChargeLocation @@ -85,47 +86,52 @@ class TeslaOwnerAvailabilityDetector( "charger has unknown connectors" ) - var statusSorted = details.siteDynamic.chargerDetails - .sortedBy { c -> - c.charger.labelLetter - ?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelLetter - } - .sortedBy { c -> - c.charger.labelNumber - ?: details.siteStatic.chargers.find { it.id == c.charger.id }?.labelNumber - } - .map { it.availability } - if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) { + 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 != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) { // apparently some connectors are missing in Tesla data // If we have just one type of charger, we can still match val numMissing = - scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - statusSorted.size + scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) { - statusSorted = - statusSorted + List(numMissing) { ChargerAvailability.UNKNOWN } + detailsSorted = + detailsSorted + List(numMissing) { + TeslaChargingOwnershipGraphQlApi.ChargerDetail( + ChargerAvailability.UNKNOWN, + TeslaChargingOwnershipGraphQlApi.ChargerId( + TeslaChargingOwnershipGraphQlApi.Text(""), + null, + null + ) + ) + } } else { throw AvailabilityDetectorException("Tesla API chargepoints do not match data source") } } - val statusMap = emptyMap>().toMutableMap() + val detailsMap = + emptyMap>().toMutableMap() var i = 0 for (connector in scV2Connectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + detailsMap[connector] = + detailsSorted.subList(i, i + connector.count) i += connector.count } if (scV2CCSConnectors.isNotEmpty()) { i = 0 for (connector in scV2CCSConnectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + detailsMap[connector] = + detailsSorted.subList(i, i + connector.count) i += connector.count } } for (connector in scV3Connectors) { - statusMap[connector] = - statusSorted.subList(i, i + connector.count).map { it.toStatus() } + detailsMap[connector] = + detailsSorted.subList(i, i + connector.count) i += connector.count } @@ -138,9 +144,17 @@ class TeslaOwnerAvailabilityDetector( } } + 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 ) diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt b/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt index 33ed0d8b..319f2009 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/ConnectorDetailsDialog.kt @@ -15,6 +15,7 @@ import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.model.FILTERS_DISABLED import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.MaterialDialogFragment +import java.time.Instant class ConnectorDetailsDialog : MaterialDialogFragment() { private lateinit var binding: DialogConnectorDetailsBinding @@ -23,13 +24,17 @@ class ConnectorDetailsDialog : MaterialDialogFragment() { fun getInstance( chargepoint: Chargepoint, status: List, - evseIds: List? = null + evseIds: List? = null, + labels: List? = null, + lastChange: List? = null ): ConnectorDetailsDialog { val dialog = ConnectorDetailsDialog() dialog.arguments = Bundle().apply { putParcelable("chargepoint", chargepoint) putParcelableArrayList("status", ArrayList(status)) putStringArrayList("evseIds", evseIds?.let { ArrayList(it) }) + putStringArrayList("labels", labels?.let { ArrayList(it) }) + putSerializable("lastChange", lastChange?.let { ArrayList(it) }) } return dialog } @@ -54,10 +59,18 @@ class ConnectorDetailsDialog : MaterialDialogFragment() { val status = BundleCompat.getParcelableArrayList(args, "status", ChargepointStatus::class.java) val evseIds = args.getStringArrayList("evseIds") + val labels = args.getStringArrayList("labels") + val lastChange = args.getSerializable("lastChange") as ArrayList? val items = List(chargepoint.count) { i -> - ConnectorDetailsAdapter.ConnectorDetails(chargepoint, status?.get(i), evseIds?.get(i)) - }.sortedBy { it.evseId } + ConnectorDetailsAdapter.ConnectorDetails( + chargepoint, + status?.get(i), + evseIds?.get(i), + labels?.get(i), + lastChange?.get(i) + ) + }.sortedBy { it.evseId ?: it.label } binding.list.apply { adapter = ConnectorDetailsAdapter().apply { 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 8cc781b3..be74c085 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -821,7 +821,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac val dialog = ConnectorDetailsDialog.getInstance( item.chargepoint, status, - it.evseIds?.get(item.chargepoint) + it.evseIds?.get(item.chargepoint), + it.labels?.get(item.chargepoint), + it.lastChange?.get(item.chargepoint), ) dialog.show(parentFragmentManager, null) } 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 90f80e96..416207d9 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.text.SpannableString +import android.text.format.DateUtils import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.widget.ImageView @@ -28,6 +29,7 @@ import com.google.android.material.slider.RangeSlider import net.vonforst.evmap.* import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.api.iconForPlugType +import java.time.Instant import kotlin.math.ceil import kotlin.math.floor import kotlin.math.roundToInt @@ -310,16 +312,27 @@ fun availabilityText(status: List?): String? { } else available.toString() } -fun availabilityText(status: ChargepointStatus?, context: Context): String? { +fun availabilityText(status: ChargepointStatus?, lastChange: Instant?, context: Context): String? { if (status == null) return null - return when (status) { + val statusText = when (status) { ChargepointStatus.UNKNOWN -> context.getString(R.string.status_unknown) ChargepointStatus.AVAILABLE -> context.getString(R.string.status_available) ChargepointStatus.CHARGING -> context.getString(R.string.status_charging) ChargepointStatus.OCCUPIED -> context.getString(R.string.status_occupied) ChargepointStatus.FAULTED -> context.getString(R.string.status_faulted) } + + return if (lastChange != null) { + val relativeTime = DateUtils.getRelativeTimeSpanString( + lastChange.toEpochMilli(), + Instant.now().toEpochMilli(), + 0 + ).toString() + return context.getString(R.string.status_since, statusText, relativeTime) + } else { + statusText + } } fun flatten(it: Iterable>?): List? { diff --git a/app/src/main/res/layout/dialog_connector_details_item.xml b/app/src/main/res/layout/dialog_connector_details_item.xml index 4e29ee70..460d326d 100644 --- a/app/src/main/res/layout/dialog_connector_details_item.xml +++ b/app/src/main/res/layout/dialog_connector_details_item.xml @@ -62,7 +62,7 @@ android:layout_marginTop="18dp" android:layout_marginEnd="16dp" android:layout_marginBottom="14dp" - android:text="@{item.evseId}" + android:text="@{(item.label != null && item.evseId != null) ? item.label + " · " + item.evseId : (item.label ?? item.evseId)}" app:layout_constraintBottom_toBottomOf="@+id/imageView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/imageView" @@ -76,7 +76,7 @@ android:layout_marginTop="2dp" android:layout_marginEnd="16dp" android:layout_marginBottom="14dp" - android:text="@{BindingAdaptersKt.availabilityText(item.status, context)}" + android:text="@{BindingAdaptersKt.availabilityText(item.status, item.lastChange, context)}" android:textAppearance="@style/TextAppearance.Material3.BodySmall" app:goneUnless="@{item.status != null}" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2808993c..d61343ce 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -373,5 +373,6 @@ Lädt Defekt Status unbekannt + %1$s seit %2$s Ladestationsname \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0c308d2..3c4cd766 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -373,5 +373,6 @@ Charging Out of order Status Unknown + %1$s since %2$s Charger name \ No newline at end of file