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.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import com.mapbox.geojson.LineString
|
import com.mapbox.geojson.LineString
|
||||||
|
import com.mapbox.geojson.Point
|
||||||
import com.mapbox.turf.TurfConstants
|
import com.mapbox.turf.TurfConstants
|
||||||
import com.mapbox.turf.TurfMeasurement
|
import com.mapbox.turf.TurfMeasurement
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
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.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.DataType
|
import io.hammerhead.karooext.models.DataType
|
||||||
import io.hammerhead.karooext.models.OnNavigationState
|
import io.hammerhead.karooext.models.OnNavigationState
|
||||||
import io.hammerhead.karooext.models.StreamState
|
import io.hammerhead.karooext.models.StreamState
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
|
||||||
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
|
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
val settingsKey = stringPreferencesKey("settings")
|
val settingsKey = stringPreferencesKey("settings")
|
||||||
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
|
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
|
||||||
val currentDataKey = stringPreferencesKey("currentForecasts")
|
val currentDataKey = stringPreferencesKey("currentForecastsUnified")
|
||||||
val statsKey = stringPreferencesKey("stats")
|
val statsKey = stringPreferencesKey("stats")
|
||||||
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
|
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
|
||||||
|
|
||||||
@ -53,12 +61,9 @@ suspend fun saveStats(context: Context, stats: HeadwindStats) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
suspend fun saveCurrentData(context: Context, response: WeatherDataResponse) {
|
||||||
data class WeatherDataResponse(val data: OpenMeteoCurrentWeatherResponse, val requestedPosition: GpsCoordinates)
|
|
||||||
|
|
||||||
suspend fun saveCurrentData(context: Context, forecast: List<WeatherDataResponse>) {
|
|
||||||
context.dataStore.edit { t ->
|
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)
|
data class UpcomingRoute(val distanceAlongRoute: Double, val routePolyline: LineString, val routeLength: Double)
|
||||||
|
|
||||||
fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
||||||
val distanceToDestinationStream = flow {
|
val distanceToDestinationStream = streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION)
|
||||||
emit(null)
|
.map { (it as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION) }
|
||||||
|
.distinctUntilChanged()
|
||||||
streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION)
|
|
||||||
.map { (it as? StreamState.Streaming)?.dataPoint?.values?.get(DataType.Field.DISTANCE_TO_DESTINATION) }
|
|
||||||
.collect { emit(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastKnownDistanceAlongRoute = 0.0
|
var lastKnownDistanceAlongRoute = 0.0
|
||||||
var lastKnownRoutePolyline: LineString? = null
|
var lastKnownRoutePolyline: LineString? = null
|
||||||
@ -130,6 +131,7 @@ fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
|||||||
.map { navigationState ->
|
.map { navigationState ->
|
||||||
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
||||||
}
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
|
Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
|
||||||
if (routePolyline != null){
|
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 ->
|
return dataStore.data.map { settingsJson ->
|
||||||
try {
|
try {
|
||||||
val data = settingsJson[currentDataKey]
|
val data = settingsJson[currentDataKey]
|
||||||
data?.let { d -> jsonWithUnknownKeys.decodeFromString<List<WeatherDataResponse>>(d) } ?: emptyList()
|
|
||||||
|
data?.let { d -> jsonWithUnknownKeys.decodeFromString<WeatherDataResponse>(d) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
|
||||||
emptyList()
|
null
|
||||||
}
|
}
|
||||||
}.distinctUntilChanged().map { response ->
|
}.distinctUntilChanged()
|
||||||
response.filter { forecast ->
|
}
|
||||||
forecast.data.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12)
|
|
||||||
|
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.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
|
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -13,10 +13,8 @@ import kotlinx.coroutines.flow.emitAll
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
|
||||||
sealed class HeadingResponse {
|
sealed class HeadingResponse {
|
||||||
data object NoGps: HeadingResponse()
|
data object NoGps: HeadingResponse()
|
||||||
data object NoWeatherData: HeadingResponse()
|
data object NoWeatherData: HeadingResponse()
|
||||||
@ -24,14 +22,14 @@ sealed class HeadingResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> {
|
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> {
|
||||||
val currentWeatherData = context.streamCurrentWeatherData()
|
val currentWeatherData = context.streamCurrentWeatherData(this)
|
||||||
|
|
||||||
return getHeadingFlow(context)
|
return getHeadingFlow(context)
|
||||||
.combine(currentWeatherData) { bearing, data -> bearing to data }
|
.combine(currentWeatherData) { bearing, data -> bearing to data }
|
||||||
.map { (bearing, data) ->
|
.map { (bearing, data) ->
|
||||||
when {
|
when {
|
||||||
bearing is HeadingResponse.Value && data.isNotEmpty() -> {
|
bearing is HeadingResponse.Value && data != null -> {
|
||||||
val windBearing = data.first().data.current.windDirection + 180
|
val windBearing = data.windDirection + 180
|
||||||
val diff = signedAngleDifference(bearing.diff, windBearing)
|
val diff = signedAngleDifference(bearing.diff, windBearing)
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: Heading $bearing vs wind $windBearing => $diff")
|
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)
|
HeadingResponse.Value(diff)
|
||||||
}
|
}
|
||||||
bearing is HeadingResponse.NoGps -> HeadingResponse.NoGps
|
bearing is HeadingResponse.NoGps -> HeadingResponse.NoGps
|
||||||
bearing is HeadingResponse.NoWeatherData || data.isEmpty() -> HeadingResponse.NoWeatherData
|
bearing is HeadingResponse.NoWeatherData || data == null -> HeadingResponse.NoWeatherData
|
||||||
else -> bearing
|
else -> bearing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package de.timklge.karooheadwind
|
package de.timklge.karooheadwind
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.util.fastZip
|
|
||||||
import com.mapbox.geojson.LineString
|
import com.mapbox.geojson.LineString
|
||||||
import com.mapbox.turf.TurfConstants
|
import com.mapbox.turf.TurfConstants
|
||||||
import com.mapbox.turf.TurfMeasurement
|
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.WindForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherProviderFactory
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.KarooExtension
|
import io.hammerhead.karooext.extension.KarooExtension
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
@ -45,11 +45,9 @@ import kotlinx.coroutines.flow.retry
|
|||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.time.debounce
|
import kotlinx.coroutines.time.debounce
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
@ -73,15 +71,15 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
WeatherDataType(karooSystem, applicationContext),
|
WeatherDataType(karooSystem, applicationContext),
|
||||||
WeatherForecastDataType(karooSystem),
|
WeatherForecastDataType(karooSystem),
|
||||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeHumidityDataType(applicationContext),
|
RelativeHumidityDataType(karooSystem, applicationContext),
|
||||||
CloudCoverDataType(applicationContext),
|
CloudCoverDataType(karooSystem, applicationContext),
|
||||||
WindGustsDataType(applicationContext),
|
WindGustsDataType(karooSystem, applicationContext),
|
||||||
WindSpeedDataType(applicationContext),
|
WindSpeedDataType(karooSystem, applicationContext),
|
||||||
TemperatureDataType(applicationContext),
|
TemperatureDataType(karooSystem, applicationContext),
|
||||||
WindDirectionDataType(karooSystem, applicationContext),
|
WindDirectionDataType(karooSystem, applicationContext),
|
||||||
PrecipitationDataType(applicationContext),
|
PrecipitationDataType(karooSystem, applicationContext),
|
||||||
SurfacePressureDataType(applicationContext),
|
SurfacePressureDataType(karooSystem, applicationContext),
|
||||||
SealevelPressureDataType(applicationContext),
|
SealevelPressureDataType(karooSystem, applicationContext),
|
||||||
UserWindSpeedDataType(karooSystem, applicationContext),
|
UserWindSpeedDataType(karooSystem, applicationContext),
|
||||||
TemperatureForecastDataType(karooSystem),
|
TemperatureForecastDataType(karooSystem),
|
||||||
PrecipitationForecastDataType(karooSystem),
|
PrecipitationForecastDataType(karooSystem),
|
||||||
@ -121,10 +119,9 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
}
|
}
|
||||||
.debounce(Duration.ofSeconds(5))
|
.debounce(Duration.ofSeconds(5))
|
||||||
|
|
||||||
var requestedGpsCoordinates: List<GpsCoordinates> = mutableListOf()
|
var requestedGpsCoordinates: List<GpsCoordinates> = emptyList()
|
||||||
|
|
||||||
val settingsStream = streamSettings(karooSystem)
|
val settingsStream = streamSettings(karooSystem).filter { it.welcomeDialogAccepted }
|
||||||
.filter { it.welcomeDialogAccepted }
|
|
||||||
|
|
||||||
data class StreamData(val settings: HeadwindSettings, val gps: GpsCoordinates?, val profile: UserProfile?, val upcomingRoute: UpcomingRoute?)
|
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?)
|
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)
|
requestedGpsCoordinates = mutableListOf(gps)
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = karooSystem.makeOpenMeteoHttpRequest(requestedGpsCoordinates, settings, profile)
|
val response = try {
|
||||||
if (response.error != null){
|
WeatherProviderFactory.makeWeatherRequest(karooSystem, requestedGpsCoordinates, settings, profile)
|
||||||
try {
|
} catch(e: Throwable){
|
||||||
|
val stats = lastKnownStats.copy(failedWeatherRequest = System.currentTimeMillis())
|
||||||
val stats = lastKnownStats.copy(failedWeatherRequest = System.currentTimeMillis())
|
launch {
|
||||||
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
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (responseBody != null) {
|
saveStats(this@KarooHeadwindExtension, stats)
|
||||||
if (responseBody.trim().startsWith("[")) {
|
} catch(e: Exception){
|
||||||
val responseArray =
|
Log.e(TAG, "Failed to write stats", e)
|
||||||
jsonWithUnknownKeys.decodeFromString<List<OpenMeteoCurrentWeatherResponse>>(
|
|
||||||
responseBody
|
|
||||||
)
|
|
||||||
weatherDataProvider = responseArray.firstOrNull()?.provider
|
|
||||||
} else {
|
|
||||||
val responseObject =
|
|
||||||
jsonWithUnknownKeys.decodeFromString<OpenMeteoCurrentWeatherResponse>(
|
|
||||||
responseBody
|
|
||||||
)
|
|
||||||
weatherDataProvider = responseObject.provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error decoding provider", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val stats = lastKnownStats.copy(
|
|
||||||
lastSuccessfulWeatherRequest = System.currentTimeMillis(),
|
|
||||||
lastSuccessfulWeatherPosition = gps,
|
|
||||||
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
|
response
|
||||||
@ -263,30 +239,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
delay(1.minutes); true
|
delay(1.minutes); true
|
||||||
}.collect { response ->
|
}.collect { response ->
|
||||||
try {
|
try {
|
||||||
val inputStream = java.io.ByteArrayInputStream(response.body ?: ByteArray(0))
|
saveCurrentData(applicationContext, response)
|
||||||
val lowercaseHeaders = response.headers.map { (k: String, v: String) -> k.lowercase() to v.lowercase() }.toMap()
|
Log.d(TAG, "Got updated weather info: $response")
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
saveWidgetSettings(applicationContext, HeadwindWidgetSettings(currentForecastHourOffset = 0))
|
saveWidgetSettings(applicationContext, HeadwindWidgetSettings(currentForecastHourOffset = 0))
|
||||||
} catch(e: Exception){
|
} 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.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
import io.hammerhead.karooext.internal.Emitter
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
import io.hammerhead.karooext.models.DataPoint
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
@ -16,22 +17,24 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class BaseDataType(
|
abstract class BaseDataType(
|
||||||
|
private val karooSystemService: KarooSystemService,
|
||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
dataTypeId: String
|
dataTypeId: String
|
||||||
) : DataTypeImpl("karoo-headwind", dataTypeId) {
|
) : DataTypeImpl("karoo-headwind", dataTypeId) {
|
||||||
abstract fun getValue(data: OpenMeteoCurrentWeatherResponse): Double
|
abstract fun getValue(data: WeatherData): Double?
|
||||||
|
|
||||||
override fun startStream(emitter: Emitter<StreamState>) {
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
|
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val currentWeatherData = applicationContext.streamCurrentWeatherData()
|
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystemService)
|
||||||
|
|
||||||
currentWeatherData
|
currentWeatherData
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.collect { data ->
|
.collect { data ->
|
||||||
val value = data.firstOrNull()?.data?.let { w -> getValue(w) }
|
val value = getValue(data)
|
||||||
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
|
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
|
||||||
if (value != null){
|
|
||||||
|
if (value != null) {
|
||||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
|
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
|
||||||
} else {
|
} else {
|
||||||
emitter.onNext(StreamState.NotAvailable)
|
emitter.onNext(StreamState.NotAvailable)
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double? {
|
||||||
return data.current.cloudCover.toDouble()
|
return data.cloudCover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ import androidx.glance.action.ActionParameters
|
|||||||
import androidx.glance.appwidget.action.ActionCallback
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.saveWidgetSettings
|
import de.timklge.karooheadwind.saveWidgetSettings
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||||
import de.timklge.karooheadwind.streamWidgetSettings
|
import de.timklge.karooheadwind.streamWidgetSettings
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
@ -21,13 +21,13 @@ class CycleHoursAction : ActionCallback {
|
|||||||
Log.d(KarooHeadwindExtension.TAG, "Cycling hours")
|
Log.d(KarooHeadwindExtension.TAG, "Cycling hours")
|
||||||
|
|
||||||
val currentSettings = context.streamWidgetSettings().first()
|
val currentSettings = context.streamWidgetSettings().first()
|
||||||
val data = context.streamCurrentWeatherData().firstOrNull()
|
val forecastData = context.streamCurrentForecastWeatherData().firstOrNull()
|
||||||
|
|
||||||
var hourOffset = currentSettings.currentForecastHourOffset + 3
|
var hourOffset = currentSettings.currentForecastHourOffset + 3
|
||||||
val requestedPositions = data?.size
|
val requestedPositions = forecastData?.data?.size
|
||||||
val requestedHours = data?.firstOrNull()?.data?.forecastData?.weatherCode?.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
|
hourOffset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,16 +25,16 @@ import de.timklge.karooheadwind.HeadingResponse
|
|||||||
import de.timklge.karooheadwind.HeadwindSettings
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
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.R
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.WeatherDataResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
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.getHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
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())
|
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 widgetSettings: HeadwindWidgetSettings? = null,
|
||||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
||||||
|
|
||||||
@ -97,57 +97,63 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
while (true) {
|
while (true) {
|
||||||
val data = (0..<10).map { index ->
|
val data = (0..<10).map { index ->
|
||||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
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(
|
val weatherData = (0..<12).map {
|
||||||
forecastTimes,
|
val forecastTime = timeAtFullHour + it * 60 * 60
|
||||||
forecastTemperatures,
|
val forecastTemperature = 20.0 + (-20..20).random()
|
||||||
forecastPrecipitationPropability,
|
val forecastPrecipitation = 0.0 + (0..10).random()
|
||||||
forecastPrecipitation,
|
val forecastPrecipitationProbability = (0..100).random()
|
||||||
forecastWeatherCodes,
|
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
|
||||||
forecastWindSpeed,
|
val forecastWindSpeed = 0.0 + (0..10).random()
|
||||||
forecastWindDirection,
|
val forecastWindDirection = 0.0 + (0..360).random()
|
||||||
forecastWindGusts
|
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 =
|
val distancePerHour =
|
||||||
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
||||||
?.toDouble() ?: 0.0
|
?.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(
|
emit(
|
||||||
StreamData(
|
StreamData(
|
||||||
data,
|
WeatherDataResponse(provider = WeatherDataProvider.OPEN_METEO, data = data),
|
||||||
SettingsAndProfile(
|
SettingsAndProfile(
|
||||||
HeadwindSettings(),
|
HeadwindSettings(),
|
||||||
settingsAndProfile?.isImperial == true,
|
settingsAndProfile?.isImperial == true,
|
||||||
@ -182,7 +188,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
previewFlow(settingsAndProfileStream)
|
previewFlow(settingsAndProfileStream)
|
||||||
} else {
|
} else {
|
||||||
combine(
|
combine(
|
||||||
context.streamCurrentWeatherData(),
|
context.streamCurrentForecastWeatherData(),
|
||||||
settingsAndProfileStream,
|
settingsAndProfileStream,
|
||||||
context.streamWidgetSettings(),
|
context.streamWidgetSettings(),
|
||||||
karooSystem.getHeadingFlow(context),
|
karooSystem.getHeadingFlow(context),
|
||||||
@ -204,7 +210,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, headingResponse, upcomingRoute) ->
|
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, headingResponse, upcomingRoute) ->
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||||
|
|
||||||
if (allData.isNullOrEmpty()){
|
if (allData?.data.isNullOrEmpty()){
|
||||||
emitter.updateView(
|
emitter.updateView(
|
||||||
getErrorWidget(
|
getErrorWidget(
|
||||||
glance,
|
glance,
|
||||||
@ -223,42 +229,29 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
|
|
||||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
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 {
|
var previousDate: String? = let {
|
||||||
val unixTime =
|
val unixTime = allData?.data?.getOrNull(positionOffset)?.forecasts?.getOrNull(hourOffset)?.time
|
||||||
allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(
|
|
||||||
hourOffset
|
|
||||||
)
|
|
||||||
val formattedDate = unixTime?.let {
|
val formattedDate = unixTime?.let {
|
||||||
getShortDateFormatter().format(
|
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
Instant.ofEpochSecond(unixTime)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedDate
|
formattedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
for (baseIndex in hourOffset..hourOffset + 2) {
|
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 (allData?.data?.getOrNull(positionIndex) == null) break
|
||||||
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size
|
if (baseIndex >= (allData.data.getOrNull(positionOffset)?.forecasts?.size ?: 0)) break
|
||||||
?: 0)
|
|
||||||
) {
|
val data = allData.data.getOrNull(positionIndex)
|
||||||
break
|
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) {
|
if (baseIndex > hourOffset) {
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = GlanceModifier.fillMaxHeight().background(
|
modifier = GlanceModifier.fillMaxHeight().background(
|
||||||
@ -280,8 +273,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
val isCurrent = baseIndex == 0 && positionIndex == 0
|
val isCurrent = baseIndex == 0 && positionIndex == 0
|
||||||
|
|
||||||
if (isCurrent && data?.current != null) {
|
if (isCurrent && data?.current != null) {
|
||||||
val interpretation =
|
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||||
WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
|
||||||
val unixTime = data.current.time
|
val unixTime = data.current.time
|
||||||
val formattedTime =
|
val formattedTime =
|
||||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
@ -307,32 +299,22 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
|
|
||||||
previousDate = formattedDate
|
previousDate = formattedDate
|
||||||
} else {
|
} else {
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(
|
val weatherData = data?.forecasts?.getOrNull(baseIndex)
|
||||||
data?.forecastData?.weatherCode?.get(baseIndex) ?: 0
|
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||||
)
|
val unixTime = data?.forecasts?.getOrNull(baseIndex)?.time ?: 0
|
||||||
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
val formattedTime =
|
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val formattedDate =
|
|
||||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
|
||||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||||
|
|
||||||
RenderWidget(
|
RenderWidget(
|
||||||
arrowBitmap = baseBitmap,
|
arrowBitmap = baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = data?.forecastData?.windDirection?.get(baseIndex)
|
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
?.roundToInt() ?: 0,
|
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
||||||
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)
|
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
||||||
?.roundToInt() ?: 0,
|
precipitation = weatherData?.precipitation ?: 0.0,
|
||||||
windGusts = data?.forecastData?.windGusts?.get(baseIndex)
|
precipitationProbability = weatherData?.precipitationProbability?.toInt(),
|
||||||
?.roundToInt() ?: 0,
|
temperature = weatherData?.temperature?.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,
|
|
||||||
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
|
|||||||
@ -16,17 +16,15 @@ import androidx.glance.layout.Column
|
|||||||
import androidx.glance.layout.ContentScale
|
import androidx.glance.layout.ContentScale
|
||||||
import androidx.glance.layout.Row
|
import androidx.glance.layout.Row
|
||||||
import androidx.glance.layout.fillMaxHeight
|
import androidx.glance.layout.fillMaxHeight
|
||||||
import androidx.glance.layout.fillMaxWidth
|
|
||||||
import androidx.glance.layout.padding
|
import androidx.glance.layout.padding
|
||||||
import androidx.glance.layout.width
|
import androidx.glance.layout.width
|
||||||
import androidx.glance.layout.wrapContentWidth
|
import androidx.glance.layout.wrapContentWidth
|
||||||
import androidx.glance.text.FontFamily
|
import androidx.glance.text.FontFamily
|
||||||
import androidx.glance.text.FontWeight
|
import androidx.glance.text.FontWeight
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|||||||
@ -42,8 +42,8 @@ class HeadwindDirectionDataType(
|
|||||||
|
|
||||||
private fun streamValues(): Flow<Double> = flow {
|
private fun streamValues(): Flow<Double> = flow {
|
||||||
karooSystem.getRelativeHeadingFlow(applicationContext)
|
karooSystem.getRelativeHeadingFlow(applicationContext)
|
||||||
.combine(applicationContext.streamCurrentWeatherData()) { headingResponse, data ->
|
.combine(applicationContext.streamCurrentWeatherData(karooSystem)) { headingResponse, data ->
|
||||||
StreamData(headingResponse, data.firstOrNull()?.data?.current?.windDirection, data.firstOrNull()?.data?.current?.windSpeed)
|
StreamData(headingResponse, data?.windDirection, data?.windSpeed)
|
||||||
}
|
}
|
||||||
.combine(applicationContext.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
|
.combine(applicationContext.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
|
||||||
.collect { streamData ->
|
.collect { streamData ->
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
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.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
@ -16,7 +16,6 @@ import io.hammerhead.karooext.models.StreamState
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
|
||||||
@ -24,18 +23,17 @@ class HeadwindSpeedDataType(
|
|||||||
private val karooSystem: KarooSystemService,
|
private val karooSystem: KarooSystemService,
|
||||||
private val context: Context) : DataTypeImpl("karoo-headwind", "headwindSpeed"){
|
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>) {
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
karooSystem.getRelativeHeadingFlow(context)
|
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 ->
|
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
|
||||||
StreamData(value, data.firstOrNull()?.data, settings)
|
StreamData(value, data, settings)
|
||||||
}
|
}
|
||||||
.filter { it.weatherResponse != null }
|
|
||||||
.collect { streamData ->
|
.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 windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double {
|
||||||
return data.current.precipitation
|
return data.precipitation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextAlign
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double? {
|
||||||
return data.current.relativeHumidity.toDouble()
|
return data.relativeHumidity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double? {
|
||||||
return data.current.sealevelPressure ?: 0.0
|
return data.sealevelPressure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double? {
|
||||||
return data.current.surfacePressure
|
return data.surfacePressure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,11 +119,11 @@ class TailwindAndRideSpeedDataType(
|
|||||||
val flow = if (config.preview) {
|
val flow = if (config.preview) {
|
||||||
previewFlow(karooSystem.streamUserProfile())
|
previewFlow(karooSystem.streamUserProfile())
|
||||||
} else {
|
} 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 isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
val absoluteWindDirection = weatherData.firstOrNull()?.data?.current?.windDirection
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
val windSpeed = weatherData.firstOrNull()?.data?.current?.windSpeed
|
val windSpeed = weatherData?.windSpeed
|
||||||
val gustSpeed = weatherData.firstOrNull()?.data?.current?.windGusts
|
val gustSpeed = weatherData?.windGusts
|
||||||
val rideSpeed = if (isImperial){
|
val rideSpeed = if (isImperial){
|
||||||
rideSpeedInMs * 2.23694
|
rideSpeedInMs * 2.23694
|
||||||
} else {
|
} else {
|
||||||
@ -141,7 +141,7 @@ class TailwindAndRideSpeedDataType(
|
|||||||
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
||||||
|
|
||||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
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
|
var headingResponse = streamData.headingResponse
|
||||||
|
|
||||||
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
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.graphics.BitmapFactory
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.intl.Locale
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
@ -34,7 +33,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
@ -99,11 +97,11 @@ class TailwindDataType(
|
|||||||
val flow = if (config.preview) {
|
val flow = if (config.preview) {
|
||||||
previewFlow(karooSystem.streamUserProfile())
|
previewFlow(karooSystem.streamUserProfile())
|
||||||
} else {
|
} 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 isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
val absoluteWindDirection = weatherData.firstOrNull()?.data?.current?.windDirection
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
val windSpeed = weatherData.firstOrNull()?.data?.current?.windSpeed
|
val windSpeed = weatherData?.windSpeed
|
||||||
val gustSpeed = weatherData.firstOrNull()?.data?.current?.windGusts
|
val gustSpeed = weatherData?.windGusts
|
||||||
val rideSpeed = if (isImperial){
|
val rideSpeed = if (isImperial){
|
||||||
rideSpeedInMs * 2.23694
|
rideSpeedInMs * 2.23694
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double {
|
||||||
return data.current.temperature
|
return data.temperature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextAlign
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
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.WindDirectionIndicatorTextSetting
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
@ -28,18 +28,18 @@ class UserWindSpeedDataType(
|
|||||||
private val context: Context
|
private val context: Context
|
||||||
) : DataTypeImpl("karoo-headwind", "userwindSpeed"){
|
) : 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 {
|
companion object {
|
||||||
fun streamValues(context: Context, karooSystem: KarooSystemService): Flow<Double> = flow {
|
fun streamValues(context: Context, karooSystem: KarooSystemService): Flow<Double> = flow {
|
||||||
karooSystem.getRelativeHeadingFlow(context)
|
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 ->
|
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
|
||||||
StreamData(value, data.firstOrNull()?.data, settings)
|
StreamData(value, data, settings)
|
||||||
}
|
}
|
||||||
.filter { it.weatherResponse != null }
|
.filter { it.weatherResponse != null }
|
||||||
.collect { streamData ->
|
.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
|
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||||
|
|
||||||
if (streamData.settings.windDirectionIndicatorTextSetting == WindDirectionIndicatorTextSetting.HEADWIND_SPEED){
|
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.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.MainActivity
|
import de.timklge.karooheadwind.MainActivity
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
|
||||||
import de.timklge.karooheadwind.OpenMeteoData
|
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
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.getHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
@ -61,12 +60,12 @@ class WeatherDataType(
|
|||||||
|
|
||||||
override fun startStream(emitter: Emitter<StreamState>) {
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val currentWeatherData = applicationContext.streamCurrentWeatherData()
|
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystem)
|
||||||
|
|
||||||
currentWeatherData
|
currentWeatherData
|
||||||
.collect { data ->
|
.collect { data ->
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.firstOrNull()?.data?.current?.weatherCode}")
|
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.weatherCode}")
|
||||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data.firstOrNull()?.data?.current?.weatherCode?.toDouble() ?: 0.0)))))
|
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.weatherCode?.toDouble() ?: 0.0)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emitter.setCancellable {
|
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)
|
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
|
||||||
|
|
||||||
private fun previewFlow(): Flow<StreamData> = flow {
|
private fun previewFlow(): Flow<StreamData> = flow {
|
||||||
while (true){
|
while (true){
|
||||||
emit(StreamData(
|
emit(StreamData(
|
||||||
OpenMeteoCurrentWeatherResponse(
|
WeatherData(Instant.now().epochSecond, 0.0,
|
||||||
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 980.0, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
|
20.0, 50.0, 3.0, 0.0, 1013.25, 980.0, 15.0, 30.0, 30.0,
|
||||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false), HeadwindSettings()))
|
||||||
|
|
||||||
null
|
|
||||||
), HeadwindSettings()
|
|
||||||
))
|
|
||||||
|
|
||||||
delay(5_000)
|
delay(5_000)
|
||||||
}
|
}
|
||||||
@ -107,8 +102,8 @@ class WeatherDataType(
|
|||||||
val dataFlow = if (config.preview){
|
val dataFlow = if (config.preview){
|
||||||
previewFlow()
|
previewFlow()
|
||||||
} else {
|
} else {
|
||||||
combine(context.streamCurrentWeatherData(), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), karooSystem.getHeadingFlow(context)) { data, settings, profile, heading ->
|
combine(context.streamCurrentWeatherData(karooSystem), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), karooSystem.getHeadingFlow(context)) { data, settings, profile, heading ->
|
||||||
StreamData(data.firstOrNull()?.data, settings, profile, heading)
|
StreamData(data, settings, profile, heading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +119,9 @@ class WeatherDataType(
|
|||||||
return@collect
|
return@collect
|
||||||
}
|
}
|
||||||
|
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
val interpretation = WeatherInterpretation.fromWeatherCode(data.weatherCode)
|
||||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time))
|
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.time))
|
||||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.current.time))
|
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.time))
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
var modifier = GlanceModifier.fillMaxSize()
|
var modifier = GlanceModifier.fillMaxSize()
|
||||||
@ -136,12 +131,12 @@ class WeatherDataType(
|
|||||||
Weather(
|
Weather(
|
||||||
baseBitmap,
|
baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = data.current.windDirection.roundToInt(),
|
windBearing = data.windDirection.roundToInt(),
|
||||||
windSpeed = data.current.windSpeed.roundToInt(),
|
windSpeed = data.windSpeed.roundToInt(),
|
||||||
windGusts = data.current.windGusts.roundToInt(),
|
windGusts = data.windGusts.roundToInt(),
|
||||||
precipitation = data.current.precipitation,
|
precipitation = data.precipitation,
|
||||||
precipitationProbability = null,
|
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,
|
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
rowAlignment = when (config.alignment){
|
rowAlignment = when (config.alignment){
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
|
||||||
class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "weatherForecast") {
|
class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "weatherForecast") {
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import androidx.glance.text.TextAlign
|
|||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import androidx.glance.text.FontFamily
|
|||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
import de.timklge.karooheadwind.streamDataFlow
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.internal.ViewEmitter
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.mapNotNull
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
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)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
private val glance = GlanceRemoteViews()
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ class WindDirectionDataType(val karooSystem: KarooSystemService, context: Contex
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double {
|
||||||
return data.current.windDirection
|
return data.windDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun previewFlow(): Flow<Double> {
|
private fun previewFlow(): Flow<Double> {
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import androidx.glance.layout.ContentScale
|
|||||||
import androidx.glance.layout.Row
|
import androidx.glance.layout.Row
|
||||||
import androidx.glance.layout.fillMaxHeight
|
import androidx.glance.layout.fillMaxHeight
|
||||||
import androidx.glance.layout.fillMaxWidth
|
import androidx.glance.layout.fillMaxWidth
|
||||||
import androidx.glance.layout.height
|
|
||||||
import androidx.glance.layout.padding
|
import androidx.glance.layout.padding
|
||||||
import androidx.glance.layout.width
|
import androidx.glance.layout.width
|
||||||
import androidx.glance.text.FontFamily
|
import androidx.glance.text.FontFamily
|
||||||
@ -26,10 +25,9 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextAlign
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WindForecast(
|
fun WindForecast(
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double {
|
||||||
return data.current.windGusts
|
return data.windGusts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
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"){
|
class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){
|
||||||
override fun getValue(data: OpenMeteoCurrentWeatherResponse): Double {
|
override fun getValue(data: WeatherData): Double {
|
||||||
return data.current.windSpeed
|
return data.windSpeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package de.timklge.karooheadwind.screens
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -25,15 +24,15 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.timklge.karooheadwind.HeadwindStats
|
import de.timklge.karooheadwind.HeadwindStats
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.ServiceStatusSingleton
|
import de.timklge.karooheadwind.ServiceStatusSingleton
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
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.ForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||||
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
||||||
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
||||||
|
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamStats
|
import de.timklge.karooheadwind.streamStats
|
||||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
@ -50,12 +49,28 @@ import kotlin.math.roundToInt
|
|||||||
fun WeatherScreen(onFinish: () -> Unit) {
|
fun WeatherScreen(onFinish: () -> Unit) {
|
||||||
var karooConnected by remember { mutableStateOf<Boolean?>(null) }
|
var karooConnected by remember { mutableStateOf<Boolean?>(null) }
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
val karooSystem = remember { KarooSystemService(ctx) }
|
val karooSystem = remember { KarooSystemService(ctx) }
|
||||||
|
|
||||||
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
val profileFlow = remember { karooSystem.streamUserProfile() }
|
||||||
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
|
val profile by profileFlow.collectAsStateWithLifecycle(null)
|
||||||
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
|
|
||||||
val weatherData by ctx.streamCurrentWeatherData().collectAsStateWithLifecycle(emptyList())
|
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(
|
val baseBitmap = BitmapFactory.decodeResource(
|
||||||
ctx.resources,
|
ctx.resources,
|
||||||
@ -85,21 +100,20 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentWeatherData = weatherData.firstOrNull()?.data
|
val requestedWeatherPosition = forecastData?.data?.firstOrNull()?.coords
|
||||||
val requestedWeatherPosition = weatherData.firstOrNull()?.requestedPosition
|
|
||||||
|
|
||||||
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
|
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(it.time)) }
|
||||||
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
|
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(it.time)) }
|
||||||
|
|
||||||
if (karooConnected == true && currentWeatherData != null) {
|
if (karooConnected == true && currentWeatherData != null) {
|
||||||
WeatherWidget(
|
WeatherWidget(
|
||||||
baseBitmap = baseBitmap,
|
baseBitmap = baseBitmap,
|
||||||
current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode),
|
current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode),
|
||||||
windBearing = currentWeatherData.current.windDirection.roundToInt(),
|
windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
windSpeed = currentWeatherData.current.windSpeed.roundToInt(),
|
windSpeed = currentWeatherData?.windSpeed?.roundToInt() ?: 0,
|
||||||
windGusts = currentWeatherData.current.windGusts.roundToInt(),
|
windGusts = currentWeatherData?.windGusts?.roundToInt() ?: 0,
|
||||||
precipitation = currentWeatherData.current.precipitation,
|
precipitation = currentWeatherData?.precipitation ?: 0.0,
|
||||||
temperature = currentWeatherData.current.temperature.toInt(),
|
temperature = currentWeatherData?.temperature?.toInt() ?: 0,
|
||||||
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
dateLabel = formattedDate,
|
dateLabel = formattedDate,
|
||||||
@ -113,8 +127,6 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
val lastPositionDistanceStr =
|
val lastPositionDistanceStr =
|
||||||
lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
||||||
|
|
||||||
val serviceStatus by ServiceStatusSingleton.getInstance().getServiceStatus().collectAsStateWithLifecycle(false)
|
|
||||||
|
|
||||||
if (!serviceStatus){
|
if (!serviceStatus){
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(5.dp),
|
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){
|
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 (forecastData?.data?.getOrNull(positionIndex) == null) break
|
||||||
if (index >= (weatherData.getOrNull(positionIndex)?.data?.forecastData?.weatherCode?.size ?: 0)) {
|
if (index >= (forecastData?.data?.getOrNull(positionIndex)?.forecasts?.size ?: 0)) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val data = weatherData.getOrNull(positionIndex)?.data
|
val data = forecastData?.data?.getOrNull(positionIndex)
|
||||||
val distanceAlongRoute = weatherData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
val distanceAlongRoute = forecastData?.data?.getOrNull(positionIndex)?.coords?.distanceAlongRoute
|
||||||
val position = weatherData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
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) {
|
if (index > 1) {
|
||||||
Spacer(
|
Spacer(
|
||||||
@ -209,29 +219,30 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
|
val weatherData = data?.forecasts?.getOrNull(index)
|
||||||
val unixTime = data?.forecastData?.time?.get(index) ?: 0
|
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||||
|
val unixTime = weatherData?.time ?: 0
|
||||||
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||||
|
|
||||||
WeatherWidget(
|
WeatherWidget(
|
||||||
baseBitmap,
|
baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = data?.forecastData?.windDirection?.get(index)?.roundToInt() ?: 0,
|
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
windSpeed = data?.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0,
|
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
||||||
windGusts = data?.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0,
|
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
||||||
precipitation = data?.forecastData?.precipitation?.get(index) ?: 0.0,
|
precipitation = weatherData?.precipitation ?: 0.0,
|
||||||
temperature = data?.forecastData?.temperature?.get(index)?.roundToInt() ?: 0,
|
temperature = weatherData?.temperature?.toInt() ?: 0,
|
||||||
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedForecastTime,
|
timeLabel = formattedForecastTime,
|
||||||
dateLabel = formattedForecastDate,
|
dateLabel = formattedForecastDate,
|
||||||
distance = distanceFromCurrent,
|
distance = distanceFromCurrent,
|
||||||
includeDistanceLabel = true,
|
includeDistanceLabel = true,
|
||||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0,
|
precipitationProbability = weatherData?.precipitationProbability?.toInt() ?: 0,
|
||||||
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.padding(30.dp))
|
Spacer(modifier = Modifier.padding(30.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
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.getArrowBitmapByBearing
|
||||||
import de.timklge.karooheadwind.datatypes.getWeatherIcon
|
import de.timklge.karooheadwind.datatypes.getWeatherIcon
|
||||||
import kotlin.math.absoluteValue
|
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 distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
val text = if (includeDistanceLabel){
|
val text = if (includeDistanceLabel){
|
||||||
@ -154,7 +154,7 @@ fun WeatherWidget(
|
|||||||
modifier = Modifier.padding(top = 4.dp)
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(),
|
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing + 180).asImageBitmap(),
|
||||||
colorFilter = ColorFilter.tint(Color.Black),
|
colorFilter = ColorFilter.tint(Color.Black),
|
||||||
contentDescription = "Wind direction",
|
contentDescription = "Wind direction",
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package de.timklge.karooheadwind
|
package de.timklge.karooheadwind.util
|
||||||
|
|
||||||
import kotlin.math.abs
|
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 de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.HttpResponseState
|
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
interface WeatherProvider {
|
interface WeatherProvider {
|
||||||
@ -11,5 +11,5 @@ interface WeatherProvider {
|
|||||||
coordinates: List<GpsCoordinates>,
|
coordinates: List<GpsCoordinates>,
|
||||||
settings: HeadwindSettings,
|
settings: HeadwindSettings,
|
||||||
profile: UserProfile?
|
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="green">#00ff00</color>
|
||||||
<color name="orange">#ff9930</color>
|
<color name="orange">#ff9930</color>
|
||||||
<color name="red">#FF2424</color>
|
<color name="red">#FF5454</color>
|
||||||
|
|
||||||
<color name="hGreen">#008000</color>
|
<color name="hGreen">#008000</color>
|
||||||
<color name="hOrange">#BB4300</color>
|
<color name="hOrange">#BB4300</color>
|
||||||
<color name="hRed">#B30000</color>
|
<color name="hRed">#A30000</color>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
x
Reference in New Issue
Block a user