diff --git a/README.md b/README.md index dd258f1..77c1649 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,16 @@ After installing this app on your Karoo and opening it once from the main menu, - Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time). Tap on this widget to open the headwind app with a forecast overview. - Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired. -The app will automatically attempt to download weather data for your current approximate location from the [open-meteo.com](https://open-meteo.com) API once your device has acquired a GPS fix. The API service is free for non-commercial use. Your location is rounded to approximately two kilometers to maintain privacy. The data is updated when you ride more than two kilometers from the location where the weather data was downloaded or after one hour at the latest. If the app cannot connect to the weather service, it will retry the download every minute. Downloading weather data should work on Karoo 2 if you have a SIM card inserted or on Karoo 3 via your phone's internet connection if you have the Karoo companion app installed. +This app can use open-meteo service or openweathermap. Use the correct one for your zone, open-meteo is easier to use because you cannot need to introduce an API key. Openweather need to have an API from Openweathermap, it's free for personal use and you don't need to pay for it, but you need to register in https://openweathermap.org/home/sign_in and get the one call API key. +Openweathermap has more local stations and is more accurate than open-meteo for current weather (but it depends of the location).) If you use openweathermap without a correct key, this app will fallback to open-meteo. + +The app will automatically attempt to download weather data for your current approximate location API once your device has acquired a GPS fix. The API service is free for non-commercial use. Your location is rounded to approximately two kilometers to maintain privacy. The data is updated when you ride more than two kilometers from the location where the weather data was downloaded or after one hour at the latest. If the app cannot connect to the weather service, it will retry the download every minute. Downloading weather data should work on Karoo 2 if you have a SIM card inserted or on Karoo 3 via your phone's internet connection if you have the Karoo companion app installed. ## Credits - Icons are from [boxicons.com](https://boxicons.com) ([MIT-licensed](icon_credits.txt)) - Made possible by the generous usage terms of [open-meteo.com](https://open-meteo.com) +- Made possible by the generous usage terms of [openweathermap.org](https://openweathermap.org) - Uses [karoo-ext](https://github.com/hammerheadnav/karoo-ext) (Apache2-licensed) ## Extension Developers: Headwind Data Type diff --git a/app/.gitignore b/app/.gitignore index 42afabf..e09316a 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1 @@ -/build \ No newline at end of file +/build!/manifest.json diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt b/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt index c37f9eb..fd42d85 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/HeadwindSettings.kt @@ -48,17 +48,21 @@ data class HeadwindWidgetSettings( } } +//Moded with openweahtermap.org @Serializable data class HeadwindStats( val lastSuccessfulWeatherRequest: Long? = null, val lastSuccessfulWeatherPosition: GpsCoordinates? = null, val failedWeatherRequest: Long? = null, + val lastSuccessfulWeatherProvider: WeatherDataProvider? = null ){ companion object { val defaultStats = Json.encodeToString(HeadwindStats()) } } + + @Serializable data class HeadwindSettings( val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR, @@ -70,6 +74,8 @@ data class HeadwindSettings( val forecastedMilesPerHour: Int = 12, val lastUpdateRequested: Long? = null, val showDistanceInForecast: Boolean = true, + val weatherProvider: WeatherDataProvider = WeatherDataProvider.OPEN_METEO, + val openWeatherMapApiKey: String = "" ){ companion object { val defaultSettings = Json.encodeToString(HeadwindSettings()) @@ -78,4 +84,12 @@ data class HeadwindSettings( fun getForecastMetersPerHour(isImperial: Boolean): Int { return if (isImperial) forecastedMilesPerHour * 1609 else forecastedKmPerHour * 1000 } +} + +//added openweathermap.org + +@Serializable +enum class WeatherDataProvider(val id: String, val label: String) { + OPEN_METEO("open-meteo", "Open-Meteo"), + OPEN_WEATHER_MAP("open-weather-map", "OpenWeatherMap") } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt index 8e13d31..3556235 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt @@ -212,6 +212,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS val response = karooSystem.makeOpenMeteoHttpRequest(requestedGpsCoordinates, settings, profile) if (response.error != null){ try { + val stats = lastKnownStats.copy(failedWeatherRequest = System.currentTimeMillis()) launch { saveStats(this@KarooHeadwindExtension, stats) } } catch(e: Exception){ @@ -220,9 +221,33 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS error("HTTP request failed: ${response.error}") } else { try { + val responseBody = response.body?.let { String(it) } + var weatherDataProvider: WeatherDataProvider? = null + + try { + if (responseBody != null) { + if (responseBody.trim().startsWith("[")) { + val responseArray = + jsonWithUnknownKeys.decodeFromString>( + responseBody + ) + weatherDataProvider = responseArray.firstOrNull()?.provider + } else { + val responseObject = + jsonWithUnknownKeys.decodeFromString( + responseBody + ) + weatherDataProvider = responseObject.provider + } + } + } catch (e: Exception) { + Log.e(TAG, "Error decoding provider", e) + } + val stats = lastKnownStats.copy( lastSuccessfulWeatherRequest = System.currentTimeMillis(), - lastSuccessfulWeatherPosition = gps + lastSuccessfulWeatherPosition = gps, + lastSuccessfulWeatherProvider = weatherDataProvider ) launch { saveStats(this@KarooHeadwindExtension, stats) } } catch(e: Exception){ diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt index b4c4914..ba44d33 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt @@ -17,7 +17,7 @@ import java.util.Locale import kotlin.time.Duration.Companion.seconds @OptIn(FlowPreview::class) -suspend fun KarooSystemService.makeOpenMeteoHttpRequest(gpsCoordinates: List, settings: HeadwindSettings, profile: UserProfile?): HttpResponseState.Complete { +suspend fun KarooSystemService.originalOpenMeteoRequest(gpsCoordinates: List, settings: HeadwindSettings, profile: UserProfile?): HttpResponseState.Complete { val precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH val temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT @@ -58,3 +58,28 @@ suspend fun KarooSystemService.makeOpenMeteoHttpRequest(gpsCoordinates: List, + settings: HeadwindSettings, + profile: UserProfile? +): HttpResponseState.Complete { + val provider = WeatherProviderFactory.getProvider(settings) + val response = provider.getWeatherData(this, gpsCoordinates, settings, profile) + + if (response.error != null) { + if (provider is OpenWeatherMapProvider) { + WeatherProviderFactory.handleOpenWeatherMapFailure() + } + } else { + + if (provider is OpenWeatherMapProvider) { + WeatherProviderFactory.resetOpenWeatherMapFailures() + } else if (provider is OpenMeteoProvider) { + WeatherProviderFactory.handleOpenMeteoSuccess() + } + } + + return response +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoData.kt b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoData.kt index 74d1000..4f61b8b 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoData.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoData.kt @@ -59,5 +59,6 @@ data class OpenMeteoCurrentWeatherResponse( val timezone: String, val elevation: Double, @SerialName("utc_offset_seconds") val utfOffsetSeconds: Int, - @SerialName("hourly") val forecastData: OpenMeteoForecastData? + @SerialName("hourly") val forecastData: OpenMeteoForecastData?, + val provider: WeatherDataProvider? = null ) \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoProvider.kt b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoProvider.kt new file mode 100644 index 0000000..8d7e509 --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteoProvider.kt @@ -0,0 +1,39 @@ +package de.timklge.karooheadwind + +import android.util.Log +import de.timklge.karooheadwind.datatypes.GpsCoordinates +import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.HttpResponseState +import io.hammerhead.karooext.models.UserProfile + +class OpenMeteoProvider : WeatherProvider { + override suspend fun getWeatherData( + karooSystem: KarooSystemService, + coordinates: List, + settings: HeadwindSettings, + profile: UserProfile? + ): HttpResponseState.Complete { + val response = karooSystem.originalOpenMeteoRequest(coordinates, settings, profile) + + if (response.error != null || response.body == null) { + return response + } + + try { + + val responseBody = response.body?.let { String(it) } + ?: return response + + val weatherData = jsonWithUnknownKeys.decodeFromString(responseBody) + + + val updatedData = weatherData.copy(provider = WeatherDataProvider.OPEN_METEO) + val updatedJson = jsonWithUnknownKeys.encodeToString(updatedData) + + return response.copy(body = updatedJson.toByteArray()) + } catch (e: Exception) { + Log.e(KarooHeadwindExtension.TAG, "Error processing OpenMeteo response", e) + return response + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/OpenWeatherMapProvider.kt b/app/src/main/kotlin/de/timklge/karooheadwind/OpenWeatherMapProvider.kt new file mode 100644 index 0000000..80dddfd --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/OpenWeatherMapProvider.kt @@ -0,0 +1,248 @@ +package de.timklge.karooheadwind + +import android.util.Log +import de.timklge.karooheadwind.datatypes.GpsCoordinates +import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.HttpResponseState +import io.hammerhead.karooext.models.OnHttpResponse +import io.hammerhead.karooext.models.UserProfile +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.single +import kotlinx.coroutines.flow.timeout +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.seconds + +@Serializable +data class OneCallResponse( + val lat: Double, + val lon: Double, + val timezone: String, + @SerialName("timezone_offset") val timezoneOffset: Int, + val current: CurrentWeather, + val hourly: List +) + +@Serializable +data class CurrentWeather( + val dt: Long, + val sunrise: Long, + val sunset: Long, + val temp: Double, + val feels_like: Double, + val pressure: Int, + val humidity: Int, + val clouds: Int, + val visibility: Int, + val wind_speed: Double, + val wind_deg: Int, + val wind_gust: Double? = null, + val rain: Rain? = null, + val snow: Snow? = null, + val weather: List +) + +@Serializable +data class HourlyForecast( + val dt: Long, + val temp: Double, + val feels_like: Double, + val pressure: Int, + val humidity: Int, + val clouds: Int, + val visibility: Int, + val wind_speed: Double, + val wind_deg: Int, + val wind_gust: Double? = null, + val pop: Double, + val rain: Rain? = null, + val snow: Snow? = null, + val weather: List +) + + +@Serializable +data class Weather( + val id: Int, + val main: String, + val description: String, + val icon: String +) + +@Serializable +data class Rain( + @SerialName("1h") val h1: Double = 0.0, + @SerialName("3h") val h3: Double = 0.0 +) + +@Serializable +data class Snow( + @SerialName("1h") val h1: Double = 0.0, + @SerialName("3h") val h3: Double = 0.0 +) + + +class OpenWeatherMapProvider(private val apiKey: String) : WeatherProvider { + override suspend fun getWeatherData( + service: KarooSystemService, + coordinates: List, + settings: HeadwindSettings, + profile: UserProfile? + ): HttpResponseState.Complete { + + val response = makeOpenWeatherMapRequest(service, coordinates, apiKey) + + if (response.error != null || response.body == null) { + return response + } + + try { + val responseBody = response.body?.let { String(it) } + ?: throw Exception("Respuesta nula de OpenWeatherMap") + + + if (coordinates.size > 1) { + val responses = mutableListOf() + + + val oneCallResponse = jsonWithUnknownKeys.decodeFromString(responseBody) + responses.add(convertToOpenMeteoFormat(oneCallResponse)) + + val finalBody = jsonWithUnknownKeys.encodeToString(responses) + return HttpResponseState.Complete( + statusCode = response.statusCode, + headers = response.headers, + body = finalBody.toByteArray(), + error = null + ) + } else { + + val oneCallResponse = jsonWithUnknownKeys.decodeFromString(responseBody) + val convertedResponse = convertToOpenMeteoFormat(oneCallResponse) + + val finalBody = jsonWithUnknownKeys.encodeToString(convertedResponse) + return HttpResponseState.Complete( + statusCode = response.statusCode, + headers = response.headers, + body = finalBody.toByteArray(), + error = null + ) + } + } catch (e: Exception) { + Log.e(KarooHeadwindExtension.TAG, "Error OpenWeatherMap answer processing", e) + return HttpResponseState.Complete( + statusCode = 500, + headers = mapOf(), + body = null, + error = "Error processing data: ${e.message}" + ) + } + } + + private fun convertToOpenMeteoFormat(oneCallResponse: OneCallResponse): OpenMeteoCurrentWeatherResponse { + + val current = OpenMeteoData( + time = oneCallResponse.current.dt, + interval = 3600, + temperature = oneCallResponse.current.temp, + relativeHumidity = oneCallResponse.current.humidity, + precipitation = oneCallResponse.current.rain?.h1 ?: 0.0, + cloudCover = oneCallResponse.current.clouds, + surfacePressure = oneCallResponse.current.pressure.toDouble(), + sealevelPressure = oneCallResponse.current.pressure.toDouble(), + windSpeed = oneCallResponse.current.wind_speed, + windDirection = oneCallResponse.current.wind_deg.toDouble(), + windGusts = oneCallResponse.current.wind_gust ?: oneCallResponse.current.wind_speed, + weatherCode = convertWeatherCodeToOpenMeteo(oneCallResponse.current.weather.firstOrNull()?.id ?: 800) + ) + + + val forecastHours = minOf(12, oneCallResponse.hourly.size) + val hourlyForecasts = oneCallResponse.hourly.take(forecastHours) + + val forecastData = OpenMeteoForecastData( + time = hourlyForecasts.map { it.dt }, + temperature = hourlyForecasts.map { it.temp }, + precipitationProbability = hourlyForecasts.map { (it.pop * 100).toInt() }, + precipitation = hourlyForecasts.map { it.rain?.h1 ?: 0.0 }, + weatherCode = hourlyForecasts.map { convertWeatherCodeToOpenMeteo(it.weather.firstOrNull()?.id ?: 800) }, + windSpeed = hourlyForecasts.map { it.wind_speed }, + windDirection = hourlyForecasts.map { it.wind_deg.toDouble() }, + windGusts = hourlyForecasts.map { it.wind_gust ?: it.wind_speed } + ) + + return OpenMeteoCurrentWeatherResponse( + current = current, + latitude = oneCallResponse.lat, + longitude = oneCallResponse.lon, + timezone = oneCallResponse.timezone, + elevation = 0.0, + utfOffsetSeconds = oneCallResponse.timezoneOffset, + forecastData = forecastData, + provider = WeatherDataProvider.OPEN_WEATHER_MAP + ) + } + + private fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int { + // Mapping OpenWeatherMap a WMO OpenMeteo + return when (owmCode) { + in 200..299 -> 95 // Tormentas + in 300..399 -> 51 // Llovizna + in 500..599 -> 61 // Lluvia + in 600..699 -> 71 // Nieve + 800 -> 0 // Despejado + in 801..804 -> 1 // Nubosidad + else -> 0 + } + } + + @OptIn(FlowPreview::class) + private suspend fun makeOpenWeatherMapRequest( + service: KarooSystemService, + coordinates: List, + apiKey: String + ): HttpResponseState.Complete { + return callbackFlow { + val coordinate = coordinates.first() + // URL API 3.0 con endpoint onecall + val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" + + "&appid=$apiKey&units=metric&exclude=minutely,daily,alerts" + + Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url") + + val listenerId = service.addConsumer( + OnHttpResponse.MakeHttpRequest( + "GET", + url, + waitForConnection = false, + headers = mapOf("User-Agent" to KarooHeadwindExtension.TAG) + ), + onEvent = { event: OnHttpResponse -> + if (event.state is HttpResponseState.Complete) { + Log.d(KarooHeadwindExtension.TAG, "Http response received from OpenWeatherMap") + trySend(event.state as HttpResponseState.Complete) + close() + } + }, + onError = { err -> + Log.e(KarooHeadwindExtension.TAG, "Http error: $err") + close(RuntimeException(err)) + } + ) + + awaitClose { + service.removeConsumer(listenerId) + } + }.timeout(30.seconds).catch { e: Throwable -> + if (e is TimeoutCancellationException) { + emit(HttpResponseState.Complete(500, mapOf(), null, "Timeout")) + } else { + throw e + } + }.single() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProvider.kt b/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProvider.kt new file mode 100644 index 0000000..47d9fb7 --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProvider.kt @@ -0,0 +1,15 @@ +package de.timklge.karooheadwind + +import de.timklge.karooheadwind.datatypes.GpsCoordinates +import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.HttpResponseState +import io.hammerhead.karooext.models.UserProfile + +interface WeatherProvider { + suspend fun getWeatherData( + karooSystem: KarooSystemService, + coordinates: List, + settings: HeadwindSettings, + profile: UserProfile? + ): HttpResponseState.Complete +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProviderFactory.kt b/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProviderFactory.kt new file mode 100644 index 0000000..db4c3fe --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/WeatherProviderFactory.kt @@ -0,0 +1,71 @@ +package de.timklge.karooheadwind + +import android.util.Log +import java.time.LocalDate + +object WeatherProviderFactory { + private var openWeatherMapConsecutiveFailures = 0 + private var openWeatherMapTotalFailures = 0 + private var openMeteoSuccessfulAfterFailures = false + private var fallbackUntilDate: LocalDate? = null + + private const val MAX_FAILURES_BEFORE_TEMP_FALLBACK = 3 + private const val MAX_FAILURES_BEFORE_DAILY_FALLBACK = 20 + + fun getProvider(settings: HeadwindSettings): WeatherProvider { + val currentDate = LocalDate.now() + + + if (fallbackUntilDate != null && !currentDate.isAfter(fallbackUntilDate)) { + Log.d(KarooHeadwindExtension.TAG, "Using diary fallback OpenMeteo until $fallbackUntilDate") + return OpenMeteoProvider() + } + + + if (settings.weatherProvider == WeatherDataProvider.OPEN_WEATHER_MAP && + openWeatherMapConsecutiveFailures >= MAX_FAILURES_BEFORE_TEMP_FALLBACK) { + openWeatherMapConsecutiveFailures = 0 + Log.d(KarooHeadwindExtension.TAG, "Using temporary fallback OpenMeteo") + return OpenMeteoProvider() + } + + + return when (settings.weatherProvider) { + WeatherDataProvider.OPEN_METEO -> OpenMeteoProvider() + WeatherDataProvider.OPEN_WEATHER_MAP -> OpenWeatherMapProvider(settings.openWeatherMapApiKey) + } + } + + fun handleOpenWeatherMapFailure() { + openWeatherMapConsecutiveFailures++ + openWeatherMapTotalFailures++ + + Log.d(KarooHeadwindExtension.TAG, "OpenWeatherMap failed $openWeatherMapConsecutiveFailures times consecutive, $openWeatherMapTotalFailures total times") + + if (openWeatherMapTotalFailures >= MAX_FAILURES_BEFORE_DAILY_FALLBACK && openMeteoSuccessfulAfterFailures) { + fallbackUntilDate = LocalDate.now() + Log.d(KarooHeadwindExtension.TAG, "Activated daily fallback OpenMeteo until $fallbackUntilDate") + } + } + + fun handleOpenMeteoSuccess() { + openMeteoSuccessfulAfterFailures = openWeatherMapTotalFailures > 0 + } + + fun resetOpenWeatherMapFailures() { + openWeatherMapConsecutiveFailures = 0 + } + + fun resetAllFailures() { + openWeatherMapConsecutiveFailures = 0 + openWeatherMapTotalFailures = 0 + openMeteoSuccessfulAfterFailures = false + fallbackUntilDate = null + } +} + + +data class ProviderState( + val provider: WeatherDataProvider, + var consecutiveFailures: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt index d7ab37d..d584635 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/SettingsScreen.kt @@ -44,6 +44,9 @@ import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.models.UserProfile import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import androidx.compose.material3.MaterialTheme +import de.timklge.karooheadwind.WeatherDataProvider + @Composable fun SettingsScreen(onFinish: () -> Unit) { @@ -70,6 +73,9 @@ fun SettingsScreen(onFinish: () -> Unit) { val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null) + var selectedWeatherProvider by remember { mutableStateOf(WeatherDataProvider.OPEN_METEO) } + var openWeatherMapApiKey by remember { mutableStateOf("") } + LaunchedEffect(Unit) { ctx.streamSettings(karooSystem).collect { settings -> selectedWindUnit = settings.windUnit @@ -79,6 +85,8 @@ fun SettingsScreen(onFinish: () -> Unit) { forecastKmPerHour = settings.forecastedKmPerHour.toString() forecastMilesPerHour = settings.forecastedMilesPerHour.toString() showDistanceInForecast = settings.showDistanceInForecast + selectedWeatherProvider = settings.weatherProvider + openWeatherMapApiKey = settings.openWeatherMapApiKey } } @@ -105,7 +113,9 @@ fun SettingsScreen(onFinish: () -> Unit) { roundLocationTo = selectedRoundLocationSetting, forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12, forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20, - showDistanceInForecast = showDistanceInForecast + showDistanceInForecast = showDistanceInForecast, + weatherProvider = selectedWeatherProvider, + openWeatherMapApiKey = openWeatherMapApiKey ) saveSettings(ctx, newSettings) @@ -225,6 +235,58 @@ fun SettingsScreen(onFinish: () -> Unit) { ) } + WeatherProviderSection( + selectedProvider = selectedWeatherProvider, + apiKey = openWeatherMapApiKey, + onProviderChanged = { selectedWeatherProvider = it }, + onApiKeyChanged = { openWeatherMapApiKey = it } + ) + Spacer(modifier = Modifier.padding(30.dp)) + + } +} + +//added +@Composable +fun WeatherProviderSection( + selectedProvider: WeatherDataProvider, + apiKey: String, + onProviderChanged: (WeatherDataProvider) -> Unit, + onApiKeyChanged: (String) -> Unit +) { + + val weatherProviderOptions = WeatherDataProvider.entries.toList() + .map { provider -> DropdownOption(provider.id, provider.label) } + val weatherProviderSelection by remember(selectedProvider) { + mutableStateOf(weatherProviderOptions.find { option -> option.id == selectedProvider.id }!!) + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + + Dropdown( + label = "Weather Provider", + options = weatherProviderOptions, + selected = weatherProviderSelection + ) { selectedOption -> + onProviderChanged(WeatherDataProvider.entries.find { provider -> provider.id == selectedOption.id }!!) + } + + + if (selectedProvider == WeatherDataProvider.OPEN_WEATHER_MAP) { + OutlinedTextField( + value = apiKey, + onValueChange = { onApiKeyChanged(it) }, + label = { Text("OpenWeatherMap API Key") }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) + + Text( + text = "If you want to use OpenWeatherMap, you need to provide an API key. If you don't provide any correct, you'll get OpenMeteo weather data.", + style = MaterialTheme.typography.bodySmall + ) + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt index b7f1b16..1515108 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt @@ -164,9 +164,12 @@ fun WeatherScreen(onFinish: () -> Unit) { ChronoUnit.SECONDS ) + val providerName = stats.lastSuccessfulWeatherProvider?.label ?: "Open-Meteo" + + Text( modifier = Modifier.padding(5.dp), - text = "Last weather data received at $localDate ${localTime}${lastPositionDistanceStr}" + text = "Last weather data received from $providerName at $localDate ${localTime}${lastPositionDistanceStr}" ) } else { Text(