Show no gps, no weather data error messages in visual data fields

This commit is contained in:
Tim Kluge 2024-12-31 13:37:38 +01:00
parent 543bcaf478
commit 407755e94b
9 changed files with 207 additions and 83 deletions

View File

@ -15,8 +15,8 @@ android {
applicationId = "de.timklge.karooheadwind" applicationId = "de.timklge.karooheadwind"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 6 versionCode = 7
versionName = "1.1.2" versionName = "1.1.3"
} }
signingConfigs { signingConfigs {

View File

@ -3,9 +3,9 @@
"packageName": "de.timklge.karooheadwind", "packageName": "de.timklge.karooheadwind",
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png", "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", "latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
"latestVersion": "1.1.2", "latestVersion": "1.1.3",
"latestVersionCode": 6, "latestVersionCode": 7,
"developer": "timklge", "developer": "timklge",
"description": "Provides headwind direction, wind speed and other weather data fields", "description": "Provides headwind direction, wind speed and other weather data fields",
"releaseNotes": "Add hourly forecast and temperature datafields. Add setting to use absolute wind direction on headwind datafield." "releaseNotes": "Add hourly forecast and temperature datafields. Show error message in fields if no weather data or gps is available."
} }

View File

@ -2,8 +2,25 @@ package de.timklge.karooheadwind
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
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 androidx.glance.GlanceModifier
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import androidx.glance.appwidget.RemoteViewsCompositionResult
import androidx.glance.color.ColorProvider
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats import de.timklge.karooheadwind.screens.HeadwindStats
@ -28,6 +45,7 @@ import kotlinx.coroutines.flow.catch
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.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -86,7 +104,30 @@ fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
} }
} }
fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse> { @OptIn(ExperimentalGlanceRemoteViewsApi::class)
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
return glance.compose(context, DpSize.Unspecified) {
Box(modifier = GlanceModifier.fillMaxSize().padding(5.dp), contentAlignment = Alignment.Center) {
val errorMessage = if (settings?.welcomeDialogAccepted == false) {
"Headwind app not set up"
} else if (headingResponse is HeadingResponse.NoGps){
"No GPS signal"
} else if (headingResponse is HeadingResponse.NoWeatherData) {
"No weather data"
} else {
"Unknown error"
}
Log.d(KarooHeadwindExtension.TAG, "Error widget: $errorMessage")
Text(text = errorMessage, style = TextStyle(fontSize = TextUnit(16f, TextUnitType.Sp),
textAlign = TextAlign.Center,
color = ColorProvider(Color.Black, Color.White)))
}
}
}
fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse?> {
return dataStore.data.map { settingsJson -> return dataStore.data.map { settingsJson ->
try { try {
val data = settingsJson[currentDataKey] val data = settingsJson[currentDataKey]
@ -95,7 +136,13 @@ fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse> {
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e) Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
null null
} }
}.filterNotNull().distinctUntilChanged().filter { it.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12) } }.distinctUntilChanged().map { response ->
if (response != null && response.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12)){
response
} else {
null
}
}
} }
fun Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> { fun Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> {
@ -212,63 +259,95 @@ fun signedAngleDifference(angle1: Double, angle2: Double): Double {
return sign * diff return sign * diff
} }
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<Double> { sealed class HeadingResponse {
data object NoGps: HeadingResponse()
data object NoWeatherData: HeadingResponse()
data class Value(val diff: Double): HeadingResponse()
}
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> {
val currentWeatherData = context.streamCurrentWeatherData() val currentWeatherData = context.streamCurrentWeatherData()
return getHeadingFlow() return getHeadingFlow()
.filter { it >= 0 }
.combine(currentWeatherData) { bearing, data -> bearing to data } .combine(currentWeatherData) { bearing, data -> bearing to data }
.map { (bearing, data) -> .map { (bearing, data) ->
val windBearing = data.current.windDirection + 180 when {
val diff = signedAngleDifference(bearing, windBearing) bearing is HeadingResponse.Value && data != null -> {
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff") val windBearing = data.current.windDirection + 180
val diff = signedAngleDifference(bearing.diff, windBearing)
diff Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff")
HeadingResponse.Value(diff)
}
bearing is HeadingResponse.NoGps -> HeadingResponse.NoGps
bearing is HeadingResponse.NoWeatherData || data == null -> HeadingResponse.NoWeatherData
else -> bearing
}
} }
} }
fun KarooSystemService.getHeadingFlow(): Flow<Double> { fun KarooSystemService.getHeadingFlow(): Flow<HeadingResponse> {
// return flowOf(20.0) // return flowOf(HeadingResponse.Value(20.0))
return streamDataFlow(DataType.Type.LOCATION) return streamDataFlow(DataType.Type.LOCATION)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.values } .map { (it as? StreamState.Streaming)?.dataPoint?.values }
.map { values -> .map { values ->
val heading = values[DataType.Field.LOC_BEARING] val heading = values?.get(DataType.Field.LOC_BEARING)
Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading") Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading")
heading ?: 0.0 val headingValue = heading?.let { HeadingResponse.Value(it) }
headingValue ?: HeadingResponse.NoGps
} }
.distinctUntilChanged() .distinctUntilChanged()
.scan(emptyList<Double>()) { acc, value -> /* Average over 3 values */ .scan(emptyList<HeadingResponse>()) { acc, value -> /* Average over 3 values */
if (value !is HeadingResponse.Value) return@scan listOf(value)
val newAcc = acc + value val newAcc = acc + value
if (newAcc.size > 3) newAcc.drop(1) else newAcc if (newAcc.size > 3) newAcc.drop(1) else newAcc
} }
.map { it.average() } .map { data ->
if (data.isEmpty()) return@map HeadingResponse.NoGps
if (data.all { it is HeadingResponse.Value }) {
val avg = data.mapNotNull { (it as? HeadingResponse.Value)?.diff }.average()
HeadingResponse.Value(avg)
} else {
data.first()
}
}
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates> { fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
// return flowOf(GpsCoordinates(52.5164069,13.3784)) // return flowOf(GpsCoordinates(52.5164069,13.3784))
return streamDataFlow(DataType.Type.LOCATION) return streamDataFlow(DataType.Type.LOCATION)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.values } .map { (it as? StreamState.Streaming)?.dataPoint?.values }
.mapNotNull { values -> .map { values ->
val lat = values[DataType.Field.LOC_LATITUDE] val lat = values?.get(DataType.Field.LOC_LATITUDE)
val lon = values[DataType.Field.LOC_LONGITUDE] val lon = values?.get(DataType.Field.LOC_LONGITUDE)
if (lat != null && lon != null){ if (lat != null && lon != null){
Log.d(KarooHeadwindExtension.TAG, "Updated gps coords: $lat $lon") Log.d(KarooHeadwindExtension.TAG, "Updated gps coordinates: $lat $lon")
GpsCoordinates(lat, lon) GpsCoordinates(lat, lon)
} else { } else {
Log.e(KarooHeadwindExtension.TAG, "Missing gps values: $values") Log.w(KarooHeadwindExtension.TAG, "Gps unavailable: $values")
null null
} }
} }
.combine(context.streamSettings(this)) { gps, settings -> gps to settings } .combine(context.streamSettings(this)) { gps, settings -> gps to settings }
.map { (gps, settings) -> .map { (gps, settings) ->
val rounded = gps.round(settings.roundLocationTo.km.toDouble()) val rounded = gps?.round(settings.roundLocationTo.km.toDouble())
Log.d(KarooHeadwindExtension.TAG, "Round location to ${settings.roundLocationTo.km} - $rounded") if (rounded != null) Log.d(KarooHeadwindExtension.TAG, "Round location to ${settings.roundLocationTo.km} - $rounded")
rounded rounded
} }
.distinctUntilChanged { old, new -> old.distanceTo(new).absoluteValue < 0.001 } .distinctUntilChanged { old, new ->
if (old != null && new != null) {
old.distanceTo(new).absoluteValue < 0.001
} else {
old == new
}
}
.debounce(Duration.ofSeconds(10)) .debounce(Duration.ofSeconds(10))
} }

