fix #15: Replace precipitation and temperature unit settings with location rounding setting

This commit is contained in:
Tim Kluge 2024-12-19 22:32:10 +01:00
parent 9624b7ec7d
commit d1cf6aa6d6
8 changed files with 67 additions and 49 deletions

View File

@ -33,7 +33,7 @@ If you are using a Karoo 2, you can use manual sideloading:
After installing this app on your Karoo and opening it once from the main menu, you can add the following new data fields to your data pages:
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction.
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h. You can change this behavior in the app settings to show the absolute wind speed instead.
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast.
- Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired.

View File

@ -15,8 +15,8 @@ android {
applicationId = "de.timklge.karooheadwind"
minSdk = 26
targetSdk = 35
versionCode = 4
versionName = "1.1"
versionCode = 5
versionName = "1.1.1"
}
signingConfigs {

View File

@ -3,8 +3,8 @@
"packageName": "de.timklge.karooheadwind",
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
"latestVersion": "1.1",
"latestVersionCode": 4,
"latestVersion": "1.1.1",
"latestVersionCode": 5,
"developer": "timklge",
"description": "Provides headwind direction, wind speed and other weather data fields",
"releaseNotes": "Add hourly forecast and temperature datafields"

View File

@ -122,12 +122,9 @@ fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<Headwin
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
val preferredMetric = preferredUnits.distance == UserProfile.PreferredUnit.UnitType.METRIC
defaultSettings.copy(
windUnit = if (preferredUnits.distance == UserProfile.PreferredUnit.UnitType.METRIC) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR,
precipitationUnit = if (preferredMetric) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
temperatureUnit = if (preferredUnits.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT
)
}
} catch(e: Throwable){
@ -162,10 +159,13 @@ fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
}
@OptIn(FlowPreview::class)
suspend fun KarooSystemService.makeOpenMeteoHttpRequest(gpsCoordinates: GpsCoordinates, settings: HeadwindSettings): HttpResponseState.Complete {
suspend fun KarooSystemService.makeOpenMeteoHttpRequest(gpsCoordinates: 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&current=surface_pressure,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 url = "https://api.open-meteo.com/v1/forecast?latitude=${gpsCoordinates.lat}&longitude=${gpsCoordinates.lon}&current=surface_pressure,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=${settings.precipitationUnit.id}&temperature_unit=${settings.temperatureUnit.id}"
val url = "https://api.open-meteo.com/v1/forecast?latitude=${gpsCoordinates.lat}&longitude=${gpsCoordinates.lon}&current=surface_pressure,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}...")
@ -246,7 +246,7 @@ fun KarooSystemService.getHeadingFlow(): Flow<Double> {
}
@OptIn(FlowPreview::class)
fun KarooSystemService.getGpsCoordinateFlow(): Flow<GpsCoordinates> {
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates> {
// return flowOf(GpsCoordinates(52.5164069,13.3784))
return streamDataFlow(DataType.Type.LOCATION)
@ -263,7 +263,12 @@ fun KarooSystemService.getGpsCoordinateFlow(): Flow<GpsCoordinates> {
null
}
}
.map { it.round() }
.combine(context.streamSettings(this)) { gps, settings -> gps to settings }
.map { (gps, settings) ->
val rounded = gps.round(settings.roundLocationTo.km.toDouble())
Log.d(KarooHeadwindExtension.TAG, "Round location to ${settings.roundLocationTo.km} - $rounded")
rounded
}
.distinctUntilChanged { old, new -> old.distanceTo(new).absoluteValue < 0.001 }
.debounce(Duration.ofSeconds(10))
}

View File

@ -14,9 +14,12 @@ import de.timklge.karooheadwind.datatypes.TemperatureDataType
import de.timklge.karooheadwind.datatypes.WeatherDataType
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -32,7 +35,7 @@ import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1") {
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.1") {
companion object {
const val TAG = "karoo-headwind"
}
@ -59,6 +62,9 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1") {
)
}
data class StreamData(val settings: HeadwindSettings, val gps: GpsCoordinates,
val profile: UserProfile? = null)
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
@ -73,7 +79,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1") {
}
val gpsFlow = karooSystem
.getGpsCoordinateFlow()
.getGpsCoordinateFlow(this@KarooHeadwindExtension)
.transformLatest { value: GpsCoordinates ->
while(true){
emit(value)
@ -83,8 +89,9 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1") {
streamSettings(karooSystem)
.filter { it.welcomeDialogAccepted }
.combine(gpsFlow) { settings, gps -> settings to gps }
.map { (settings, gps) ->
.combine(gpsFlow) { settings, gps -> StreamData(settings, gps) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.map { (settings, gps, profile) ->
Log.d(TAG, "Acquired updated gps coordinates: $gps")
val lastKnownStats = try {
@ -94,7 +101,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1") {
HeadwindStats()
}
val response = karooSystem.makeOpenMeteoHttpRequest(gps, settings)
val response = karooSystem.makeOpenMeteoHttpRequest(gps, settings, profile)
if (response.error != null){
try {
val stats = lastKnownStats.copy(failedWeatherRequest = System.currentTimeMillis())

View File

@ -14,8 +14,11 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl
import io.hammerhead.karooext.internal.Emitter
@ -24,6 +27,7 @@ import io.hammerhead.karooext.models.DataPoint
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -75,17 +79,19 @@ class WeatherDataType(
de.timklge.karooheadwind.R.drawable.arrow_0
)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings,
val profile: UserProfile? = null)
val viewJob = CoroutineScope(Dispatchers.IO).launch {
context.streamCurrentWeatherData()
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.onCompletion {
// Clear view on completion
val result = glance.compose(context, DpSize.Unspecified) { }
emitter.updateView(result.remoteViews)
}
.collect { (data, settings) ->
.collect { (data, settings, userProfile) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view")
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time))
@ -100,9 +106,9 @@ class WeatherDataType(
windSpeedUnit = settings.windUnit,
precipitation = data.current.precipitation,
precipitationProbability = null,
precipitationUnit = settings.precipitationUnit,
precipitationUnit = if (userProfile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
temperature = data.current.temperature.roundToInt(),
temperatureUnit = settings.temperatureUnit,
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedTime
)
}

View File

@ -34,8 +34,11 @@ import de.timklge.karooheadwind.saveSettings
import de.timklge.karooheadwind.saveWidgetSettings
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.streamWidgetSettings
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl
@ -45,6 +48,7 @@ import io.hammerhead.karooext.models.DataPoint
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -118,18 +122,20 @@ class WeatherForecastDataType(
de.timklge.karooheadwind.R.drawable.arrow_0
)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings, val widgetSettings: HeadwindWidgetSettings? = null)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings,
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null)
val viewJob = CoroutineScope(Dispatchers.IO).launch {
context.streamCurrentWeatherData()
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.combine(context.streamWidgetSettings()) { data, widgetSettings -> data.copy(widgetSettings = widgetSettings) }
.onCompletion {
// Clear view on completion
val result = glance.compose(context, DpSize.Unspecified) { }
emitter.updateView(result.remoteViews)
}
.collect { (data, settings, widgetSettings) ->
.collect { (data, settings, widgetSettings, userProfile) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view")
val result = glance.compose(context, DpSize.Unspecified) {
@ -161,9 +167,9 @@ class WeatherForecastDataType(
windSpeedUnit = settings.windUnit,
precipitation = data.forecastData.precipitation[index],
precipitationProbability = data.forecastData.precipitationProbability[index],
precipitationUnit = settings.precipitationUnit,
precipitationUnit = if (userProfile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
temperature = data.forecastData.temperature[index].roundToInt(),
temperatureUnit = settings.temperatureUnit,
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedTime
)
}

View File

@ -71,13 +71,18 @@ enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: S
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
}
enum class RoundLocationSetting(val id: String, val label: String, val km: Int){
KM_1("1 km", "1 km", 1),
KM_2("2 km", "2 km", 2),
KM_5("5 km", "5 km", 5)
}
@Serializable
data class HeadwindSettings(
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
val precipitationUnit: PrecipitationUnit = PrecipitationUnit.MILLIMETERS,
val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS,
val welcomeDialogAccepted: Boolean = false,
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_2
){
companion object {
val defaultSettings = Json.encodeToString(HeadwindSettings())
@ -113,23 +118,21 @@ fun MainScreen() {
val karooSystem = remember { KarooSystemService(ctx) }
var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
var selectedPrecipitationUnit by remember { mutableStateOf(PrecipitationUnit.MILLIMETERS) }
var selectedTemperatureUnit by remember { mutableStateOf(TemperatureUnit.CELSIUS) }
var welcomeDialogVisible by remember { mutableStateOf(false) }
var selectedWindDirectionIndicatorTextSetting by remember { mutableStateOf(WindDirectionIndicatorTextSetting.HEADWIND_SPEED) }
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_2) }
val stats by ctx.streamStats().collectAsState(HeadwindStats())
val location by karooSystem.getGpsCoordinateFlow().collectAsState(initial = null)
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsState(initial = null)
var savedDialogVisible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
ctx.streamSettings(karooSystem).collect { settings ->
selectedWindUnit = settings.windUnit
selectedPrecipitationUnit = settings.precipitationUnit
welcomeDialogVisible = !settings.welcomeDialogAccepted
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
selectedTemperatureUnit = settings.temperatureUnit
selectedRoundLocationSetting = settings.roundLocationTo
}
}
@ -164,28 +167,21 @@ fun MainScreen() {
selectedWindUnit = WindUnit.entries.find { unit -> unit.id == selectedOption.id }!!
}
val precipitationUnitDropdownOptions = PrecipitationUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val precipitationUnitInitialSelection by remember(selectedPrecipitationUnit) {
mutableStateOf(precipitationUnitDropdownOptions.find { option -> option.id == selectedPrecipitationUnit.id }!!)
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
mutableStateOf(roundLocationDropdownOptions.find { option -> option.id == selectedRoundLocationSetting.id }!!)
}
Dropdown(label = "Precipitation Unit", options = precipitationUnitDropdownOptions, selected = precipitationUnitInitialSelection) { selectedOption ->
selectedPrecipitationUnit = PrecipitationUnit.entries.find { unit -> unit.id == selectedOption.id }!!
Dropdown(label = "Round Location", options = roundLocationDropdownOptions, selected = roundLocationInitialSelection) { selectedOption ->
selectedRoundLocationSetting = RoundLocationSetting.entries.find { unit -> unit.id == selectedOption.id }!!
}
val temperatureUnitDropdownOptions = TemperatureUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val temperatureUnitInitialSelection by remember(selectedTemperatureUnit) {
mutableStateOf(temperatureUnitDropdownOptions.find { option -> option.id == selectedTemperatureUnit.id }!!)
}
Dropdown(label = "Temperature Unit", options = temperatureUnitDropdownOptions, selected = temperatureUnitInitialSelection) { selectedOption ->
selectedTemperatureUnit = TemperatureUnit.entries.find { unit -> unit.id == selectedOption.id }!!
}
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
val newSettings = HeadwindSettings(windUnit = selectedWindUnit, precipitationUnit = selectedPrecipitationUnit,
temperatureUnit = selectedTemperatureUnit,
welcomeDialogAccepted = true, windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting)
val newSettings = HeadwindSettings(windUnit = selectedWindUnit,
welcomeDialogAccepted = true, windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
roundLocationTo = selectedRoundLocationSetting)
coroutineScope.launch {
saveSettings(ctx, newSettings)
@ -237,8 +233,6 @@ fun MainScreen() {
confirmButton = { Button(onClick = {
coroutineScope.launch {
saveSettings(ctx, HeadwindSettings(windUnit = selectedWindUnit,
precipitationUnit = selectedPrecipitationUnit,
temperatureUnit = selectedTemperatureUnit,
welcomeDialogAccepted = true))
}
}) { Text("OK") } },