Merge pull request #20 from timklge/feat/error-indication

Show no gps / no weather data error in visual data fields instead of default "No sensor"
This commit is contained in:
timklge 2025-01-17 17:08:29 +01:00 committed by GitHub
commit a948ed4561
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 595 additions and 306 deletions

View File

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

View File

@ -3,9 +3,9 @@
"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.2",
"latestVersionCode": 6,
"latestVersion": "1.1.3",
"latestVersionCode": 7,
"developer": "timklge",
"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": "Adds hourly forecast. Shows error message in fields if weather data or gps are unavailable and remembers last known gps position."
}

View File

@ -4,13 +4,13 @@
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/wind"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat">
<activity
android:name="de.timklge.karooheadwind.MainActivity"
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,162 @@
package de.timklge.karooheadwind
import android.content.Context
import android.util.Log
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
import de.timklge.karooheadwind.screens.WindUnit
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
val settingsKey = stringPreferencesKey("settings")
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
val currentDataKey = stringPreferencesKey("current")
val statsKey = stringPreferencesKey("stats")
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
suspend fun saveSettings(context: Context, settings: HeadwindSettings) {
context.dataStore.edit { t ->
t[settingsKey] = Json.encodeToString(settings)
}
}
suspend fun saveWidgetSettings(context: Context, settings: HeadwindWidgetSettings) {
context.dataStore.edit { t ->
t[widgetSettingsKey] = Json.encodeToString(settings)
}
}
suspend fun saveStats(context: Context, stats: HeadwindStats) {
context.dataStore.edit { t ->
t[statsKey] = Json.encodeToString(stats)
}
}
suspend fun saveCurrentData(context: Context, forecast: OpenMeteoCurrentWeatherResponse) {
context.dataStore.edit { t ->
t[currentDataKey] = Json.encodeToString(forecast)
}
}
suspend fun saveLastKnownPosition(context: Context, gpsCoordinates: GpsCoordinates) {
Log.i(KarooHeadwindExtension.TAG, "Saving last known position: $gpsCoordinates")
try {
context.dataStore.edit { t ->
t[lastKnownPositionKey] = Json.encodeToString(gpsCoordinates)
}
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to save last known position", e)
}
}
fun Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> {
return dataStore.data.map { settingsJson ->
try {
if (settingsJson.contains(widgetSettingsKey)){
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(settingsJson[widgetSettingsKey]!!)
} else {
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
}
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read widget preferences", e)
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
}
}.distinctUntilChanged()
}
fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<HeadwindSettings> {
return dataStore.data.map { settingsJson ->
try {
if (settingsJson.contains(settingsKey)){
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
} else {
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
defaultSettings.copy(
windUnit = if (preferredUnits.distance == UserProfile.PreferredUnit.UnitType.METRIC) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR,
)
}
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read preferences", e)
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
}
}.distinctUntilChanged()
}
fun Context.streamStats(): Flow<HeadwindStats> {
return dataStore.data.map { statsJson ->
try {
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(
statsJson[statsKey] ?: HeadwindStats.defaultStats
)
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read stats", e)
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(HeadwindStats.defaultStats)
}
}.distinctUntilChanged()
}
suspend fun Context.getLastKnownPosition(): GpsCoordinates? {
val settingsJson = dataStore.data.first()
try {
val lastKnownPositionString = settingsJson[lastKnownPositionKey] ?: return null
val lastKnownPosition = jsonWithUnknownKeys.decodeFromString<GpsCoordinates>(
lastKnownPositionString
)
return lastKnownPosition
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read last known position", e)
return null
}
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile ->
trySendBlocking(userProfile)
}
awaitClose {
removeConsumer(listenerId)
}
}
}
fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse?> {
return dataStore.data.map { settingsJson ->
try {
val data = settingsJson[currentDataKey]
data?.let { d -> jsonWithUnknownKeys.decodeFromString<OpenMeteoCurrentWeatherResponse>(d) }
} catch (e: Throwable) {
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
null
}
}.distinctUntilChanged().map { response ->
if (response != null && response.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12)){
response
} else {
null
}
}
}

View File