View File

@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") { class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") {
companion object { companion object {
const val TAG = "karoo-headwind" const val TAG = "karoo-headwind"
} }
@ -62,7 +62,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
) )
} }
data class StreamData(val settings: HeadwindSettings, val gps: GpsCoordinates, data class StreamData(val settings: HeadwindSettings, val gps: GpsCoordinates?,
val profile: UserProfile? = null) val profile: UserProfile? = null)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -80,7 +80,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
val gpsFlow = karooSystem val gpsFlow = karooSystem
.getGpsCoordinateFlow(this@KarooHeadwindExtension) .getGpsCoordinateFlow(this@KarooHeadwindExtension)
.transformLatest { value: GpsCoordinates -> .transformLatest { value: GpsCoordinates? ->
while(true){ while(true){
emit(value) emit(value)
delay(1.hours) delay(1.hours)
@ -101,6 +101,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
HeadwindStats() HeadwindStats()
} }
if (gps == null){
error("No GPS coordinates available")
}
val response = karooSystem.makeOpenMeteoHttpRequest(gps, settings, profile) val response = karooSystem.makeOpenMeteoHttpRequest(gps, settings, profile)
if (response.error != null){ if (response.error != null){
try { try {

View File

@ -12,6 +12,7 @@ import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState 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.filterNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
abstract class BaseDataType( abstract class BaseDataType(
@ -25,10 +26,12 @@ abstract class BaseDataType(
val job = CoroutineScope(Dispatchers.IO).launch { val job = CoroutineScope(Dispatchers.IO).launch {
val currentWeatherData = applicationContext.streamCurrentWeatherData() val currentWeatherData = applicationContext.streamCurrentWeatherData()
currentWeatherData.collect { data -> currentWeatherData
val value = getValue(data) .filterNotNull()
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value") .collect { data ->
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value)))) val value = getValue(data)
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
} }
} }
emitter.setCancellable { emitter.setCancellable {

View File

@ -6,7 +6,9 @@ import android.util.Log
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.getErrorWidget
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
@ -47,7 +49,8 @@ class HeadwindDirectionDataType(
val job = CoroutineScope(Dispatchers.IO).launch { val job = CoroutineScope(Dispatchers.IO).launch {
karooSystem.getRelativeHeadingFlow(applicationContext) karooSystem.getRelativeHeadingFlow(applicationContext)
.collect { diff -> .collect { diff ->
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to diff)))) val value = (diff as? HeadingResponse.Value)?.diff ?: 0.0
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
} }
} }
emitter.setCancellable { emitter.setCancellable {
@ -55,7 +58,7 @@ class HeadwindDirectionDataType(
} }
} }
data class StreamData(val value: Double, val absoluteWindDirection: Double, val windSpeed: Double, val settings: HeadwindSettings) data class StreamData(val headingResponse: HeadingResponse?, val absoluteWindDirection: Double?, val windSpeed: Double?, val settings: HeadwindSettings? = null)
private fun previewFlow(): Flow<StreamData> { private fun previewFlow(): Flow<StreamData> {
return flow { return flow {
@ -63,7 +66,7 @@ class HeadwindDirectionDataType(
val bearing = (0..360).random().toDouble() val bearing = (0..360).random().toDouble()
val windSpeed = (-20..20).random() val windSpeed = (-20..20).random()
emit(StreamData(bearing, bearing, windSpeed.toDouble(), HeadwindSettings())) emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings()))
delay(2_000) delay(2_000)
} }
} }
@ -86,36 +89,47 @@ class HeadwindDirectionDataType(
val flow = if (config.preview) { val flow = if (config.preview) {
previewFlow() previewFlow()
} else { } else {
karooSystem.streamDataFlow(dataTypeId) karooSystem.getRelativeHeadingFlow(context)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .combine(context.streamCurrentWeatherData()) { headingResponse, data -> StreamData(headingResponse, data?.current?.windDirection, data?.current?.windSpeed) }
.combine(context.streamCurrentWeatherData()) { value, data -> value to data } .combine(context.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
StreamData(value, data.current.windDirection, data.current.windSpeed, settings)
}
} }
val viewJob = CoroutineScope(Dispatchers.IO).launch { val viewJob = CoroutineScope(Dispatchers.IO).launch {
flow.collect { streamData -> flow.collect { streamData ->
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view") Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
val windSpeed = streamData.windSpeed
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){ val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> streamData.value if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180 var headingResponse = streamData.headingResponse
}
val text = when (streamData.settings.windDirectionIndicatorTextSetting) { if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> { headingResponse = HeadingResponse.NoWeatherData
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
headwindSpeed.roundToInt().toString()
}
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString()
WindDirectionIndicatorTextSetting.NONE -> ""
} }
val result = glance.compose(context, DpSize.Unspecified) { emitter.updateView(getErrorWidget(glance, context, streamData.settings, headingResponse).remoteViews)
HeadwindDirection(baseBitmap, windDirection.roundToInt(), config.textSize, text)
}
emitter.updateView(result.remoteViews) return@collect
}
val windSpeed = streamData.windSpeed
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> value
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
}
val text = when (streamData.settings.windDirectionIndicatorTextSetting) {
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
headwindSpeed.roundToInt().toString()
}
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString()
WindDirectionIndicatorTextSetting.NONE -> ""
}
val result = glance.compose(context, DpSize.Unspecified) {
HeadwindDirection(baseBitmap, windDirection.roundToInt(), config.textSize, text)
}
emitter.updateView(result.remoteViews)
} }
} }
emitter.setCancellable { emitter.setCancellable {

View File

@ -1,6 +1,7 @@
package de.timklge.karooheadwind.datatypes package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.getRelativeHeadingFlow import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
@ -15,6 +16,7 @@ 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
@ -22,16 +24,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 value: Double, val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings) data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: OpenMeteoCurrentWeatherResponse?, 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()) { value, data -> value to data }
.combine(context.streamSettings(karooSystem)) { (value, data), settings -> StreamData(value, data, settings) } .combine(context.streamSettings(karooSystem)) { (value, data), settings -> StreamData(value, data, settings) }
.filter { it.weatherResponse != null }
.collect { streamData -> .collect { streamData ->
val windSpeed = streamData.data.current.windSpeed val windSpeed = streamData.weatherResponse?.current?.windSpeed ?: 0.0
val windDirection = streamData.value 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
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to headwindSpeed)))) emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to headwindSpeed))))

