Add route forecast support for OpenWeatherMap (#145)

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* timklge updates 20250308
Forecast in route (openweathermap)

* Forecast in route (openweathermap)

* Forecast in route (openweathermap)

* Update app/src/main/kotlin/de/timklge/karooheadwind/weatherprovider/openweathermap/OpenWeatherMapWeatherProvider.kt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Enderthor 2025-06-11 19:21:48 +02:00 committed by GitHub
parent 958c576a5e
commit 5b163f6f7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,7 +6,6 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.WeatherDataProvider import de.timklge.karooheadwind.WeatherDataProvider
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.jsonWithUnknownKeys import de.timklge.karooheadwind.jsonWithUnknownKeys
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
import de.timklge.karooheadwind.weatherprovider.WeatherProvider import de.timklge.karooheadwind.weatherprovider.WeatherProvider
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
@ -16,13 +15,17 @@ import io.hammerhead.karooext.models.OnHttpResponse
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.timeout
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -48,6 +51,8 @@ data class Snow(
class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider { class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider {
companion object { companion object {
private const val MAX_API_CALLS = 4
fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int { fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int {
// Mapping OpenWeatherMap to WMO OpenMeteo // Mapping OpenWeatherMap to WMO OpenMeteo
return when (owmCode) { return when (owmCode) {
@ -67,37 +72,84 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
coordinates: List<GpsCoordinates>, coordinates: List<GpsCoordinates>,
settings: HeadwindSettings, settings: HeadwindSettings,
profile: UserProfile? profile: UserProfile?
): WeatherDataResponse { ): WeatherDataResponse = coroutineScope {
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
val responses = mutableListOf<WeatherDataForLocation>() val selectedCoordinates = when {
coordinates.size <= MAX_API_CALLS -> coordinates
else -> {
val openWeatherMapWeatherDataForLocation = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody) val mandatoryCoordinates = coordinates.take(3).toMutableList()
responses.add(openWeatherMapWeatherDataForLocation.toWeatherDataForLocation(null))
// FIXME Route forecast
return WeatherDataResponse( val fourthIndex = if (coordinates.size > 6) {
coordinates.size - 3
} else {
(coordinates.size / 2) + 1
}
mandatoryCoordinates.add(coordinates[fourthIndex.coerceIn(3, coordinates.lastIndex)])
mandatoryCoordinates
}
}
Log.d(KarooHeadwindExtension.TAG, "OpenWeatherMap: searching for ${selectedCoordinates.size} locations from ${coordinates.size} total")
selectedCoordinates.forEachIndexed { index, coord ->
Log.d(KarooHeadwindExtension.TAG, "Point #$index: ${coord.lat}, ${coord.lon}, distance: ${coord.distanceAlongRoute}")
}
val weatherDataForSelectedLocations = selectedCoordinates.map { coordinate ->
async {
val response = makeOpenWeatherMapRequest(karooSystem, coordinate, apiKey)
val responseBody = response.body?.let { String(it) }
?: throw WeatherProviderException(response.statusCode, "Null Response from OpenWeatherMap")
val weatherData = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
coordinate to weatherData
}
}.awaitAll()
val allLocationData = coordinates.map { originalCoord ->
val directMatch = weatherDataForSelectedLocations.find { it.first == originalCoord }
if (directMatch != null) {
directMatch.second.toWeatherDataForLocation(originalCoord.distanceAlongRoute)
} else {
val closestCoord = weatherDataForSelectedLocations.minByOrNull { (coord, _) ->
if (originalCoord.distanceAlongRoute != null && coord.distanceAlongRoute != null) {
(originalCoord.distanceAlongRoute - coord.distanceAlongRoute).absoluteValue
} else {
originalCoord.distanceTo(coord)
}
} ?: throw WeatherProviderException(500, "Error finding nearest coordinate")
closestCoord.second.toWeatherDataForLocation(originalCoord.distanceAlongRoute)
}
}
WeatherDataResponse(
provider = WeatherDataProvider.OPEN_WEATHER_MAP, provider = WeatherDataProvider.OPEN_WEATHER_MAP,
data = responses data = allLocationData
) )
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private suspend fun makeOpenWeatherMapRequest( private suspend fun makeOpenWeatherMapRequest(
service: KarooSystemService, service: KarooSystemService,
coordinates: List<GpsCoordinates>, coordinate: GpsCoordinates,
apiKey: String apiKey: String
): HttpResponseState.Complete { ): HttpResponseState.Complete {
val response = callbackFlow { val response = callbackFlow {
// OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature
val coordinate = coordinates.first()
// URL API 3.0 with onecall endpoint // URL API 3.0 with onecall endpoint
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" + val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" +
"&appid=$apiKey&exclude=minutely,daily,alerts&units=metric" "&appid=$apiKey&exclude=minutely,daily,alerts&units=metric"
Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url") Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url")