Support to OpenWeatherMap 3.0 (#62)

* Added openweathermap support

* Set default location rounding to 3 km

* Back from settings page navigates to weather page first

* Enlarge font in single date weather display (#55)

* Shorter date format

* Enlarge font size in single date weather widget and make clickable

* Fix off by one in route forecast

* Fix indentation lint

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Added openweathermap support

* Update readme (openweathermap)

* Update readme (openweathermap)

* Timgkle fixes

* Timgkle fixes

* Delete app/manifest.json

* Timgkle fixes

* timklge updates 20250308

* timklge updates 20250308
Default provider is Open_METEO

---------

Co-authored-by: Tim Kluge <timklge@gmail.com>
Co-authored-by: timklge <2026103+timklge@users.noreply.github.com>
This commit is contained in:
Enderthor 2025-03-23 21:54:20 +01:00 committed by GitHub
parent dbc0e6b511
commit 973bed718f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 514 additions and 7 deletions

View File

@ -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

2
app/.gitignore vendored
View File

@ -1 +1 @@
/build
/build!/manifest.json

View File

@ -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())
@ -79,3 +85,11 @@ data class HeadwindSettings(
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")
}

View File

@ -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<List<OpenMeteoCurrentWeatherResponse>>(
responseBody
)
weatherDataProvider = responseArray.firstOrNull()?.provider
} else {
val responseObject =
jsonWithUnknownKeys.decodeFromString<OpenMeteoCurrentWeatherResponse>(
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){

View File

@ -17,7 +17,7 @@ import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@OptIn(FlowPreview::class)
suspend fun KarooSystemService.makeOpenMeteoHttpRequest(gpsCoordinates: List<GpsCoordinates>, settings: HeadwindSettings, profile: UserProfile?): HttpResponseState.Complete {
suspend fun KarooSystemService.originalOpenMeteoRequest(gpsCoordinates: List<GpsCoordinates>, 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<Gps
}
}.single()
}
@OptIn(FlowPreview::class)
suspend fun KarooSystemService.makeOpenMeteoHttpRequest(
gpsCoordinates: List<GpsCoordinates>,
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
}

View File

@ -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
)

View File

@ -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<GpsCoordinates>,
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<OpenMeteoCurrentWeatherResponse>(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
}
}
}

View File

@ -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<HourlyForecast>
)
@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<Weather>
)
@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<Weather>
)
@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<GpsCoordinates>,
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<OpenMeteoCurrentWeatherResponse>()
val oneCallResponse = jsonWithUnknownKeys.decodeFromString<OneCallResponse>(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<OneCallResponse>(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<GpsCoordinates>,
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()
}
}

View File

@ -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<GpsCoordinates>,
settings: HeadwindSettings,
profile: UserProfile?
): HttpResponseState.Complete
}

View File

@ -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
)

View File

@ -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
)
}
}
}

View File

@ -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(