View File

@ -4,15 +4,22 @@ import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.appwidget.GlanceRemoteViews
import androidx.glance.layout.Alignment import androidx.glance.layout.Alignment
import androidx.glance.layout.Box import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.getErrorWidget
import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit import de.timklge.karooheadwind.screens.TemperatureUnit
@ -33,6 +40,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
@ -57,6 +66,7 @@ class WeatherDataType(
val currentWeatherData = applicationContext.streamCurrentWeatherData() val currentWeatherData = applicationContext.streamCurrentWeatherData()
currentWeatherData currentWeatherData
.filterNotNull()
.collect { data -> .collect { data ->
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}") Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble())))) emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble()))))
@ -79,15 +89,23 @@ class WeatherDataType(
de.timklge.karooheadwind.R.drawable.arrow_0 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 profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
val viewJob = CoroutineScope(Dispatchers.IO).launch { val viewJob = CoroutineScope(Dispatchers.IO).launch {
context.streamCurrentWeatherData() context.streamCurrentWeatherData()
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } .combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.collect { (data, settings, userProfile) -> .combine(karooSystem.getHeadingFlow()) { data, heading -> data.copy(headingResponse = heading) }
.collect { (data, settings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view") Log.d(KarooHeadwindExtension.TAG, "Updating weather view")
if (data == null){
emitter.updateView(getErrorWidget(glance, context, settings, headingResponse).remoteViews)
return@collect
}
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time)) val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time))

View File

@ -3,10 +3,6 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -25,12 +21,13 @@ import androidx.glance.layout.Row
import androidx.glance.layout.Spacer import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.width import androidx.glance.layout.width
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.saveSettings import de.timklge.karooheadwind.getErrorWidget
import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.saveWidgetSettings import de.timklge.karooheadwind.saveWidgetSettings
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
@ -55,7 +52,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@ -74,7 +70,7 @@ class CycleHoursAction : ActionCallback {
val data = context.streamCurrentWeatherData().first() val data = context.streamCurrentWeatherData().first()
var hourOffset = currentSettings.currentForecastHourOffset + 3 var hourOffset = currentSettings.currentForecastHourOffset + 3
if (hourOffset >= data.forecastData.weatherCode.size) { if (data == null || hourOffset >= data.forecastData.weatherCode.size) {
hourOffset = 0 hourOffset = 0
} }
@ -101,8 +97,8 @@ class WeatherForecastDataType(
currentWeatherData currentWeatherData
.collect { data -> .collect { data ->
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}") Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.current?.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble())))) emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.current?.weatherCode?.toDouble() ?: 0.0)))))
} }
} }
emitter.setCancellable { emitter.setCancellable {
@ -122,16 +118,23 @@ class WeatherForecastDataType(
de.timklge.karooheadwind.R.drawable.arrow_0 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 widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null) val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
val viewJob = CoroutineScope(Dispatchers.IO).launch { val viewJob = CoroutineScope(Dispatchers.IO).launch {
context.streamCurrentWeatherData() context.streamCurrentWeatherData()
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } .combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.combine(context.streamWidgetSettings()) { data, widgetSettings -> data.copy(widgetSettings = widgetSettings) } .combine(context.streamWidgetSettings()) { data, widgetSettings -> data.copy(widgetSettings = widgetSettings) }
.collect { (data, settings, widgetSettings, userProfile) -> .combine(karooSystem.getHeadingFlow()) { data, headingResponse -> data.copy(headingResponse = headingResponse) }
Log.d(KarooHeadwindExtension.TAG, "Updating weather view") .collect { (data, settings, widgetSettings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
if (data == null){
emitter.updateView(getErrorWidget(glance, context, settings, headingResponse).remoteViews)
return@collect
}
val result = glance.compose(context, DpSize.Unspecified) { val result = glance.compose(context, DpSize.Unspecified) {
Row(modifier = GlanceModifier.fillMaxSize().clickable(onClick = actionRunCallback<CycleHoursAction>()), horizontalAlignment = Alignment.Horizontal.CenterHorizontally) { Row(modifier = GlanceModifier.fillMaxSize().clickable(onClick = actionRunCallback<CycleHoursAction>()), horizontalAlignment = Alignment.Horizontal.CenterHorizontally) {