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:
commit
a948ed4561
@ -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 {
|
||||||
|
|||||||
@ -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": "Adds hourly forecast. Shows error message in fields if weather data or gps are unavailable and remembers last known gps position."
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@drawable/wind"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AppCompat">
|
android:theme="@style/Theme.AppCompat">
|
||||||
<activity
|
<activity
|
||||||
android:name="de.timklge.karooheadwind.MainActivity"
|
android:name="de.timklge.karooheadwind.MainActivity"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
162
app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt
Normal file
162
app/src/main/kotlin/de/timklge/karooheadwind/DataStore.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -22,17 +39,17 @@ import kotlinx.coroutines.FlowPreview
|
|||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
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.emitAll
|
||||||
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.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.scan
|
import kotlinx.coroutines.flow.scan
|
||||||
import kotlinx.coroutines.flow.single
|
import kotlinx.coroutines.flow.single
|
||||||
import kotlinx.coroutines.flow.timeout
|
import kotlinx.coroutines.flow.timeout
|
||||||
@ -44,37 +61,6 @@ import kotlin.math.abs
|
|||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.time.Duration.Companion.seconds
|
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> {
|
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
|
||||||
return callbackFlow {
|
return callbackFlow {
|
||||||
val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState ->
|
val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState ->
|
||||||
@ -86,189 +72,14 @@ fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.streamCurrentWeatherData(): Flow<OpenMeteoCurrentWeatherResponse> {
|
fun<T> Flow<T>.throttle(timeout: Long): Flow<T> = flow {
|
||||||
return dataStore.data.map { settingsJson ->
|
var lastEmissionTime = 0L
|
||||||
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 Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> {
|
collect { value ->
|
||||||
return dataStore.data.map { settingsJson ->
|
val currentTime = System.currentTimeMillis()
|
||||||
try {
|
if (currentTime - lastEmissionTime >= timeout) {
|
||||||
if (settingsJson.contains(widgetSettingsKey)){
|
emit(value)
|
||||||
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(settingsJson[widgetSettingsKey]!!)
|
lastEmissionTime = currentTime
|
||||||
} 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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¤t=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}¤t=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))
|
|
||||||
}
|
|
||||||
145
app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt
Normal file
145
app/src/main/kotlin/de/timklge/karooheadwind/HeadingFlow.kt
Normal 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()
|
||||||
|
}
|
||||||
@ -23,25 +23,32 @@ import io.hammerhead.karooext.models.UserProfile
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.retry
|
import kotlinx.coroutines.flow.retry
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.time.debounce
|
||||||
|
import java.time.Duration
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
lateinit var karooSystem: KarooSystemService
|
lateinit var karooSystem: KarooSystemService
|
||||||
|
|
||||||
|
private var updateLastKnownGpsJob: Job? = null
|
||||||
private var serviceJob: Job? = null
|
private var serviceJob: Job? = null
|
||||||
|
|
||||||
override val types by lazy {
|
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)
|
val profile: UserProfile? = null)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
karooSystem = KarooSystemService(applicationContext)
|
karooSystem = KarooSystemService(applicationContext)
|
||||||
|
|
||||||
|
updateLastKnownGpsJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
karooSystem.updateLastKnownGps(this@KarooHeadwindExtension)
|
||||||
|
}
|
||||||
|
|
||||||
serviceJob = CoroutineScope(Dispatchers.IO).launch {
|
serviceJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
karooSystem.connect { connected ->
|
karooSystem.connect { connected ->
|
||||||
if (connected) {
|
if (connected) {
|
||||||
@ -80,7 +91,15 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.2") {
|
|||||||
|
|
||||||
val gpsFlow = karooSystem
|
val gpsFlow = karooSystem
|
||||||
.getGpsCoordinateFlow(this@KarooHeadwindExtension)
|
.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){
|
while(true){
|
||||||
emit(value)
|
emit(value)
|
||||||
delay(1.hours)
|
delay(1.hours)
|
||||||
@ -101,6 +120,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 {
|
||||||
|
|||||||
55
app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt
Normal file
55
app/src/main/kotlin/de/timklge/karooheadwind/OpenMeteo.kt
Normal 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¤t=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}¤t=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()
|
||||||
|
}
|
||||||
21
app/src/main/kotlin/de/timklge/karooheadwind/Utils.kt
Normal file
21
app/src/main/kotlin/de/timklge/karooheadwind/Utils.kt
Normal 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
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import kotlin.math.sin
|
|||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
@Serializable
|
@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 {
|
companion object {
|
||||||
private fun roundDegrees(degrees: Double, km: Double): Double {
|
private fun roundDegrees(degrees: Double, km: Double): Double {
|
||||||
val nkm = degrees * 111
|
val nkm = degrees * 111
|
||||||
|
|||||||
@ -6,13 +6,13 @@ 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.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
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
@ -30,8 +30,6 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -47,7 +45,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 +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> {
|
private fun previewFlow(): Flow<StreamData> {
|
||||||
return flow {
|
return flow {
|
||||||
@ -63,7 +62,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 +85,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 {
|
||||||
|
|||||||
@ -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))))
|
||||||
|
|||||||
@ -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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,9 +10,11 @@ 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 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.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,7 +35,6 @@ 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.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
|
||||||
@ -58,8 +59,8 @@ class WeatherDataType(
|
|||||||
|
|
||||||
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 {
|
||||||
@ -79,15 +80,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(context)) { 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))
|
||||||
|
|
||||||
|
|||||||
@ -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,12 @@ 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.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 +51,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 +69,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 +96,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 +117,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(context)) { 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) {
|
||||||
|
|||||||
6
app/src/main/res/drawable/launch_background.xml
Normal file
6
app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
app/src/main/res/drawable/wind.png
Normal file
BIN
app/src/main/res/drawable/wind.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@ -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>
|
|
||||||
@ -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>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#000000</color>
|
|
||||||
</resources>
|
|
||||||
6
app/src/main/res/values/styles.xml
Normal file
6
app/src/main/res/values/styles.xml
Normal 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>
|
||||||
@ -1,35 +1,35 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ExtensionInfo
|
<ExtensionInfo
|
||||||
displayName="@string/extension_name"
|
displayName="@string/extension_name"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
id="karoo-headwind"
|
id="karoo-headwind"
|
||||||
scansDevices="false">
|
scansDevices="false">
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/headwind_description"
|
description="@string/headwind_description"
|
||||||
displayName="@string/headwind"
|
displayName="@string/headwind"
|
||||||
graphical="true"
|
graphical="true"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="headwind" />
|
typeId="headwind" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/weather_description"
|
description="@string/weather_description"
|
||||||
displayName="@string/weather"
|
displayName="@string/weather"
|
||||||
graphical="true"
|
graphical="true"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="weather" />
|
typeId="weather" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/weather_forecast_description"
|
description="@string/weather_forecast_description"
|
||||||
displayName="@string/weather_forecast"
|
displayName="@string/weather_forecast"
|
||||||
graphical="true"
|
graphical="true"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="weatherForecast" />
|
typeId="weatherForecast" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/headwind_speed_description"
|
description="@string/headwind_speed_description"
|
||||||
displayName="@string/headwind_speed"
|
displayName="@string/headwind_speed"
|
||||||
graphical="false"
|
graphical="false"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="headwindSpeed" />
|
typeId="headwindSpeed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
@ -50,21 +50,21 @@
|
|||||||
description="@string/windSpeed_description"
|
description="@string/windSpeed_description"
|
||||||
displayName="@string/windSpeed"
|
displayName="@string/windSpeed"
|
||||||
graphical="false"
|
graphical="false"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="windSpeed" />
|
typeId="windSpeed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/windGusts_description"
|
description="@string/windGusts_description"
|
||||||
displayName="@string/windGusts"
|
displayName="@string/windGusts"
|
||||||
graphical="false"
|
graphical="false"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="windGusts" />
|
typeId="windGusts" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/windDirection_description"
|
description="@string/windDirection_description"
|
||||||
displayName="@string/windDirection"
|
displayName="@string/windDirection"
|
||||||
graphical="true"
|
graphical="true"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="windDirection" />
|
typeId="windDirection" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user