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:
parent
dbc0e6b511
commit
973bed718f
@ -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
2
app/.gitignore
vendored
@ -1 +1 @@
|
||||
/build
|
||||
/build!/manifest.json
|
||||
|
||||
@ -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")
|
||||
}
|
||||
@ -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){
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user