Use interpolated forecast data to update current weather data, refactor weather provider code (#83)
* Use interpolated forecast data to update current weather data, refactor weather provider code * Fix interpolation between locations * Fix copypaste error message for open meteo http * Fix error display * Show interpolated time * Fix position forecasts after refactoring * fix #84: Add lerpAngle * fix #85: Fix weather widget shows wind direction rotated by 180 degrees * Make red background color slightly lighter to improve contrast
This commit is contained in:
parent
90eb0a0821
commit
efaa8efc2c
@ -5,33 +5,41 @@ import android.util.Log
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.mapbox.geojson.LineString
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.turf.TurfConstants
|
||||
import com.mapbox.turf.TurfMeasurement
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.models.DataType
|
||||
import io.hammerhead.karooext.models.OnNavigationState
|
||||
import io.hammerhead.karooext.models.StreamState
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
|
||||
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
|
||||
|
||||
val settingsKey = stringPreferencesKey("settings")
|
||||
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
|
||||
val currentDataKey = stringPreferencesKey("currentForecasts")
|
||||
val currentDataKey = stringPreferencesKey("currentForecastsUnified")
|
||||
val statsKey = stringPreferencesKey("stats")
|
||||
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
|
||||
|
||||
@ -53,12 +61,9 @@ suspend fun saveStats(context: Context, stats: HeadwindStats) {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class WeatherDataResponse(val data: OpenMeteoCurrentWeatherResponse, val requestedPosition: GpsCoordinates)
|
||||
|
||||
suspend fun saveCurrentData(context: Context, forecast: List<WeatherDataResponse>) {
|
||||
suspend fun saveCurrentData(context: Context, response: WeatherDataResponse) {
|
||||
context.dataStore.edit { t ->
|
||||
t[currentDataKey] = Json.encodeToString(forecast)
|
||||
t[currentDataKey] = Json.encodeToString(response)
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,13 +119,9 @@ fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<Headwin
|
||||
data class UpcomingRoute(val distanceAlongRoute: Double, val routePolyline: LineString, val routeLength: Double)
|
||||
|
||||
fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
||||
val distanceToDestinationStream = flow {
|
||||
emit(null)
|
||||
|
||||
streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION)
|
||||
.map { (it as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION) }
|
||||
.collect { emit(it) }
|
||||
}
|
||||
val distanceToDestinationStream = streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION)
|
||||
.map { (it as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
var lastKnownDistanceAlongRoute = 0.0
|
||||
var lastKnownRoutePolyline: LineString? = null
|
||||
@ -130,6 +131,7 @@ fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
||||
.map { navigationState ->
|
||||
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
|
||||
if (routePolyline != null){
|
||||
@ -190,18 +192,191 @@ fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.streamCurrentWeatherData(): Flow<List<WeatherDataResponse>> {
|
||||
fun Context.streamCurrentForecastWeatherData(): Flow<WeatherDataResponse?> {
|
||||
return dataStore.data.map { settingsJson ->
|
||||
try {
|
||||
val data = settingsJson[currentDataKey]
|
||||
data?.let { d -> jsonWithUnknownKeys.decodeFromString<List<WeatherDataResponse>>(d) } ?: emptyList()
|
||||
|
||||
data?.let { d -> jsonWithUnknownKeys.decodeFromString<WeatherDataResponse>(d) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
|
||||
emptyList()
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().map { response ->
|
||||
response.filter { forecast ->
|
||||
forecast.data.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun lerpNullable(
|
||||
start: Double?,
|
||||
end: Double?,
|
||||
factor: Double
|
||||
): Double? {
|
||||
if (start == null && end == null) return null
|
||||
if (start == null) return end
|
||||
if (end == null) return start
|
||||
|
||||
return start + (end - start) * factor
|
||||
}
|
||||
|
||||
/**
|
||||
* Linearly interpolates between two angles in degrees
|
||||
*
|
||||
* @param start Starting angle in degrees [0-360]
|
||||
* @param end Ending angle in degrees [0-360]
|
||||
* @param factor Interpolation factor [0-1]
|
||||
* @return Interpolated angle in degrees [0-360]
|
||||
*/
|
||||
fun lerpAngle(
|
||||
start: Double,
|
||||
end: Double,
|
||||
factor: Double
|
||||
): Double {
|
||||
val normalizedStart = start % 360.0
|
||||
val normalizedEnd = end % 360.0
|
||||
|
||||
var diff = normalizedEnd - normalizedStart
|
||||
if (diff > 180.0) {
|
||||
diff -= 360.0
|
||||
} else if (diff < -180.0) {
|
||||
diff += 360.0
|
||||
}
|
||||
|
||||
var result = normalizedStart + diff * factor
|
||||
|
||||
if (result < 0) {
|
||||
result += 360.0
|
||||
} else if (result >= 360.0) {
|
||||
result -= 360.0
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun lerpWeather(
|
||||
start: WeatherData,
|
||||
end: WeatherData,
|
||||
factor: Double
|
||||
): WeatherData {
|
||||
val closestWeatherData = if (factor < 0.5) start else end
|
||||
|
||||
return WeatherData(
|
||||
time = (start.time + (end.time - start.time) * factor).toLong(),
|
||||
temperature = start.temperature + (end.temperature - start.temperature) * factor,
|
||||
relativeHumidity = lerpNullable(start.relativeHumidity, end.relativeHumidity, factor),
|
||||
precipitation = start.precipitation + (end.precipitation - start.precipitation) * factor,
|
||||
cloudCover = lerpNullable(start.cloudCover, end.cloudCover, factor),
|
||||
surfacePressure = lerpNullable(start.surfacePressure, end.surfacePressure, factor),
|
||||
sealevelPressure = lerpNullable(start.sealevelPressure, end.sealevelPressure, factor),
|
||||
windSpeed = start.windSpeed + (end.windSpeed - start.windSpeed) * factor,
|
||||
windDirection = lerpAngle(start.windDirection, end.windDirection, factor),
|
||||
windGusts = start.windGusts + (end.windGusts - start.windGusts) * factor,
|
||||
weatherCode = closestWeatherData.weatherCode,
|
||||
isForecast = closestWeatherData.isForecast
|
||||
)
|
||||
}
|
||||
|
||||
fun lerpWeatherTime(
|
||||
weatherData: List<WeatherData>?,
|
||||
currentWeatherData: WeatherData
|
||||
): WeatherData {
|
||||
val now = System.currentTimeMillis()
|
||||
val nextWeatherForecastData = weatherData?.find { forecast -> forecast.time * 1000 >= now }
|
||||
val previousWeatherForecastData = weatherData?.findLast { forecast -> forecast.time * 1000 < now }
|
||||
|
||||
val interpolateStartWeatherData = previousWeatherForecastData ?: currentWeatherData
|
||||
val interpolateEndWeatherData = nextWeatherForecastData ?: interpolateStartWeatherData
|
||||
|
||||
val lerpFactor = ((now - (interpolateStartWeatherData.time * 1000)).toDouble() / (interpolateEndWeatherData.time * 1000 - (interpolateStartWeatherData.time * 1000)).absoluteValue).coerceIn(0.0, 1.0)
|
||||
|
||||
return lerpWeather(
|
||||
start = interpolateStartWeatherData,
|
||||
end = interpolateEndWeatherData,
|
||||
factor = lerpFactor
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun Context.streamCurrentWeatherData(karooSystemService: KarooSystemService): Flow<WeatherData?> {
|
||||
val locationFlow = flow {
|
||||
emit(null)
|
||||
emitAll(karooSystemService.getGpsCoordinateFlow(this@streamCurrentWeatherData))
|
||||
}
|
||||
|
||||
return dataStore.data.map { settingsJson ->
|
||||
try {
|
||||
val data = settingsJson[currentDataKey]
|
||||
data?.let { d -> jsonWithUnknownKeys.decodeFromString<WeatherDataResponse>(d) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
|
||||
null
|
||||
}
|
||||
}.combine(locationFlow) {
|
||||
weatherData, location -> weatherData to location
|
||||
}.distinctUntilChanged()
|
||||
.flatMapLatest { (weatherData, location) ->
|
||||
flow {
|
||||
if (!weatherData?.data.isNullOrEmpty()) {
|
||||
while(true){
|
||||
// Get weather for closest position
|
||||
val weatherDataForCurrentPosition = if (location == null || weatherData?.data?.size == 1) weatherData?.data?.first() else {
|
||||
val weatherDatas = weatherData?.data?.sortedBy { data ->
|
||||
TurfMeasurement.distance(
|
||||
Point.fromLngLat(location.lon, location.lat),
|
||||
Point.fromLngLat(data.coords.lon, data.coords.lat),
|
||||
TurfConstants.UNIT_METERS
|
||||
)
|
||||
}!!.take(2)
|
||||
|
||||
val location1 = weatherDatas[0]
|
||||
val location2 = weatherDatas[1]
|
||||
val distanceToLocation1 = TurfMeasurement.distance(
|
||||
Point.fromLngLat(location.lon, location.lat),
|
||||
Point.fromLngLat(location1.coords.lon, location1.coords.lat),
|
||||
TurfConstants.UNIT_METERS
|
||||
)
|
||||
val distanceToLocation2 = TurfMeasurement.distance(
|
||||
Point.fromLngLat(location.lon, location.lat),
|
||||
Point.fromLngLat(location2.coords.lon, location2.coords.lat),
|
||||
TurfConstants.UNIT_METERS
|
||||
)
|
||||
val lerpFactor = (distanceToLocation1 / (distanceToLocation1 + distanceToLocation2)).coerceIn(0.0, 1.0)
|
||||
|
||||
val interpolatedWeatherData = lerpWeather(
|
||||
start = location1.current,
|
||||
end = location2.current,
|
||||
factor = lerpFactor
|
||||
)
|
||||
|
||||
val interpolatedForecasts = location1.forecasts?.mapIndexed { index, forecast ->
|
||||
val forecast2 = location2.forecasts?.get(index) ?: error("Mismatched forecast lengths")
|
||||
val interpolatedForecast = lerpWeather(
|
||||
start = forecast,
|
||||
end = forecast2,
|
||||
factor = lerpFactor
|
||||
)
|
||||
interpolatedForecast
|
||||
}
|
||||
|
||||
WeatherDataForLocation(
|
||||
coords = location,
|
||||
current = interpolatedWeatherData,
|
||||
forecasts = interpolatedForecasts
|
||||
)
|
||||
}
|
||||
|
||||
if (weatherDataForCurrentPosition != null) {
|
||||
emit(lerpWeatherTime(
|
||||
weatherDataForCurrentPosition.forecasts,
|
||||
weatherDataForCurrentPosition.current
|
||||
))
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
|
||||
delay(1.minutes)
|
||||
}
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ package de.timklge.karooheadwind
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -13,10 +13,8 @@ import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
|
||||
sealed class HeadingResponse {
|
||||
data object NoGps: HeadingResponse()
|
||||
data object NoWeatherData: HeadingResponse()
|
||||
@ -24,14 +22,14 @@ sealed class HeadingResponse {
|
||||
}
|
||||
|
||||
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> {
|
||||
val currentWeatherData = context.streamCurrentWeatherData()
|
||||
val currentWeatherData = context.streamCurrentWeatherData(this)
|
||||
|
||||
return getHeadingFlow(context)
|
||||
.combine(currentWeatherData) { bearing, data -> bearing to data }
|
||||
.map { (bearing, data) ->
|
||||
when {
|
||||
bearing is HeadingResponse.Value && data.isNotEmpty() -> {
|
||||
val windBearing = data.first().data.current.windDirection + 180
|
||||
bearing is HeadingResponse.Value && data != null -> {
|
||||
val windBearing = data.windDirection + 180
|
||||
val diff = signedAngleDifference(bearing.diff, windBearing)
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: Heading $bearing vs wind $windBearing => $diff")
|
||||
@ -39,7 +37,7 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingRes
|
||||
HeadingResponse.Value(diff)
|
||||
}
|
||||
bearing is HeadingResponse.NoGps -> HeadingResponse.NoGps
|
||||
bearing is HeadingResponse.NoWeatherData || data.isEmpty() -> HeadingResponse.NoWeatherData
|
||||
bearing is HeadingResponse.NoWeatherData || data == null -> HeadingResponse.NoWeatherData
|
||||
else -> bearing
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package de.timklge.karooheadwind
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.util.fastZip
|
||||
import com.mapbox.geojson.LineString
|
||||
import com.mapbox.turf.TurfConstants
|
||||
import com.mapbox.turf.TurfMeasurement
|
||||
@ -26,6 +25,7 @@ import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherProviderFactory
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.KarooExtension
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
@ -45,11 +45,9 @@ import kotlinx.coroutines.flow.retry
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.time.debounce
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
@ -73,15 +71,15 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
WeatherDataType(karooSystem, applicationContext),
|
||||
WeatherForecastDataType(karooSystem),
|
||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||
RelativeHumidityDataType(applicationContext),
|
||||
CloudCoverDataType(applicationContext),
|
||||
WindGustsDataType(applicationContext),
|
||||
WindSpeedDataType(applicationContext),
|
||||
TemperatureDataType(applicationContext),
|
||||
RelativeHumidityDataType(karooSystem, applicationContext),
|
||||
CloudCoverDataType(karooSystem, applicationContext),
|
||||
WindGustsDataType(karooSystem, applicationContext),
|
||||
WindSpeedDataType(karooSystem, applicationContext),
|
||||
TemperatureDataType(karooSystem, applicationContext),
|
||||
WindDirectionDataType(karooSystem, applicationContext),
|
||||
PrecipitationDataType(applicationContext),
|
||||
SurfacePressureDataType(applicationContext),
|
||||
SealevelPressureDataType(applicationContext),
|
||||
PrecipitationDataType(karooSystem, applicationContext),
|
||||
SurfacePressureDataType(karooSystem, applicationContext),
|
||||
SealevelPressureDataType(karooSystem, applicationContext),
|
||||
UserWindSpeedDataType(karooSystem, applicationContext),
|
||||
TemperatureForecastDataType(karooSystem),
|
||||
PrecipitationForecastDataType(karooSystem),
|
||||
@ -121,10 +119,9 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
}
|
||||
.debounce(Duration.ofSeconds(5))
|
||||
|
||||
var requestedGpsCoordinates: List<GpsCoordinates> = mutableListOf()
|
||||
var requestedGpsCoordinates: List<GpsCoordinates> = emptyList()
|
||||
|
||||
val settingsStream = streamSettings(karooSystem)
|
||||
.filter { it.welcomeDialogAccepted }
|
||||
val settingsStream = streamSettings(karooSystem).filter { it.welcomeDialogAccepted }
|
||||
|
||||
data class StreamData(val settings: HeadwindSettings, val gps: GpsCoordinates?, val profile: UserProfile?, val upcomingRoute: UpcomingRoute?)
|
||||
data class StreamDataIdentity(val settings: HeadwindSettings, val gpsLat: Double?, val gpsLon: Double?, val profile: UserProfile?, val routePolyline: LineString?)
|
||||
@ -211,50 +208,29 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
requestedGpsCoordinates = mutableListOf(gps)
|
||||
}
|
||||
|
||||
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){
|
||||
Log.e(TAG, "Failed to write stats", e)
|
||||
}
|
||||
error("HTTP request failed: ${response.error}")
|
||||
} else {
|
||||
try {
|
||||
val responseBody = response.body?.let { String(it) }
|
||||
var weatherDataProvider: WeatherDataProvider? = null
|
||||
|
||||
val response = try {
|
||||
WeatherProviderFactory.makeWeatherRequest(karooSystem, requestedGpsCoordinates, settings, profile)
|
||||
} catch(e: Throwable){
|
||||
val stats = lastKnownStats.copy(failedWeatherRequest = System.currentTimeMillis())
|
||||
launch {
|
||||
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)
|
||||
saveStats(this@KarooHeadwindExtension, stats)
|
||||
} catch(e: Exception){
|
||||
Log.e(TAG, "Failed to write stats", e)
|
||||
}
|
||||
|
||||
val stats = lastKnownStats.copy(
|
||||
lastSuccessfulWeatherRequest = System.currentTimeMillis(),
|
||||
lastSuccessfulWeatherPosition = gps,
|
||||
lastSuccessfulWeatherProvider = weatherDataProvider
|
||||
)
|
||||
launch { saveStats(this@KarooHeadwindExtension, stats) }
|
||||
} catch(e: Exception){
|
||||
Log.e(TAG, "Failed to write stats", e)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
val stats = lastKnownStats.copy(
|
||||
lastSuccessfulWeatherRequest = System.currentTimeMillis(),
|
||||
lastSuccessfulWeatherPosition = gps,
|
||||
lastSuccessfulWeatherProvider = response.provider
|
||||
)
|
||||
launch { saveStats(this@KarooHeadwindExtension, stats) }
|
||||
} catch(e: Exception){
|
||||
Log.e(TAG, "Failed to write stats", e)
|
||||
}
|
||||
|
||||
response
|
||||
@ -263,30 +239,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
delay(1.minutes); true
|
||||
}.collect { response ->
|
||||
try {
|
||||
val inputStream = java.io.ByteArrayInputStream(response.body ?: ByteArray(0))
|
||||
val lowercaseHeaders = response.headers.map { (k: String, v: String) -> k.lowercase() to v.lowercase() }.toMap()
|
||||
val isGzippedResponse = lowercaseHeaders["content-encoding"]?.contains("gzip") == true
|
||||
val responseString = if(isGzippedResponse){
|
||||
val gzipStream = withContext(Dispatchers.IO) { GZIPInputStream(inputStream) }
|
||||
gzipStream.use { stream -> String(stream.readBytes()) }
|
||||
} else {
|
||||
inputStream.use { stream -> String(stream.readBytes()) }
|
||||
}
|
||||
if (requestedGpsCoordinates.size == 1){
|
||||
val weatherData = jsonWithUnknownKeys.decodeFromString<OpenMeteoCurrentWeatherResponse>(responseString)
|
||||
val data = WeatherDataResponse(weatherData, requestedGpsCoordinates.single())
|
||||
|
||||
saveCurrentData(applicationContext, listOf(data))
|
||||
|
||||
Log.d(TAG, "Got updated weather info: $data")
|
||||
} else {
|
||||
val weatherData = jsonWithUnknownKeys.decodeFromString<List<OpenMeteoCurrentWeatherResponse>>(responseString)
|
||||
val data = weatherData.fastZip(requestedGpsCoordinates) { weather, gps -> WeatherDataResponse(weather, gps) }
|
||||
|
||||
saveCurrentData(applicationContext, data)
|
||||
|
||||
Log.d(TAG, "Got updated weather info: $data")
|
||||
}
|
||||
saveCurrentData(applicationContext, response)
|
||||
Log.d(TAG, "Got updated weather info: $response")
|
||||
|
||||
saveWidgetSettings(applicationContext, HeadwindWidgetSettings(currentForecastHourOffset = 0))
|
||||
} catch(e: Exception){
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
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 java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
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
|
||||
|
||||
return callbackFlow {
|
||||
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12
|
||||
val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) }
|
||||
val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) }
|
||||
val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}¤t=surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=${settings.windUnit.id}&precipitation_unit=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}"
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
||||
|
||||
val listenerId = addConsumer(
|
||||
OnHttpResponse.MakeHttpRequest(
|
||||
"GET",
|
||||
url,
|
||||
waitForConnection = false,
|
||||
headers = mapOf("User-Agent" to KarooHeadwindExtension.TAG, "Accept-Encoding" to "gzip"),
|
||||
),
|
||||
onEvent = { event: OnHttpResponse ->
|
||||
if (event.state is HttpResponseState.Complete){
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http response received")
|
||||
trySend(event.state as HttpResponseState.Complete)
|
||||
close()
|
||||
}
|
||||
},
|
||||
onError = { err ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http error: $err")
|
||||
close(RuntimeException(err))
|
||||
})
|
||||
awaitClose {
|
||||
removeConsumer(listenerId)
|
||||
}
|
||||
}.timeout(30.seconds).catch { e: Throwable ->
|
||||
if (e is TimeoutCancellationException){
|
||||
emit(HttpResponseState.Complete(500, mapOf(), null, "Timeout"))
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}.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
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
package de.timklge.karooheadwind
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoData(
|
||||
val time: Long, val interval: Int,
|
||||
@SerialName("temperature_2m") val temperature: Double,
|
||||
@SerialName("relative_humidity_2m") val relativeHumidity: Int,
|
||||
@SerialName("precipitation") val precipitation: Double,
|
||||
@SerialName("cloud_cover") val cloudCover: Int,
|
||||
@SerialName("surface_pressure") val surfacePressure: Double,
|
||||
@SerialName("pressure_msl") val sealevelPressure: Double? = null,
|
||||
@SerialName("wind_speed_10m") val windSpeed: Double,
|
||||
@SerialName("wind_direction_10m") val windDirection: Double,
|
||||
@SerialName("wind_gusts_10m") val windGusts: Double,
|
||||
@SerialName("weather_code") val weatherCode: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoForecastData(
|
||||
@SerialName("time") val time: List<Long>,
|
||||
@SerialName("temperature_2m") val temperature: List<Double>,
|
||||
@SerialName("precipitation_probability") val precipitationProbability: List<Int>,
|
||||
@SerialName("precipitation") val precipitation: List<Double>,
|
||||
@SerialName("weather_code") val weatherCode: List<Int>,
|
||||
@SerialName("wind_speed_10m") val windSpeed: List<Double>,
|
||||
@SerialName("wind_direction_10m") val windDirection: List<Double>,
|
||||
@SerialName("wind_gusts_10m") val windGusts: List<Double>,
|
||||
)
|
||||
|
||||
enum class WeatherInterpretation {
|
||||
CLEAR, CLOUDY, RAINY, SNOWY, DRIZZLE, THUNDERSTORM, UNKNOWN;
|
||||
|
||||
companion object {
|
||||
// WMO weather interpretation codes (WW)
|
||||
fun fromWeatherCode(code: Int): WeatherInterpretation {
|
||||
return when(code){
|
||||
0 -> CLEAR
|
||||
1, 2, 3 -> CLOUDY
|
||||
45, 48, 61, 63, 65, 66, 67, 80, 81, 82 -> RAINY
|
||||
71, 73, 75, 77, 85, 86 -> SNOWY
|
||||
51, 53, 55, 56, 57 -> DRIZZLE
|
||||
95, 96, 99 -> THUNDERSTORM
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun getKnownWeatherCodes(): Set<Int> = setOf(0, 1, 2, 3, 45, 48, 61, 63, 65, 66, 67, 80, 81, 82, 71, 73, 75, 77, 85, 86, 51, 53, 55, 56, 57, 95, 96, 99)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoCurrentWeatherResponse(
|
||||
val current: OpenMeteoData,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val timezone: String,
|
||||
val elevation: Double,
|
||||
@SerialName("utc_offset_seconds") val utfOffsetSeconds: Int,
|
||||
@SerialName("hourly") val forecastData: OpenMeteoForecastData?,
|
||||
val provider: WeatherDataProvider? = null
|
||||
)
|
||||
@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,248 +0,0 @@
|
||||
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("Null response from 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 to WMO OpenMeteo
|
||||
return when (owmCode) {
|
||||
in 200..299 -> 95 // Thunderstorm
|
||||
in 300..399 -> 51 // Drizzle
|
||||
in 500..599 -> 61 // Rain
|
||||
in 600..699 -> 71 // Snow
|
||||
800 -> 0 // Clear
|
||||
in 801..804 -> 1 // Cloudy
|
||||
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 with onecall endpoint
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
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
|
||||
)
|
||||
@ -3,8 +3,9 @@ package de.timklge.karooheadwind.datatypes
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.Emitter
|
||||
import io.hammerhead.karooext.models.DataPoint
|
||||
@ -16,22 +17,24 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class BaseDataType(
|
||||
private val karooSystemService: KarooSystemService,
|
||||
private val applicationContext: Context,
|
||||
dataTypeId: String
|
||||
) : DataTypeImpl("karoo-headwind", dataTypeId) {
|
||||
abstract fun getValue(data: OpenMeteoCurrentWeatherResponse): Double
|
||||
abstract fun getValue(data: WeatherData): Double?
|
||||
|
||||
override fun startStream(emitter: Emitter<StreamState>) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
|
||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||
val currentWeatherData = applicationContext.streamCurrentWeatherData()
|
||||
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystemService)
|
||||
|
||||
currentWeatherData
|
||||
.filterNotNull()
|
||||
.collect { data ->
|
||||
val value = data.firstOrNull()?.data?.let { w -> getValue(w) }
|
||||
val value = getValue(data)
|
||||
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
|
||||
if (value != null){
|
||||
|
||||
if (value != null) {
|
||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
|
||||
} else {
|
||||
emitter.onNext(StreamState.NotAvailable)
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class CloudCoverDataType(context: Context) : BaseDataType(context, "cloudCover"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.cloudCover.toDouble()
|
||||
class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){
|
||||
override fun getValue(data: WeatherData): Double? {
|
||||
return data.cloudCover
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import androidx.glance.action.ActionParameters
|
||||
import androidx.glance.appwidget.action.ActionCallback
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.saveWidgetSettings
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||
import de.timklge.karooheadwind.streamWidgetSettings
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@ -21,13 +21,13 @@ class CycleHoursAction : ActionCallback {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Cycling hours")
|
||||
|
||||
val currentSettings = context.streamWidgetSettings().first()
|
||||
val data = context.streamCurrentWeatherData().firstOrNull()
|
||||
val forecastData = context.streamCurrentForecastWeatherData().firstOrNull()
|
||||
|
||||
var hourOffset = currentSettings.currentForecastHourOffset + 3
|
||||
val requestedPositions = data?.size
|
||||
val requestedHours = data?.firstOrNull()?.data?.forecastData?.weatherCode?.size
|
||||
val requestedPositions = forecastData?.data?.size
|
||||
val requestedHours = forecastData?.data?.firstOrNull()?.forecasts?.size
|
||||
|
||||
if (data == null || requestedHours == null || requestedPositions == null || hourOffset >= requestedHours || (requestedPositions > 1 && hourOffset >= requestedPositions)) {
|
||||
if (forecastData == null || requestedHours == null || requestedPositions == null || hourOffset >= requestedHours || (requestedPositions in 2..hourOffset)) {
|
||||
hourOffset = 0
|
||||
}
|
||||
|
||||
|
||||
@ -25,16 +25,16 @@ import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.OpenMeteoData
|
||||
import de.timklge.karooheadwind.OpenMeteoForecastData
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.UpcomingRoute
|
||||
import de.timklge.karooheadwind.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import de.timklge.karooheadwind.WeatherDataProvider
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
@ -84,7 +84,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
data class StreamData(val data: List<WeatherDataResponse>?, val settings: SettingsAndProfile,
|
||||
data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile,
|
||||
val widgetSettings: HeadwindWidgetSettings? = null,
|
||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
||||
|
||||
@ -97,57 +97,63 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
while (true) {
|
||||
val data = (0..<10).map { index ->
|
||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
|
||||
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
|
||||
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
|
||||
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWeatherCodes =
|
||||
(0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
|
||||
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
|
||||
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val weatherData = OpenMeteoCurrentWeatherResponse(
|
||||
OpenMeteoData(
|
||||
Instant.now().epochSecond,
|
||||
0,
|
||||
20.0,
|
||||
50,
|
||||
3.0,
|
||||
0,
|
||||
1013.25,
|
||||
980.0,
|
||||
15.0,
|
||||
30.0,
|
||||
30.0,
|
||||
WeatherInterpretation.getKnownWeatherCodes().random()
|
||||
),
|
||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||
|
||||
OpenMeteoForecastData(
|
||||
forecastTimes,
|
||||
forecastTemperatures,
|
||||
forecastPrecipitationPropability,
|
||||
forecastPrecipitation,
|
||||
forecastWeatherCodes,
|
||||
forecastWindSpeed,
|
||||
forecastWindDirection,
|
||||
forecastWindGusts
|
||||
val weatherData = (0..<12).map {
|
||||
val forecastTime = timeAtFullHour + it * 60 * 60
|
||||
val forecastTemperature = 20.0 + (-20..20).random()
|
||||
val forecastPrecipitation = 0.0 + (0..10).random()
|
||||
val forecastPrecipitationProbability = (0..100).random()
|
||||
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
|
||||
val forecastWindSpeed = 0.0 + (0..10).random()
|
||||
val forecastWindDirection = 0.0 + (0..360).random()
|
||||
val forecastWindGusts = 0.0 + (0..10).random()
|
||||
WeatherData(
|
||||
time = forecastTime,
|
||||
temperature = forecastTemperature,
|
||||
relativeHumidity = 20.0,
|
||||
precipitation = forecastPrecipitation,
|
||||
cloudCover = 3.0,
|
||||
sealevelPressure = 1013.25,
|
||||
surfacePressure = 1013.25,
|
||||
precipitationProbability = forecastPrecipitationProbability.toDouble(),
|
||||
windSpeed = forecastWindSpeed,
|
||||
windDirection = forecastWindDirection,
|
||||
windGusts = forecastWindGusts,
|
||||
weatherCode = forecastWeatherCode,
|
||||
isForecast = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val distancePerHour =
|
||||
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
||||
?.toDouble() ?: 0.0
|
||||
val gpsCoords =
|
||||
GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour)
|
||||
|
||||
WeatherDataResponse(weatherData, gpsCoords)
|
||||
WeatherDataForLocation(
|
||||
current = WeatherData(
|
||||
time = timeAtFullHour,
|
||||
temperature = 20.0,
|
||||
relativeHumidity = 20.0,
|
||||
precipitation = 0.0,
|
||||
cloudCover = 3.0,
|
||||
sealevelPressure = 1013.25,
|
||||
surfacePressure = 1013.25,
|
||||
windSpeed = 5.0,
|
||||
windDirection = 180.0,
|
||||
windGusts = 10.0,
|
||||
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
||||
isForecast = false
|
||||
),
|
||||
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
||||
timezone = "UTC",
|
||||
elevation = null,
|
||||
forecasts = weatherData
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
emit(
|
||||
StreamData(
|
||||
data,
|
||||
WeatherDataResponse(provider = WeatherDataProvider.OPEN_METEO, data = data),
|
||||
SettingsAndProfile(
|
||||
HeadwindSettings(),
|
||||
settingsAndProfile?.isImperial == true,
|
||||
@ -182,7 +188,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
previewFlow(settingsAndProfileStream)
|
||||
} else {
|
||||
combine(
|
||||
context.streamCurrentWeatherData(),
|
||||
context.streamCurrentForecastWeatherData(),
|
||||
settingsAndProfileStream,
|
||||
context.streamWidgetSettings(),
|
||||
karooSystem.getHeadingFlow(context),
|
||||
@ -204,7 +210,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, headingResponse, upcomingRoute) ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||
|
||||
if (allData.isNullOrEmpty()){
|
||||
if (allData?.data.isNullOrEmpty()){
|
||||
emitter.updateView(
|
||||
getErrorWidget(
|
||||
glance,
|
||||
@ -223,42 +229,29 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
|
||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
||||
val positionOffset = if (allData.size == 1) 0 else hourOffset
|
||||
val positionOffset = if (allData?.data?.size == 1) 0 else hourOffset
|
||||
|
||||
var previousDate: String? = let {
|
||||
val unixTime =
|
||||
allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(
|
||||
hourOffset
|
||||
)
|
||||
val unixTime = allData?.data?.getOrNull(positionOffset)?.forecasts?.getOrNull(hourOffset)?.time
|
||||
val formattedDate = unixTime?.let {
|
||||
getShortDateFormatter().format(
|
||||
Instant.ofEpochSecond(unixTime)
|
||||
)
|
||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
}
|
||||
|
||||
formattedDate
|
||||
}
|
||||
|
||||
for (baseIndex in hourOffset..hourOffset + 2) {
|
||||
val positionIndex = if (allData.size == 1) 0 else baseIndex
|
||||
val positionIndex = if (allData?.data?.size == 1) 0 else baseIndex
|
||||
|
||||
if (allData.getOrNull(positionIndex) == null) break
|
||||
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size
|
||||
?: 0)
|
||||
) {
|
||||
break
|
||||
if (allData?.data?.getOrNull(positionIndex) == null) break
|
||||
if (baseIndex >= (allData.data.getOrNull(positionOffset)?.forecasts?.size ?: 0)) break
|
||||
|
||||
val data = allData.data.getOrNull(positionIndex)
|
||||
val distanceAlongRoute = allData.data.getOrNull(positionIndex)?.coords?.distanceAlongRoute
|
||||
val position = allData.data.getOrNull(positionIndex)?.coords?.let {
|
||||
"${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}"
|
||||
}
|
||||
|
||||
val data = allData.getOrNull(positionIndex)?.data
|
||||
val distanceAlongRoute =
|
||||
allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||
val position =
|
||||
allData.getOrNull(positionIndex)?.requestedPosition?.let {
|
||||
"${
|
||||
(it.distanceAlongRoute?.div(1000.0))?.toInt()
|
||||
} at ${it.lat}, ${it.lon}"
|
||||
}
|
||||
|
||||
if (baseIndex > hourOffset) {
|
||||
Spacer(
|
||||
modifier = GlanceModifier.fillMaxHeight().background(
|
||||
@ -280,8 +273,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
val isCurrent = baseIndex == 0 && positionIndex == 0
|
||||
|
||||
if (isCurrent && data?.current != null) {
|
||||
val interpretation =
|
||||
WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val unixTime = data.current.time
|
||||
val formattedTime =
|
||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
@ -307,32 +299,22 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
|
||||
previousDate = formattedDate
|
||||
} else {
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(
|
||||
data?.forecastData?.weatherCode?.get(baseIndex) ?: 0
|
||||
)
|
||||
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
||||
val formattedTime =
|
||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate =
|
||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val weatherData = data?.forecasts?.getOrNull(baseIndex)
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||
val unixTime = data?.forecasts?.getOrNull(baseIndex)?.time ?: 0
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
RenderWidget(
|
||||
arrowBitmap = baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data?.forecastData?.windDirection?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
windGusts = data?.forecastData?.windGusts?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
precipitation = data?.forecastData?.precipitation?.get(baseIndex)
|
||||
?: 0.0,
|
||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(
|
||||
baseIndex
|
||||
) ?: 0,
|
||||
temperature = data?.forecastData?.temperature?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
||||
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
||||
precipitation = weatherData?.precipitation ?: 0.0,
|
||||
precipitationProbability = weatherData?.precipitationProbability?.toInt(),
|
||||
temperature = weatherData?.temperature?.roundToInt() ?: 0,
|
||||
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = if (hasNewDate) formattedDate else null,
|
||||
|
||||
@ -16,17 +16,15 @@ import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.layout.wrapContentWidth
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
@ -42,8 +42,8 @@ class HeadwindDirectionDataType(
|
||||
|
||||
private fun streamValues(): Flow<Double> = flow {
|
||||
karooSystem.getRelativeHeadingFlow(applicationContext)
|
||||
.combine(applicationContext.streamCurrentWeatherData()) { headingResponse, data ->
|
||||
StreamData(headingResponse, data.firstOrNull()?.data?.current?.windDirection, data.firstOrNull()?.data?.current?.windSpeed)
|
||||
.combine(applicationContext.streamCurrentWeatherData(karooSystem)) { headingResponse, data ->
|
||||
StreamData(headingResponse, data?.windDirection, data?.windSpeed)
|
||||
}
|
||||
.combine(applicationContext.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
|
||||
.collect { streamData ->
|
||||
|
||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
@ -16,7 +16,6 @@ import io.hammerhead.karooext.models.StreamState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.cos
|
||||
|
||||
@ -24,18 +23,17 @@ class HeadwindSpeedDataType(
|
||||
private val karooSystem: KarooSystemService,
|
||||
private val context: Context) : DataTypeImpl("karoo-headwind", "headwindSpeed"){
|
||||
|
||||
data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings)
|
||||
data class StreamData(val headingResponse: HeadingResponse, val weatherData: WeatherData?, val settings: HeadwindSettings)
|
||||
|
||||
override fun startStream(emitter: Emitter<StreamState>) {
|
||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||
karooSystem.getRelativeHeadingFlow(context)
|
||||
.combine(context.streamCurrentWeatherData()) { value, data -> value to data }
|
||||
.combine(context.streamCurrentWeatherData(karooSystem)) { value, data -> value to data }
|
||||
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
|
||||
StreamData(value, data.firstOrNull()?.data, settings)
|
||||
StreamData(value, data, settings)
|
||||
}
|
||||
.filter { it.weatherResponse != null }
|
||||
.collect { streamData ->
|
||||
val windSpeed = streamData.weatherResponse?.current?.windSpeed ?: 0.0
|
||||
val windSpeed = streamData.weatherData?.windSpeed ?: 0.0
|
||||
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class PrecipitationDataType(context: Context) : BaseDataType(context, "precipitation"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.precipitation
|
||||
class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){
|
||||
override fun getValue(data: WeatherData): Double {
|
||||
return data.precipitation
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class RelativeHumidityDataType(context: Context) : BaseDataType(context, "relativeHumidity"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.relativeHumidity.toDouble()
|
||||
class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){
|
||||
override fun getValue(data: WeatherData): Double? {
|
||||
return data.relativeHumidity
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class SealevelPressureDataType(context: Context) : BaseDataType(context, "sealevelPressure"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.sealevelPressure ?: 0.0
|
||||
class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){
|
||||
override fun getValue(data: WeatherData): Double? {
|
||||
return data.sealevelPressure
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class SurfacePressureDataType(context: Context) : BaseDataType(context, "surfacePressure"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.surfacePressure
|
||||
class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){
|
||||
override fun getValue(data: WeatherData): Double? {
|
||||
return data.surfacePressure
|
||||
}
|
||||
}
|
||||
@ -119,11 +119,11 @@ class TailwindAndRideSpeedDataType(
|
||||
val flow = if (config.preview) {
|
||||
previewFlow(karooSystem.streamUserProfile())
|
||||
} else {
|
||||
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), streamSpeedInMs()) { headingResponse, weatherData, settings, userProfile, rideSpeedInMs ->
|
||||
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(karooSystem), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), streamSpeedInMs()) { headingResponse, weatherData, settings, userProfile, rideSpeedInMs ->
|
||||
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
val absoluteWindDirection = weatherData.firstOrNull()?.data?.current?.windDirection
|
||||
val windSpeed = weatherData.firstOrNull()?.data?.current?.windSpeed
|
||||
val gustSpeed = weatherData.firstOrNull()?.data?.current?.windGusts
|
||||
val absoluteWindDirection = weatherData?.windDirection
|
||||
val windSpeed = weatherData?.windSpeed
|
||||
val gustSpeed = weatherData?.windGusts
|
||||
val rideSpeed = if (isImperial){
|
||||
rideSpeedInMs * 2.23694
|
||||
} else {
|
||||
@ -141,7 +141,7 @@ class TailwindAndRideSpeedDataType(
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
||||
|
||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||
if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){
|
||||
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
||||
var headingResponse = streamData.headingResponse
|
||||
|
||||
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
||||
|
||||
@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
@ -34,7 +33,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
@ -99,11 +97,11 @@ class TailwindDataType(
|
||||
val flow = if (config.preview) {
|
||||
previewFlow(karooSystem.streamUserProfile())
|
||||
} else {
|
||||
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), streamSpeedInMs()) { headingResponse, weatherData, settings, userProfile, rideSpeedInMs ->
|
||||
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(karooSystem), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), streamSpeedInMs()) { headingResponse, weatherData, settings, userProfile, rideSpeedInMs ->
|
||||
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
val absoluteWindDirection = weatherData.firstOrNull()?.data?.current?.windDirection
|
||||
val windSpeed = weatherData.firstOrNull()?.data?.current?.windSpeed
|
||||
val gustSpeed = weatherData.firstOrNull()?.data?.current?.windGusts
|
||||
val absoluteWindDirection = weatherData?.windDirection
|
||||
val windSpeed = weatherData?.windSpeed
|
||||
val gustSpeed = weatherData?.windGusts
|
||||
val rideSpeed = if (isImperial){
|
||||
rideSpeedInMs * 2.23694
|
||||
} else {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class TemperatureDataType(context: Context) : BaseDataType(context, "temperature"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.temperature
|
||||
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
|
||||
override fun getValue(data: WeatherData): Double {
|
||||
return data.temperature
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
@ -28,18 +28,18 @@ class UserWindSpeedDataType(
|
||||
private val context: Context
|
||||
) : DataTypeImpl("karoo-headwind", "userwindSpeed"){
|
||||
|
||||
data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings)
|
||||
data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: WeatherData?, val settings: HeadwindSettings)
|
||||
|
||||
companion object {
|
||||
fun streamValues(context: Context, karooSystem: KarooSystemService): Flow<Double> = flow {
|
||||
karooSystem.getRelativeHeadingFlow(context)
|
||||
.combine(context.streamCurrentWeatherData()) { value, data -> value to data }
|
||||
.combine(context.streamCurrentWeatherData(karooSystem)) { value, data -> value to data }
|
||||
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
|
||||
StreamData(value, data.firstOrNull()?.data, settings)
|
||||
StreamData(value, data, settings)
|
||||
}
|
||||
.filter { it.weatherResponse != null }
|
||||
.collect { streamData ->
|
||||
val windSpeed = streamData.weatherResponse?.current?.windSpeed ?: 0.0
|
||||
val windSpeed = streamData.weatherResponse?.windSpeed ?: 0.0
|
||||
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||
|
||||
if (streamData.settings.windDirectionIndicatorTextSetting == WindDirectionIndicatorTextSetting.HEADWIND_SPEED){
|
||||
|
||||
@ -16,10 +16,9 @@ import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.MainActivity
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.OpenMeteoData
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
@ -61,12 +60,12 @@ class WeatherDataType(
|
||||
|
||||
override fun startStream(emitter: Emitter<StreamState>) {
|
||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||
val currentWeatherData = applicationContext.streamCurrentWeatherData()
|
||||
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystem)
|
||||
|
||||
currentWeatherData
|
||||
.collect { data ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.firstOrNull()?.data?.current?.weatherCode}")
|
||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data.firstOrNull()?.data?.current?.weatherCode?.toDouble() ?: 0.0)))))
|
||||
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.weatherCode}")
|
||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.weatherCode?.toDouble() ?: 0.0)))))
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
@ -74,19 +73,15 @@ class WeatherDataType(
|
||||
}
|
||||
}
|
||||
|
||||
data class StreamData(val data: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings,
|
||||
data class StreamData(val data: WeatherData?, val settings: HeadwindSettings,
|
||||
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
|
||||
|
||||
private fun previewFlow(): Flow<StreamData> = flow {
|
||||
while (true){
|
||||
emit(StreamData(
|
||||
OpenMeteoCurrentWeatherResponse(
|
||||
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 980.0, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
|
||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||
|
||||
null
|
||||
), HeadwindSettings()
|
||||
))
|
||||
WeatherData(Instant.now().epochSecond, 0.0,
|
||||
20.0, 50.0, 3.0, 0.0, 1013.25, 980.0, 15.0, 30.0, 30.0,
|
||||
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false), HeadwindSettings()))
|
||||
|
||||
delay(5_000)
|
||||
}
|
||||
@ -107,8 +102,8 @@ class WeatherDataType(
|
||||
val dataFlow = if (config.preview){
|
||||
previewFlow()
|
||||
} else {
|
||||
combine(context.streamCurrentWeatherData(), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), karooSystem.getHeadingFlow(context)) { data, settings, profile, heading ->
|
||||
StreamData(data.firstOrNull()?.data, settings, profile, heading)
|
||||
combine(context.streamCurrentWeatherData(karooSystem), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), karooSystem.getHeadingFlow(context)) { data, settings, profile, heading ->
|
||||
StreamData(data, settings, profile, heading)
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,9 +119,9 @@ class WeatherDataType(
|
||||
return@collect
|
||||
}
|
||||
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.current.time))
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.weatherCode)
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.time))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.time))
|
||||
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
var modifier = GlanceModifier.fillMaxSize()
|
||||
@ -136,12 +131,12 @@ class WeatherDataType(
|
||||
Weather(
|
||||
baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data.current.windDirection.roundToInt(),
|
||||
windSpeed = data.current.windSpeed.roundToInt(),
|
||||
windGusts = data.current.windGusts.roundToInt(),
|
||||
precipitation = data.current.precipitation,
|
||||
windBearing = data.windDirection.roundToInt(),
|
||||
windSpeed = data.windSpeed.roundToInt(),
|
||||
windGusts = data.windGusts.roundToInt(),
|
||||
precipitation = data.precipitation,
|
||||
precipitationProbability = null,
|
||||
temperature = data.current.temperature.roundToInt(),
|
||||
temperature = data.temperature.roundToInt(),
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
rowAlignment = when (config.alignment){
|
||||
|
||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "weatherForecast") {
|
||||
|
||||
@ -31,7 +31,7 @@ import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@ -17,7 +17,7 @@ import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.streamDataFlow
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class WindDirectionDataType(val karooSystem: KarooSystemService, context: Context) : BaseDataType(context, "windDirection"){
|
||||
class WindDirectionDataType(val karooSystem: KarooSystemService, context: Context) : BaseDataType(karooSystem, context, "windDirection"){
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
@ -48,8 +48,8 @@ class WindDirectionDataType(val karooSystem: KarooSystemService, context: Contex
|
||||
)
|
||||
}
|
||||
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.windDirection
|
||||
override fun getValue(data: WeatherData): Double {
|
||||
return data.windDirection
|
||||
}
|
||||
|
||||
private fun previewFlow(): Flow<Double> {
|
||||
|
||||
@ -17,7 +17,6 @@ import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontFamily
|
||||
@ -26,10 +25,9 @@ import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun WindForecast(
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class WindGustsDataType(context: Context) : BaseDataType(context, "windGusts"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.windGusts
|
||||
class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){
|
||||
override fun getValue(data: WeatherData): Double {
|
||||
return data.windGusts
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
|
||||
class WindSpeedDataType(context: Context) : BaseDataType(context, "windSpeed"){
|
||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
||||
return data.current.windSpeed
|
||||
class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){
|
||||
override fun getValue(data: WeatherData): Double {
|
||||
return data.windSpeed
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package de.timklge.karooheadwind.screens
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -25,15 +24,15 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.timklge.karooheadwind.HeadwindStats
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.ServiceStatusSingleton
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.datatypes.ForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
||||
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
||||
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamStats
|
||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
@ -50,12 +49,28 @@ import kotlin.math.roundToInt
|
||||
fun WeatherScreen(onFinish: () -> Unit) {
|
||||
var karooConnected by remember { mutableStateOf<Boolean?>(null) }
|
||||
val ctx = LocalContext.current
|
||||
val karooSystem = remember { KarooSystemService(ctx) }
|
||||
val karooSystem = remember { KarooSystemService(ctx) }
|
||||
|
||||
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
||||
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
|
||||
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
|
||||
val weatherData by ctx.streamCurrentWeatherData().collectAsStateWithLifecycle(emptyList())
|
||||
val profileFlow = remember { karooSystem.streamUserProfile() }
|
||||
val profile by profileFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val statsFlow = remember { ctx.streamStats() }
|
||||
val stats by statsFlow.collectAsStateWithLifecycle(HeadwindStats())
|
||||
|
||||
val locationFlow = remember { karooSystem.getGpsCoordinateFlow(ctx) }
|
||||
val location by locationFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val currentWeatherDataFlow = remember { ctx.streamCurrentWeatherData(karooSystem) }
|
||||
val currentWeatherData by currentWeatherDataFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val forecastDataFlow = remember { ctx.streamCurrentForecastWeatherData() }
|
||||
val forecastData by forecastDataFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val upcomingRouteFlow = remember { karooSystem.streamUpcomingRoute() }
|
||||
val upcomingRoute by upcomingRouteFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val serviceStatusFlow = remember { ServiceStatusSingleton.getInstance().getServiceStatus() }
|
||||
val serviceStatus by serviceStatusFlow.collectAsStateWithLifecycle(false)
|
||||
|
||||
val baseBitmap = BitmapFactory.decodeResource(
|
||||
ctx.resources,
|
||||
@ -85,21 +100,20 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
val currentWeatherData = weatherData.firstOrNull()?.data
|
||||
val requestedWeatherPosition = weatherData.firstOrNull()?.requestedPosition
|
||||
val requestedWeatherPosition = forecastData?.data?.firstOrNull()?.coords
|
||||
|
||||
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
|
||||
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
|
||||
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(it.time)) }
|
||||
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(it.time)) }
|
||||
|
||||
if (karooConnected == true && currentWeatherData != null) {
|
||||
WeatherWidget(
|
||||
baseBitmap = baseBitmap,
|
||||
current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode),
|
||||
windBearing = currentWeatherData.current.windDirection.roundToInt(),
|
||||
windSpeed = currentWeatherData.current.windSpeed.roundToInt(),
|
||||
windGusts = currentWeatherData.current.windGusts.roundToInt(),
|
||||
precipitation = currentWeatherData.current.precipitation,
|
||||
temperature = currentWeatherData.current.temperature.toInt(),
|
||||
current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode),
|
||||
windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0,
|
||||
windSpeed = currentWeatherData?.windSpeed?.roundToInt() ?: 0,
|
||||
windGusts = currentWeatherData?.windGusts?.roundToInt() ?: 0,
|
||||
precipitation = currentWeatherData?.precipitation ?: 0.0,
|
||||
temperature = currentWeatherData?.temperature?.toInt() ?: 0,
|
||||
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = formattedDate,
|
||||
@ -113,8 +127,6 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
val lastPositionDistanceStr =
|
||||
lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
||||
|
||||
val serviceStatus by ServiceStatusSingleton.getInstance().getServiceStatus().collectAsStateWithLifecycle(false)
|
||||
|
||||
if (!serviceStatus){
|
||||
Text(
|
||||
modifier = Modifier.padding(5.dp),
|
||||
@ -178,21 +190,19 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
val upcomingRoute by karooSystem.streamUpcomingRoute().collectAsStateWithLifecycle(null)
|
||||
|
||||
for (index in 1..12){
|
||||
val positionIndex = if (weatherData.size == 1) 0 else index
|
||||
val positionIndex = if (forecastData?.data?.size == 1) 0 else index
|
||||
|
||||
if (weatherData.getOrNull(positionIndex) == null) break
|
||||
if (index >= (weatherData.getOrNull(positionIndex)?.data?.forecastData?.weatherCode?.size ?: 0)) {
|
||||
if (forecastData?.data?.getOrNull(positionIndex) == null) break
|
||||
if (index >= (forecastData?.data?.getOrNull(positionIndex)?.forecasts?.size ?: 0)) {
|
||||
break
|
||||
}
|
||||
|
||||
val data = weatherData.getOrNull(positionIndex)?.data
|
||||
val distanceAlongRoute = weatherData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||
val position = weatherData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
||||
val data = forecastData?.data?.getOrNull(positionIndex)
|
||||
val distanceAlongRoute = forecastData?.data?.getOrNull(positionIndex)?.coords?.distanceAlongRoute
|
||||
val position = forecastData?.data?.getOrNull(positionIndex)?.coords?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Distance along route index ${positionIndex}: $position")
|
||||
// Log.d(KarooHeadwindExtension.TAG, "Distance along route index ${positionIndex}: $position")
|
||||
|
||||
if (index > 1) {
|
||||
Spacer(
|
||||
@ -209,25 +219,26 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||
}
|
||||
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
|
||||
val unixTime = data?.forecastData?.time?.get(index) ?: 0
|
||||
val weatherData = data?.forecasts?.getOrNull(index)
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||
val unixTime = weatherData?.time ?: 0
|
||||
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
|
||||
WeatherWidget(
|
||||
baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data?.forecastData?.windDirection?.get(index)?.roundToInt() ?: 0,
|
||||
windSpeed = data?.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0,
|
||||
windGusts = data?.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0,
|
||||
precipitation = data?.forecastData?.precipitation?.get(index) ?: 0.0,
|
||||
temperature = data?.forecastData?.temperature?.get(index)?.roundToInt() ?: 0,
|
||||
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
||||
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
||||
precipitation = weatherData?.precipitation ?: 0.0,
|
||||
temperature = weatherData?.temperature?.toInt() ?: 0,
|
||||
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedForecastTime,
|
||||
dateLabel = formattedForecastDate,
|
||||
distance = distanceFromCurrent,
|
||||
includeDistanceLabel = true,
|
||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0,
|
||||
precipitationProbability = weatherData?.precipitationProbability?.toInt() ?: 0,
|
||||
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
)
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.datatypes.getArrowBitmapByBearing
|
||||
import de.timklge.karooheadwind.datatypes.getWeatherIcon
|
||||
import kotlin.math.absoluteValue
|
||||
@ -65,7 +65,7 @@ fun WeatherWidget(
|
||||
)
|
||||
}
|
||||
|
||||
if (distance != null) {
|
||||
if (distance != null && distance > 200) {
|
||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||
val text = if (includeDistanceLabel){
|
||||
@ -154,7 +154,7 @@ fun WeatherWidget(
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
) {
|
||||
Image(
|
||||
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(),
|
||||
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing + 180).asImageBitmap(),
|
||||
colorFilter = ColorFilter.tint(Color.Black),
|
||||
contentDescription = "Wind direction",
|
||||
modifier = Modifier.size(20.dp)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package de.timklge.karooheadwind
|
||||
package de.timklge.karooheadwind.util
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
19
app/src/main/kotlin/de/timklge/karooheadwind/util/Gzip.kt
Normal file
19
app/src/main/kotlin/de/timklge/karooheadwind/util/Gzip.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package de.timklge.karooheadwind.util
|
||||
|
||||
import io.hammerhead.karooext.models.HttpResponseState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
suspend fun ungzip(response: HttpResponseState.Complete): String {
|
||||
val inputStream = java.io.ByteArrayInputStream(response.body ?: ByteArray(0))
|
||||
val lowercaseHeaders = response.headers.map { (k: String, v: String) -> k.lowercase() to v.lowercase() }.toMap()
|
||||
val isGzippedResponse = lowercaseHeaders["content-encoding"]?.contains("gzip") == true
|
||||
|
||||
return if(isGzippedResponse){
|
||||
val gzipStream = withContext(Dispatchers.IO) { GZIPInputStream(inputStream) }
|
||||
gzipStream.use { stream -> String(stream.readBytes()) }
|
||||
} else {
|
||||
inputStream.use { stream -> String(stream.readBytes()) }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WeatherData(
|
||||
val time: Long,
|
||||
val temperature: Double,
|
||||
val relativeHumidity: Double? = null,
|
||||
val precipitation: Double,
|
||||
val precipitationProbability: Double? = null,
|
||||
val cloudCover: Double? = null,
|
||||
val sealevelPressure: Double? = null,
|
||||
val surfacePressure: Double? = null,
|
||||
val windSpeed: Double,
|
||||
val windDirection: Double,
|
||||
val windGusts: Double,
|
||||
val weatherCode: Int,
|
||||
val isForecast: Boolean
|
||||
)
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WeatherDataForLocation(
|
||||
val current: WeatherData,
|
||||
val coords: GpsCoordinates,
|
||||
val timezone: String? = null,
|
||||
val elevation: Double? = null,
|
||||
val forecasts: List<WeatherData>? = null,
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
import de.timklge.karooheadwind.WeatherDataProvider
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WeatherDataResponse(
|
||||
val error: String? = null,
|
||||
val provider: WeatherDataProvider,
|
||||
val data: List<WeatherDataForLocation>,
|
||||
)
|
||||
@ -0,0 +1,22 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
enum class WeatherInterpretation {
|
||||
CLEAR, CLOUDY, RAINY, SNOWY, DRIZZLE, THUNDERSTORM, UNKNOWN;
|
||||
|
||||
companion object {
|
||||
// WMO weather interpretation codes (WW)
|
||||
fun fromWeatherCode(code: Int?): WeatherInterpretation {
|
||||
return when(code){
|
||||
0 -> CLEAR
|
||||
1, 2, 3 -> CLOUDY
|
||||
45, 48, 61, 63, 65, 66, 67, 80, 81, 82 -> RAINY
|
||||
71, 73, 75, 77, 85, 86 -> SNOWY
|
||||
51, 53, 55, 56, 57 -> DRIZZLE
|
||||
95, 96, 99 -> THUNDERSTORM
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun getKnownWeatherCodes(): Set<Int> = setOf(0, 1, 2, 3, 45, 48, 61, 63, 65, 66, 67, 80, 81, 82, 71, 73, 75, 77, 85, 86, 51, 53, 55, 56, 57, 95, 96, 99)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package de.timklge.karooheadwind
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
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 {
|
||||
@ -11,5 +11,5 @@ interface WeatherProvider {
|
||||
coordinates: List<GpsCoordinates>,
|
||||
settings: HeadwindSettings,
|
||||
profile: UserProfile?
|
||||
): HttpResponseState.Complete
|
||||
): WeatherDataResponse
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
class WeatherProviderException(val statusCode: Int, message: String) : Exception(message)
|
||||
@ -0,0 +1,82 @@
|
||||
package de.timklge.karooheadwind.weatherprovider
|
||||
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.weatherprovider.openmeteo.OpenMeteoWeatherProvider
|
||||
import de.timklge.karooheadwind.weatherprovider.openweathermap.OpenWeatherMapWeatherProvider
|
||||
import de.timklge.karooheadwind.WeatherDataProvider
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
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
|
||||
|
||||
suspend fun makeWeatherRequest(
|
||||
karooSystemService: KarooSystemService,
|
||||
gpsCoordinates: List<GpsCoordinates>,
|
||||
settings: HeadwindSettings,
|
||||
profile: UserProfile?
|
||||
): WeatherDataResponse {
|
||||
val provider = getProvider(settings)
|
||||
|
||||
try {
|
||||
val response = provider.getWeatherData(karooSystemService, gpsCoordinates, settings, profile)
|
||||
|
||||
return response
|
||||
} catch(e: Throwable){
|
||||
Log.d(KarooHeadwindExtension.TAG, "Weather request failed: $e")
|
||||
|
||||
if (provider is OpenWeatherMapWeatherProvider && (e is WeatherProviderException && (e.statusCode == 401 || e.statusCode == 403))) {
|
||||
handleOpenWeatherMapFailure()
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProvider(settings: HeadwindSettings): WeatherProvider {
|
||||
val currentDate = LocalDate.now()
|
||||
|
||||
if (fallbackUntilDate != null && !currentDate.isAfter(fallbackUntilDate)) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Using fallback OpenMeteo until $fallbackUntilDate")
|
||||
return OpenMeteoWeatherProvider()
|
||||
}
|
||||
|
||||
|
||||
if (settings.weatherProvider == WeatherDataProvider.OPEN_WEATHER_MAP &&
|
||||
openWeatherMapConsecutiveFailures >= MAX_FAILURES_BEFORE_TEMP_FALLBACK
|
||||
) {
|
||||
openWeatherMapConsecutiveFailures = 0
|
||||
Log.d(KarooHeadwindExtension.TAG, "Using temporary fallback OpenMeteo")
|
||||
return OpenMeteoWeatherProvider()
|
||||
}
|
||||
|
||||
|
||||
return when (settings.weatherProvider) {
|
||||
WeatherDataProvider.OPEN_METEO -> OpenMeteoWeatherProvider()
|
||||
WeatherDataProvider.OPEN_WEATHER_MAP -> OpenWeatherMapWeatherProvider(settings.openWeatherMapApiKey)
|
||||
}
|
||||
}
|
||||
|
||||
private 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openmeteo
|
||||
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoWeatherData(
|
||||
val time: Long, val interval: Int,
|
||||
@SerialName("temperature_2m") val temperature: Double,
|
||||
@SerialName("relative_humidity_2m") val relativeHumidity: Int,
|
||||
@SerialName("precipitation") val precipitation: Double,
|
||||
@SerialName("cloud_cover") val cloudCover: Int,
|
||||
@SerialName("surface_pressure") val surfacePressure: Double,
|
||||
@SerialName("pressure_msl") val sealevelPressure: Double? = null,
|
||||
@SerialName("wind_speed_10m") val windSpeed: Double,
|
||||
@SerialName("wind_direction_10m") val windDirection: Double,
|
||||
@SerialName("wind_gusts_10m") val windGusts: Double,
|
||||
@SerialName("weather_code") val weatherCode: Int,
|
||||
) {
|
||||
fun toWeatherData(): WeatherData = WeatherData(
|
||||
temperature = temperature,
|
||||
relativeHumidity = relativeHumidity.toDouble(),
|
||||
precipitation = precipitation,
|
||||
cloudCover = cloudCover.toDouble(),
|
||||
surfacePressure = surfacePressure,
|
||||
sealevelPressure = sealevelPressure,
|
||||
windSpeed = windSpeed,
|
||||
windDirection = windDirection,
|
||||
windGusts = windGusts,
|
||||
weatherCode = weatherCode,
|
||||
time = time,
|
||||
isForecast = false,
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openmeteo
|
||||
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoWeatherDataForLocation(
|
||||
val current: OpenMeteoWeatherData,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val timezone: String,
|
||||
val elevation: Double,
|
||||
@SerialName("utc_offset_seconds") val utfOffsetSeconds: Int,
|
||||
@SerialName("hourly") val forecastData: OpenMeteoWeatherForecastData?,
|
||||
) {
|
||||
fun toWeatherDataForLocation(distanceAlongRoute: Double?): WeatherDataForLocation {
|
||||
val forecasts = forecastData?.toWeatherData()
|
||||
return WeatherDataForLocation(
|
||||
current = current.toWeatherData(),
|
||||
coords = GpsCoordinates(latitude, longitude, bearing = null, distanceAlongRoute = distanceAlongRoute),
|
||||
timezone = timezone,
|
||||
elevation = elevation,
|
||||
forecasts = forecasts
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openmeteo
|
||||
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenMeteoWeatherForecastData(
|
||||
@SerialName("time") val time: List<Long>,
|
||||
@SerialName("temperature_2m") val temperature: List<Double>,
|
||||
@SerialName("precipitation_probability") val precipitationProbability: List<Int>,
|
||||
@SerialName("precipitation") val precipitation: List<Double>,
|
||||
@SerialName("weather_code") val weatherCode: List<Int>,
|
||||
@SerialName("wind_speed_10m") val windSpeed: List<Double>,
|
||||
@SerialName("wind_direction_10m") val windDirection: List<Double>,
|
||||
@SerialName("wind_gusts_10m") val windGusts: List<Double>,
|
||||
) {
|
||||
fun toWeatherData(): List<WeatherData> {
|
||||
return time.mapIndexed { index, t ->
|
||||
WeatherData(
|
||||
temperature = temperature[index],
|
||||
precipitation = precipitation[index],
|
||||
windSpeed = windSpeed[index],
|
||||
windDirection = windDirection[index],
|
||||
windGusts = windGusts[index],
|
||||
weatherCode = weatherCode[index],
|
||||
time = t,
|
||||
isForecast = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openmeteo
|
||||
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.PrecipitationUnit
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherDataProvider
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.jsonWithUnknownKeys
|
||||
import de.timklge.karooheadwind.util.ungzip
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherProvider
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
|
||||
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 java.util.Locale
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class OpenMeteoWeatherProvider : WeatherProvider {
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun makeOpenMeteoWeatherRequest(karooSystemService: KarooSystemService, gpsCoordinates: List<GpsCoordinates>, settings: HeadwindSettings, profile: UserProfile?): String {
|
||||
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
|
||||
|
||||
val response = callbackFlow {
|
||||
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12
|
||||
val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) }
|
||||
val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) }
|
||||
val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}¤t=surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=${settings.windUnit.id}&precipitation_unit=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}"
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
||||
|
||||
val listenerId = karooSystemService.addConsumer(
|
||||
OnHttpResponse.MakeHttpRequest(
|
||||
"GET",
|
||||
url,
|
||||
waitForConnection = false,
|
||||
headers = mapOf("User-Agent" to KarooHeadwindExtension.TAG, "Accept-Encoding" to "gzip"),
|
||||
),
|
||||
onEvent = { event: OnHttpResponse ->
|
||||
if (event.state is HttpResponseState.Complete){
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http response received")
|
||||
trySend(event.state as HttpResponseState.Complete)
|
||||
close()
|
||||
}
|
||||
},
|
||||
onError = { err ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Http error: $err")
|
||||
close(WeatherProviderException(0, "Http error: $err"))
|
||||
})
|
||||
awaitClose {
|
||||
karooSystemService.removeConsumer(listenerId)
|
||||
}
|
||||
}.timeout(30.seconds).catch { e: Throwable ->
|
||||
if (e is TimeoutCancellationException){
|
||||
emit(HttpResponseState.Complete(500, mapOf(), null, "Timeout"))
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}.single()
|
||||
|
||||
if (response.statusCode !in 200..299) {
|
||||
Log.e(KarooHeadwindExtension.TAG, "OpenMeteo API request failed with status code ${response.statusCode}")
|
||||
throw WeatherProviderException(response.statusCode, "OpenMeteo API request failed with status code ${response.statusCode}")
|
||||
}
|
||||
|
||||
return ungzip(response)
|
||||
}
|
||||
|
||||
override suspend fun getWeatherData(
|
||||
karooSystem: KarooSystemService,
|
||||
coordinates: List<GpsCoordinates>,
|
||||
settings: HeadwindSettings,
|
||||
profile: UserProfile?
|
||||
): WeatherDataResponse {
|
||||
val openMeteoResponse = makeOpenMeteoWeatherRequest(karooSystem, coordinates, settings, profile)
|
||||
|
||||
val weatherData = if (coordinates.size == 1) {
|
||||
listOf(jsonWithUnknownKeys.decodeFromString<OpenMeteoWeatherDataForLocation>(openMeteoResponse))
|
||||
} else {
|
||||
jsonWithUnknownKeys.decodeFromString<List<OpenMeteoWeatherDataForLocation>>(openMeteoResponse)
|
||||
}
|
||||
|
||||
val response = WeatherDataResponse(
|
||||
provider = WeatherDataProvider.OPEN_METEO,
|
||||
data = weatherData.zip(coordinates) { openMeteoWeatherDataForLocation, location ->
|
||||
openMeteoWeatherDataForLocation.toWeatherDataForLocation(location.distanceAlongRoute)
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openweathermap
|
||||
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenWeatherMapForecastData(
|
||||
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>
|
||||
) {
|
||||
fun toWeatherData(): WeatherData = WeatherData(
|
||||
temperature = temp,
|
||||
relativeHumidity = humidity.toDouble(),
|
||||
precipitation = rain?.h1 ?: 0.0,
|
||||
cloudCover = clouds.toDouble(),
|
||||
surfacePressure = pressure.toDouble(),
|
||||
sealevelPressure = pressure.toDouble(), // FIXME
|
||||
windSpeed = wind_speed,
|
||||
windDirection = wind_deg.toDouble(),
|
||||
windGusts = wind_gust ?: wind_speed,
|
||||
weatherCode = OpenWeatherMapWeatherProvider.convertWeatherCodeToOpenMeteo(
|
||||
weather.firstOrNull()?.id ?: 800
|
||||
),
|
||||
time = dt,
|
||||
isForecast = true
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openweathermap
|
||||
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenWeatherMapWeatherData(
|
||||
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>){
|
||||
|
||||
fun toWeatherData(): WeatherData = WeatherData(
|
||||
temperature = temp,
|
||||
relativeHumidity = humidity.toDouble(),
|
||||
precipitation = rain?.h1 ?: 0.0,
|
||||
cloudCover = clouds.toDouble(),
|
||||
surfacePressure = pressure.toDouble(),
|
||||
sealevelPressure = pressure.toDouble(), // FIXME
|
||||
windSpeed = wind_speed,
|
||||
windDirection = wind_deg.toDouble(),
|
||||
windGusts = wind_gust ?: wind_speed,
|
||||
weatherCode = OpenWeatherMapWeatherProvider.convertWeatherCodeToOpenMeteo(
|
||||
weather.firstOrNull()?.id ?: 800
|
||||
),
|
||||
time = dt,
|
||||
isForecast = false
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openweathermap
|
||||
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OpenWeatherMapWeatherDataForLocation(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
val timezone: String,
|
||||
@SerialName("timezone_offset") val timezoneOffset: Int,
|
||||
val current: OpenWeatherMapWeatherData,
|
||||
val hourly: List<OpenWeatherMapForecastData>
|
||||
){
|
||||
fun toWeatherDataForLocation(distanceAlongRoute: Double?): WeatherDataForLocation = WeatherDataForLocation(
|
||||
current = current.toWeatherData(),
|
||||
coords = GpsCoordinates(lat, lon, bearing = null, distanceAlongRoute = distanceAlongRoute),
|
||||
timezone = timezone,
|
||||
forecasts = hourly.map { it.toWeatherData() }
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
package de.timklge.karooheadwind.weatherprovider.openweathermap
|
||||
|
||||
import android.util.Log
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.WeatherDataProvider
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.jsonWithUnknownKeys
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherProvider
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
|
||||
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 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 OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider {
|
||||
companion object {
|
||||
fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int {
|
||||
// Mapping OpenWeatherMap to WMO OpenMeteo
|
||||
return when (owmCode) {
|
||||
in 200..299 -> 95 // Thunderstorm
|
||||
in 300..399 -> 51 // Drizzle
|
||||
in 500..599 -> 61 // Rain
|
||||
in 600..699 -> 71 // Snow
|
||||
800 -> 0 // Clear
|
||||
in 801..804 -> 1 // Cloudy
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getWeatherData(
|
||||
karooSystem: KarooSystemService,
|
||||
coordinates: List<GpsCoordinates>,
|
||||
settings: HeadwindSettings,
|
||||
profile: UserProfile?
|
||||
): WeatherDataResponse {
|
||||
|
||||
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
|
||||
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
|
||||
|
||||
val responses = mutableListOf<WeatherDataForLocation>()
|
||||
|
||||
val openWeatherMapWeatherDataForLocation = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
|
||||
responses.add(openWeatherMapWeatherDataForLocation.toWeatherDataForLocation(null))
|
||||
|
||||
// FIXME Route forecast
|
||||
|
||||
return WeatherDataResponse(
|
||||
provider = WeatherDataProvider.OPEN_WEATHER_MAP,
|
||||
data = responses
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun makeOpenWeatherMapRequest(
|
||||
service: KarooSystemService,
|
||||
coordinates: List<GpsCoordinates>,
|
||||
apiKey: String
|
||||
): HttpResponseState.Complete {
|
||||
val response = callbackFlow {
|
||||
val coordinate = coordinates.first()
|
||||
// URL API 3.0 with onecall endpoint
|
||||
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(WeatherProviderException(0, 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()
|
||||
|
||||
if (response.statusCode == 401 || response.statusCode == 403){
|
||||
Log.e(KarooHeadwindExtension.TAG, "OpenWeatherMap API key is invalid or expired")
|
||||
throw WeatherProviderException(response.statusCode, "OpenWeatherMap API key is invalid or expired")
|
||||
} else if (response.statusCode !in 200..299) {
|
||||
Log.e(KarooHeadwindExtension.TAG, "OpenWeatherMap API request failed with status code ${response.statusCode}")
|
||||
throw WeatherProviderException(response.statusCode, "OpenWeatherMap API request failed with status code ${response.statusCode}")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
@ -8,9 +8,9 @@
|
||||
|
||||
<color name="green">#00ff00</color>
|
||||
<color name="orange">#ff9930</color>
|
||||
<color name="red">#FF2424</color>
|
||||
<color name="red">#FF5454</color>
|
||||
|
||||
<color name="hGreen">#008000</color>
|
||||
<color name="hOrange">#BB4300</color>
|
||||
<color name="hRed">#B30000</color>
|
||||
<color name="hRed">#A30000</color>
|
||||
</resources>
|
||||
Loading…
x
Reference in New Issue
Block a user