@ -2,8 +2,25 @@ package de.timklge.karooheadwind
import android.content.Context
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.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.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindStats
@ -22,17 +39,17 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.timeout
@ -44,37 +61,6 @@ import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.seconds
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
val settingsKey = stringPreferencesKey("settings")
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
val currentDataKey = stringPreferencesKey("current")
val statsKey = stringPreferencesKey("stats")
suspend fun saveSettings(context: Context, settings: HeadwindSettings) {
context.dataStore.edit { t ->
t[settingsKey] = Json.encodeToString(settings)
}
}
suspend fun saveWidgetSettings(context: Context, settings: HeadwindWidgetSettings) {
context.dataStore.edit { t ->
t[widgetSettingsKey] = Json.encodeToString(settings)
}
}
suspend fun saveStats(context: Context, stats: HeadwindStats) {
context.dataStore.edit { t ->
t[statsKey] = Json.encodeToString(stats)
}
}
suspend fun saveCurrentData(context: Context, forecast: OpenMeteoCurrentWeatherResponse) {
context.dataStore.edit { t ->
t[currentDataKey] = Json.encodeToString(forecast)
}
}
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
return callbackFlow {
val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState ->
@ -86,189 +72,14 @@ fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
}
}
fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse> {
return dataStore.data.map { settingsJson ->
try {
val data = settingsJson[currentDataKey]
data?.let { d -> jsonWithUnknownKeys.decodeFromString<OpenMeteoCurrentWeatherResponse>(d) }
} catch (e: Throwable) {
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
null
}
}.filterNotNull().distinctUntilChanged().filter { it.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12) }
}
fun<T> Flow<T>.throttle(timeout: Long): Flow<T> = flow {
var lastEmissionTime = 0L
fun Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> {
return dataStore.data.map { settingsJson ->
try {
if (settingsJson.contains(widgetSettingsKey)){
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(settingsJson[widgetSettingsKey]!!)
} else {
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
}
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read widget preferences", e)
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
}
}.distinctUntilChanged()
}
fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<HeadwindSettings> {
return dataStore.data.map { settingsJson ->
try {
if (settingsJson.contains(settingsKey)){
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
} else {
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
defaultSettings.copy(
windUnit = if (preferredUnits.distance == UserProfile.PreferredUnit.UnitType.METRIC) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR,
)
}
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read preferences", e)
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
}
}.distinctUntilChanged()
}
fun Context.streamStats(): Flow<HeadwindStats> {
return dataStore.data.map { statsJson ->
try {
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(
statsJson[statsKey] ?: HeadwindStats.defaultStats
)
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read stats", e)
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(HeadwindStats.defaultStats)
}
}.distinctUntilChanged()
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile ->
trySendBlocking(userProfile)
}
awaitClose {
removeConsumer(listenerId)
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= timeout) {
emit(value)
lastEmissionTime = currentTime
}
}
}
@OptIn(FlowPreview::class)
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=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}"
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
val listenerId = addConsumer(
OnHttpResponse.MakeHttpRequest(
"GET",
url,
waitForConnection = false,
),
) { event: OnHttpResponse ->
Log.d(KarooHeadwindExtension.TAG, "Http response event $event")
if (event.state is HttpResponseState.Complete){
trySend(event.state as HttpResponseState.Complete)
close()
}
}
awaitClose {
removeConsumer(listenerId)
}
}.timeout(20.seconds).catch { e: Throwable ->
if (e is TimeoutCancellationException){
emit(HttpResponseState.Complete(500, mapOf(), null, "Timeout"))
} else {
throw e
}
}.single()
}
fun signedAngleDifference(angle1: Double, angle2: Double): Double {
val a1 = angle1 % 360
val a2 = angle2 % 360
var diff = abs(a1 - a2)
val sign = if (a1 < a2) {
if (diff > 180.0) -1 else 1
} else {
if (diff > 180.0) 1 else -1
}
if (diff > 180.0) {
diff = 360.0 - diff
}
return sign * diff
}
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<Double> {
val currentWeatherData = context.streamCurrentWeatherData()
return getHeadingFlow()
.filter { it >= 0 }
.combine(currentWeatherData) { bearing, data -> bearing to data }
.map { (bearing, data) ->
val windBearing = data.current.windDirection + 180
val diff = signedAngleDifference(bearing, windBearing)
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff")
diff
}
}
fun KarooSystemService.getHeadingFlow(): Flow<Double> {
// return flowOf(20.0)
return streamDataFlow(DataType.Type.LOCATION)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.values }
.map { values ->
val heading = values[DataType.Field.LOC_BEARING]
Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading")
heading ?: 0.0
}
.distinctUntilChanged()
.scan(emptyList<Double>()) { acc, value -> /* Average over 3 values */
val newAcc = acc + value
if (newAcc.size > 3) newAcc.drop(1) else newAcc
}
.map { it.average() }
}
@OptIn(FlowPreview::class)
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates> {
// return flowOf(GpsCoordinates(52.5164069,13.3784))
return streamDataFlow(DataType.Type.LOCATION)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.values }
.mapNotNull { values ->
val lat = values[DataType.Field.LOC_LATITUDE]
val lon = values[DataType.Field.LOC_LONGITUDE]
if (lat != null && lon != null){
Log.d(KarooHeadwindExtension.TAG, "Updated gps coords: $lat $lon")
GpsCoordinates(lat, lon)
} else {
Log.e(KarooHeadwindExtension.TAG, "Missing gps values: $values")
null
}
}
.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

@ -0,0 +1,145 @@
package de.timklge.karooheadwind
import android.content.Context
import android.util.Log
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.scan
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()
return getHeadingFlow(context)
.combine(currentWeatherData) { bearing, data -> bearing to data }
.map { (bearing, data) ->
when {
bearing is HeadingResponse.Value && data != null -> {
val windBearing = data.current.windDirection + 180
val diff = signedAngleDifference(bearing.diff, windBearing)
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(context: Context): Flow<HeadingResponse> {
// return flowOf(HeadingResponse.Value(20.0))
return getGpsCoordinateFlow(context)
.map { coords ->
val heading = coords?.bearing
Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading")
val headingValue = heading?.let { HeadingResponse.Value(it) }
headingValue ?: HeadingResponse.NoGps
}
.distinctUntilChanged()
.scan(emptyList<HeadingResponse>()) { acc, value -> /* Average over 3 values */
if (value !is HeadingResponse.Value) return@scan listOf(value)
val newAcc = acc + value
if (newAcc.size > 3) newAcc.drop(1) else newAcc
}
.map { data ->
Log.i(KarooHeadwindExtension.TAG, "Heading value: $data")
if (data.isEmpty()) return@map HeadingResponse.NoGps
if (data.firstOrNull() !is HeadingResponse.Value) return@map data.first()
val avgValues = data.mapNotNull { (it as? HeadingResponse.Value)?.diff }
if (avgValues.isEmpty()) return@map HeadingResponse.NoGps
val avg = avgValues.average()
HeadingResponse.Value(avg)
}
}
fun <T> concatenate(vararg flows: Flow<T>) = flow {
for (flow in flows) {
emitAll(flow)
}
}
fun<T> Flow<T>.dropNullsIfNullEncountered(): Flow<T?> = flow {
var hadValue = false
collect { value ->
if (!hadValue) {
emit(value)
if (value != null) hadValue = true
} else {
if (value != null) emit(value)
}
}
}
suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
while (true) {
getGpsCoordinateFlow(context)
.filterNotNull()
.throttle(60 * 1_000) // Only update last known gps position once every minute
.collect { gps ->
saveLastKnownPosition(context, gps)
}
delay(1_000)
}
}
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
// return flowOf(GpsCoordinates(52.5164069,13.3784))
val initialFlow = flow {
val lastKnownPosition = context.getLastKnownPosition()
if (lastKnownPosition != null) emit(lastKnownPosition)
}
val gpsFlow = streamDataFlow(DataType.Type.LOCATION)
.map { (it as? StreamState.Streaming)?.dataPoint?.values }
.map { values ->
val lat = values?.get(DataType.Field.LOC_LATITUDE)
val lon = values?.get(DataType.Field.LOC_LONGITUDE)
val bearing = values?.get(DataType.Field.LOC_BEARING)
if (lat != null && lon != null){
// Log.d(KarooHeadwindExtension.TAG, "Updated gps coordinates: $lat $lon")
GpsCoordinates(lat, lon, bearing)
} else {
// Log.w(KarooHeadwindExtension.TAG, "Gps unavailable: $values")
null
}
}
val concatenatedFlow = concatenate(initialFlow, gpsFlow)
return concatenatedFlow
.combine(context.streamSettings(this)) { gps, settings -> gps to settings }
.map { (gps, settings) ->
gps?.round(settings.roundLocationTo.km.toDouble())
}
.dropNullsIfNullEncountered()
}

View File

@ -23,25 +23,32 @@ import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retry
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.debounce
import java.time.Duration
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") {
companion object {
const val TAG = "karoo-headwind"
}
lateinit var karooSystem: KarooSystemService
private var updateLastKnownGpsJob: Job? = null
private var serviceJob: Job? = null
override val types by lazy {
@ -62,15 +69,19 @@ 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)
@OptIn(ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
override fun onCreate() {
super.onCreate()
karooSystem = KarooSystemService(applicationContext)
updateLastKnownGpsJob = CoroutineScope(Dispatchers.IO).launch {
karooSystem.updateLastKnownGps(this@KarooHeadwindExtension)
}
serviceJob = CoroutineScope(Dispatchers.IO).launch {
karooSystem.connect { connected ->
if (connected) {
@ -80,7 +91,15 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
val gpsFlow = karooSystem
.getGpsCoordinateFlow(this@KarooHeadwindExtension)
.transformLatest { value: GpsCoordinates ->
.distinctUntilChanged { old, new ->
if (old != null && new != null) {
old.distanceTo(new).absoluteValue < 0.001
} else {
old == new
}
}
.debounce(Duration.ofSeconds(5))
.transformLatest { value: GpsCoordinates? ->
while(true){
emit(value)
delay(1.hours)
@ -101,6 +120,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
HeadwindStats()
}
if (gps == null){
error("No GPS coordinates available")
}
val response = karooSystem.makeOpenMeteoHttpRequest(gps, settings, profile)
if (response.error != null){
try {

View File

@ -0,0 +1,55 @@
package de.timklge.karooheadwind
import android.util.Log
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
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 kotlin.time.Duration.Companion.seconds
@OptIn(FlowPreview::class)
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=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}"
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
val listenerId = addConsumer(
OnHttpResponse.MakeHttpRequest(
"GET",
url,
waitForConnection = false,
),
) { event: OnHttpResponse ->
Log.d(KarooHeadwindExtension.TAG, "Http response event $event")
if (event.state is HttpResponseState.Complete){
trySend(event.state as HttpResponseState.Complete)
close()
}
}
awaitClose {
removeConsumer(listenerId)
}
}.timeout(20.seconds).catch { e: Throwable ->
if (e is TimeoutCancellationException){
emit(HttpResponseState.Complete(500, mapOf(), null, "Timeout"))
} else {
throw e
}
}.single()
}

View File

@ -0,0 +1,21 @@
package de.timklge.karooheadwind
import kotlin.math.abs
fun signedAngleDifference(angle1: Double, angle2: Double): Double {
val a1 = angle1 % 360
val a2 = angle2 % 360
var diff = abs(a1 - a2)
val sign = if (a1 < a2) {
if (diff > 180.0) -1 else 1
} else {
if (diff > 180.0) 1 else -1
}
if (diff > 180.0) {
diff = 360.0 - diff
}
return sign * diff
}

View File

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

View File

@ -9,7 +9,7 @@ import kotlin.math.sin
import kotlin.math.sqrt
@Serializable
data class GpsCoordinates(val lat: Double, val lon: Double){
data class GpsCoordinates(val lat: Double, val lon: Double, val bearing: Double? = 0.0){
companion object {
private fun roundDegrees(degrees: Double, km: Double): Double {
val nkm = degrees * 111

View File

@ -6,13 +6,13 @@ import android.util.Log
import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamDataFlow
import de.timklge.karooheadwind.streamSettings
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl
@ -30,8 +30,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlin.math.cos
import kotlin.math.roundToInt
@ -47,7 +45,8 @@ class HeadwindDirectionDataType(
val job = CoroutineScope(Dispatchers.IO).launch {
karooSystem.getRelativeHeadingFlow(applicationContext)
.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 {
@ -55,7 +54,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> {
return flow {
@ -63,7 +62,7 @@ class HeadwindDirectionDataType(
val bearing = (0..360).random().toDouble()
val windSpeed = (-20..20).random()
emit(StreamData(bearing, bearing, windSpeed.toDouble(), HeadwindSettings()))
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings()))
delay(2_000)
}
}
@ -86,36 +85,47 @@ class HeadwindDirectionDataType(
val flow = if (config.preview) {
previewFlow()
} else {
karooSystem.streamDataFlow(dataTypeId)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(context.streamCurrentWeatherData()) { value, data -> value to data }
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
StreamData(value, data.current.windDirection, data.current.windSpeed, settings)
}
karooSystem.getRelativeHeadingFlow(context)
.combine(context.streamCurrentWeatherData()) { headingResponse, data -> StreamData(headingResponse, data?.current?.windDirection, data?.current?.windSpeed) }
.combine(context.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
}
val viewJob = CoroutineScope(Dispatchers.IO).launch {
flow.collect { streamData ->
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
val windSpeed = streamData.windSpeed
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> streamData.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 -> ""
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){
var headingResponse = streamData.headingResponse
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
headingResponse = HeadingResponse.NoWeatherData
}
val result = glance.compose(context, DpSize.Unspecified) {
HeadwindDirection(baseBitmap, windDirection.roundToInt(), config.textSize, text)
}
emitter.updateView(getErrorWidget(glance, context, streamData.settings, headingResponse).remoteViews)
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 {

View File

@ -1,6 +1,7 @@
package de.timklge.karooheadwind.datatypes
import android.content.Context
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
@ -15,6 +16,7 @@ import io.hammerhead.karooext.models.StreamState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlin.math.cos
@ -22,16 +24,17 @@ class HeadwindSpeedDataType(
private val karooSystem: KarooSystemService,
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>) {
val job = CoroutineScope(Dispatchers.IO).launch {
karooSystem.getRelativeHeadingFlow(context)
.combine(context.streamCurrentWeatherData()) { value, data -> value to data }
.combine(context.streamSettings(karooSystem)) { (value, data), settings -> StreamData(value, data, settings) }
.filter { it.weatherResponse != null }
.collect { streamData ->
val windSpeed = streamData.data.current.windSpeed
val windDirection = streamData.value
val windSpeed = streamData.weatherResponse?.current?.windSpeed ?: 0.0
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to headwindSpeed))))

View File

@ -0,0 +1,47 @@
package de.timklge.karooheadwind.datatypes
import android.content.Context
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.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.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.screens.HeadwindSettings
@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 {
"Weather data download failed"
}
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)
)
)
}
}
}

View File

@ -10,9 +10,11 @@ import androidx.glance.appwidget.GlanceRemoteViews
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.PrecipitationUnit
import de.timklge.karooheadwind.screens.TemperatureUnit
@ -33,7 +35,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.ZoneId
@ -58,8 +59,8 @@ class WeatherDataType(
currentWeatherData
.collect { data ->
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble()))))
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.current?.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.current?.weatherCode?.toDouble() ?: 0.0)))))
}
}
emitter.setCancellable {
@ -79,15 +80,23 @@ class WeatherDataType(
de.timklge.karooheadwind.R.drawable.arrow_0
)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings,
val profile: UserProfile? = null)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings,
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = 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) }
.collect { (data, settings, userProfile) ->
.combine(karooSystem.getHeadingFlow(context)) { data, heading -> data.copy(headingResponse = heading) }
.collect { (data, settings, userProfile, headingResponse) ->
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 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.graphics.BitmapFactory
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.unit.DpSize
import androidx.compose.ui.unit.dp
@ -25,12 +21,12 @@ import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.width
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.saveSettings
import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.saveWidgetSettings
import de.timklge.karooheadwind.screens.HeadwindSettings
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
@ -55,7 +51,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.ZoneId
@ -74,7 +69,7 @@ class CycleHoursAction : ActionCallback {
val data = context.streamCurrentWeatherData().first()
var hourOffset = currentSettings.currentForecastHourOffset + 3
if (hourOffset >= data.forecastData.weatherCode.size) {
if (data == null || hourOffset >= data.forecastData.weatherCode.size) {
hourOffset = 0
}
@ -101,8 +96,8 @@ class WeatherForecastDataType(
currentWeatherData
.collect { data ->
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble()))))
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.current?.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.current?.weatherCode?.toDouble() ?: 0.0)))))
}
}
emitter.setCancellable {
@ -122,16 +117,23 @@ class WeatherForecastDataType(
de.timklge.karooheadwind.R.drawable.arrow_0
)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse, val settings: HeadwindSettings,
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null)
data class StreamData(val data: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings,
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null, val headingResponse: HeadingResponse? = 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) }
.collect { (data, settings, widgetSettings, userProfile) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view")
.combine(karooSystem.getHeadingFlow(context)) { data, headingResponse -> data.copy(headingResponse = headingResponse) }
.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) {
Row(modifier = GlanceModifier.fillMaxSize().clickable(onClick = actionRunCallback<CycleHoursAction>()), horizontalAlignment = Alignment.Horizontal.CenterHorizontally) {

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="#FFFFFF" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

View File

@ -1,35 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<ExtensionInfo
displayName="@string/extension_name"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
id="karoo-headwind"
scansDevices="false">
<DataType
description="@string/headwind_description"
displayName="@string/headwind"
graphical="true"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="headwind" />
<DataType
description="@string/weather_description"
displayName="@string/weather"
graphical="true"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="weather" />
<DataType
description="@string/weather_forecast_description"
displayName="@string/weather_forecast"
graphical="true"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="weatherForecast" />
<DataType
description="@string/headwind_speed_description"
displayName="@string/headwind_speed"
graphical="false"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="headwindSpeed" />
<DataType
@ -50,21 +50,21 @@
description="@string/windSpeed_description"
displayName="@string/windSpeed"
graphical="false"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="windSpeed" />
<DataType
description="@string/windGusts_description"
displayName="@string/windGusts"
graphical="false"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="windGusts" />
<DataType
description="@string/windDirection_description"
displayName="@string/windDirection"
graphical="true"
icon="@drawable/ic_launcher"
icon="@drawable/wind"
typeId="windDirection" />
<DataType