Compare commits

...

5 Commits

Author SHA1 Message Date
a8f7f53d66 Remove apk archival step
All checks were successful
Build / build (push) Successful in 5m37s
2025-05-30 13:31:44 +02:00
823fe3a8bb Update pipeline
Some checks failed
Build / build (push) Failing after 9m14s
2025-05-30 13:13:29 +02:00
522364dd84 Unit conversions
Some checks failed
Build / build (push) Failing after 34s
2025-05-29 15:24:52 +02:00
855ce46b99 ref #136: Fix cloud cover, surface level pressure, sealevel pressure and relative humidity are not included in forecast values 2025-05-24 11:51:53 +02:00
0332e032d4 Refactor unit conversion 2025-05-24 11:51:53 +02:00
33 changed files with 259 additions and 157 deletions

View File

@ -3,10 +3,10 @@ name: Build
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ "master" ] branches: [ "**" ]
tags: [ "*" ] tags: [ "*" ]
pull_request: pull_request:
branches: [ "master" ] branches: [ "**" ]
jobs: jobs:
build: build:
@ -19,13 +19,14 @@ jobs:
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Set up environment variables - name: Set up environment variables
run: | run: |
echo "GPR_USER=${{ github.actor }}" >> $GITHUB_ENV echo "GPR_USER=${{ secrets.GHUB_USER || github.actor }}" >> $GITHUB_ENV
echo "GPR_KEY=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV echo "GPR_KEY=${{ secrets.GHUB_TOKEN || secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV
echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV
echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV
echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV
echo "BASE_URL=${{ secrets.BASE_URL || 'https://github.com/timklge/karoo-headwind/releases/latest/download' }}" >> $GITHUB_ENV
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: set up JDK 17 - name: set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@ -34,25 +35,30 @@ jobs:
distribution: 'temurin' distribution: 'temurin'
cache: gradle cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew build
- name: Archive APK
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: ncipollo/release-action@v1 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
name: ${{ github.ref_name }} name: ${{ github.ref_name }}
prerelease: false draft: false
generateReleaseNotes: true generate_release_notes: true
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-headwind.png, preview0.png, preview1.png, preview2.png, preview3.png make_latest: true
files: |
app/build/outputs/apk/release/app-release.apk
app/manifest.json
app/karoo-headwind.png
preview0.png
preview1.png
preview2.png
preview3.png
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
.cxx .cxx
local.properties local.properties
/app/release /app/release
app/manifest.json

View File

@ -1,3 +1,4 @@
import com.android.build.gradle.tasks.ProcessApplicationManifest
import java.util.Base64 import java.util.Base64
plugins { plugins {
@ -60,29 +61,38 @@ tasks.register("generateManifest") {
group = "build" group = "build"
doLast { doLast {
val baseUrl = System.getenv("BASE_URL") ?: "https://github.com/timklge/karoo-headwind/releases/latest/download"
val manifestFile = file("$projectDir/manifest.json") val manifestFile = file("$projectDir/manifest.json")
val manifest = mapOf( val manifest = mapOf(
"label" to "Headwind", "label" to "Headwind",
"packageName" to "de.timklge.karooheadwind", "packageName" to "de.timklge.karooheadwind",
"iconUrl" to "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png", "iconUrl" to "$baseUrl/karoo-headwind.png",
"latestApkUrl" to "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk", "latestApkUrl" to "$baseUrl/app-release.apk",
"latestVersion" to android.defaultConfig.versionName, "latestVersion" to android.defaultConfig.versionName,
"latestVersionCode" to android.defaultConfig.versionCode, "latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "github.com/timklge", "developer" to "github.com/timklge",
"description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.", "description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.",
"releaseNotes" to "* Remove crashlytics\n" + "releaseNotes" to "* Refactor unit conversions\n* Remove crashlytics\n" +
"* Reduce refresh rate on K2, add refresh rate setting\n" + "* Reduce refresh rate on K2, add refresh rate setting\n" +
"screenshotUrls" to listOf( "screenshotUrls" to listOf(
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview1.png", "$baseUrl/preview1.png",
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview3.png", "$baseUrl/preview3.png",
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview2.png", "$baseUrl/preview2.png",
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview0.png", "$baseUrl/preview0.png",
) )
) )
val gson = groovy.json.JsonBuilder(manifest).toPrettyString() val gson = groovy.json.JsonBuilder(manifest).toPrettyString()
manifestFile.writeText(gson) manifestFile.writeText(gson)
println("Generated manifest.json with version ${android.defaultConfig.versionName} (${android.defaultConfig.versionCode})") println("Generated manifest.json with version ${android.defaultConfig.versionName} (${android.defaultConfig.versionCode})")
if (System.getenv()["BASE_URL"] != null){
val androidManifestFile = file("$projectDir/src/main/AndroidManifest.xml")
var androidManifestContent = androidManifestFile.readText()
androidManifestContent = androidManifestContent.replace("\$BASE_URL\$", baseUrl)
androidManifestFile.writeText(androidManifestContent)
println("Replaced \$BASE_URL$ in AndroidManifest.xml")
}
} }
} }
@ -90,6 +100,12 @@ tasks.named("assemble") {
dependsOn("generateManifest") dependsOn("generateManifest")
} }
tasks.withType<ProcessApplicationManifest>().configureEach {
if (name == "processDebugMainManifest" || name == "processReleaseMainManifest") {
dependsOn(tasks.named("generateManifest"))
}
}
dependencies { dependencies {
implementation(libs.mapbox.sdk.turf) implementation(libs.mapbox.sdk.turf)
implementation(libs.hammerhead.karoo.ext) implementation(libs.hammerhead.karoo.ext)

View File

@ -32,6 +32,6 @@
<meta-data <meta-data
android:name="io.hammerhead.karooext.MANIFEST_URL" android:name="io.hammerhead.karooext.MANIFEST_URL"
android:value="https://github.com/timklge/karoo-headwind/releases/latest/download/manifest.json" /> android:value="$BASE_URL$/manifest.json" />
</application> </application>
</manifest> </manifest>

View File

@ -210,6 +210,22 @@ fun Context.streamCurrentForecastWeatherData(): Flow<WeatherDataResponse?> {
}.distinctUntilChanged() }.distinctUntilChanged()
} }
fun lerp(
start: Double,
end: Double,
factor: Double
): Double {
return start + (end - start) * factor
}
fun lerp(
start: Int,
end: Int,
factor: Double
): Int {
return (start + (end - start) * factor).toInt()
}
fun lerpNullable( fun lerpNullable(
start: Double?, start: Double?,
end: Double?, end: Double?,
@ -266,12 +282,12 @@ fun lerpWeather(
return WeatherData( return WeatherData(
time = (start.time + (end.time - start.time) * factor).toLong(), time = (start.time + (end.time - start.time) * factor).toLong(),
temperature = start.temperature + (end.temperature - start.temperature) * factor, temperature = start.temperature + (end.temperature - start.temperature) * factor,
relativeHumidity = lerpNullable(start.relativeHumidity, end.relativeHumidity, factor), relativeHumidity = lerp(start.relativeHumidity, end.relativeHumidity, factor),
precipitation = start.precipitation + (end.precipitation - start.precipitation) * factor, precipitation = start.precipitation + (end.precipitation - start.precipitation) * factor,
precipitationProbability = lerpNullable(start.precipitationProbability, end.precipitationProbability, factor), precipitationProbability = lerpNullable(start.precipitationProbability, end.precipitationProbability, factor),
cloudCover = lerpNullable(start.cloudCover, end.cloudCover, factor), cloudCover = lerp(start.cloudCover, end.cloudCover, factor),
surfacePressure = lerpNullable(start.surfacePressure, end.surfacePressure, factor), surfacePressure = lerp(start.surfacePressure, end.surfacePressure, factor),
sealevelPressure = lerpNullable(start.sealevelPressure, end.sealevelPressure, factor), sealevelPressure = lerp(start.sealevelPressure, end.sealevelPressure, factor),
windSpeed = start.windSpeed + (end.windSpeed - start.windSpeed) * factor, windSpeed = start.windSpeed + (end.windSpeed - start.windSpeed) * factor,
windDirection = lerpAngle(start.windDirection, end.windDirection, factor), windDirection = lerpAngle(start.windDirection, end.windDirection, factor),
windGusts = start.windGusts + (end.windGusts - start.windGusts) * factor, windGusts = start.windGusts + (end.windGusts - start.windGusts) * factor,

View File

@ -4,16 +4,23 @@ import android.content.Context
import android.util.Log import android.util.Log
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
import io.hammerhead.karooext.internal.Emitter import io.hammerhead.karooext.internal.Emitter
import io.hammerhead.karooext.internal.ViewEmitter
import io.hammerhead.karooext.models.DataPoint import io.hammerhead.karooext.models.DataPoint
import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -22,20 +29,25 @@ abstract class BaseDataType(
private val applicationContext: Context, private val applicationContext: Context,
dataTypeId: String dataTypeId: String
) : DataTypeImpl("karoo-headwind", dataTypeId) { ) : DataTypeImpl("karoo-headwind", dataTypeId) {
abstract fun getValue(data: WeatherData): Double? abstract fun getValue(data: WeatherData, userProfile: UserProfile): Double?
open fun getFormatDataType(): String? = null
override fun startStream(emitter: Emitter<StreamState>) { override fun startStream(emitter: Emitter<StreamState>) {
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream") Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
val job = CoroutineScope(Dispatchers.IO).launch { val job = CoroutineScope(Dispatchers.IO).launch {
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystemService) data class StreamData(val weatherData: WeatherData, val userProfile: UserProfile)
val currentWeatherData = combine(applicationContext.streamCurrentWeatherData(karooSystemService).filterNotNull(), karooSystemService.streamUserProfile()) { weatherData, userProfile ->
StreamData(weatherData, userProfile)
}
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(applicationContext) val refreshRate = karooSystemService.getRefreshRateInMilliseconds(applicationContext)
currentWeatherData currentWeatherData.filterNotNull()
.filterNotNull()
.throttle(refreshRate) .throttle(refreshRate)
.collect { data -> .collect { (data, userProfile) ->
val value = getValue(data) val value = getValue(data, userProfile)
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value") Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
if (value != null) { if (value != null) {
@ -50,4 +62,12 @@ abstract class BaseDataType(
job.cancel() job.cancel()
} }
} }
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
Log.d(KarooHeadwindExtension.TAG, "Starting $dataTypeId view with $emitter")
if (getFormatDataType() != null){
emitter.onNext(UpdateGraphicConfig(formatDataTypeId = getFormatDataType()))
}
}
} }

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){ class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){
override fun getValue(data: WeatherData): Double? { override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
return data.cloudCover return data.cloudCover
} }
} }

View File

@ -37,6 +37,9 @@ import de.timklge.karooheadwind.streamUpcomingRoute
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.streamWidgetSettings import de.timklge.karooheadwind.streamWidgetSettings
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.celciusInUserUnit
import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
@ -116,7 +119,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
WeatherData( WeatherData(
time = forecastTime, time = forecastTime,
temperature = forecastTemperature, temperature = forecastTemperature,
relativeHumidity = 20.0, relativeHumidity = 20,
precipitation = forecastPrecipitation, precipitation = forecastPrecipitation,
cloudCover = 3.0, cloudCover = 3.0,
sealevelPressure = 1013.25, sealevelPressure = 1013.25,
@ -139,7 +142,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
current = WeatherData( current = WeatherData(
time = timeAtFullHour, time = timeAtFullHour,
temperature = 20.0, temperature = 20.0,
relativeHumidity = 20.0, relativeHumidity = 20,
precipitation = 0.0, precipitation = 0.0,
cloudCover = 3.0, cloudCover = 3.0,
sealevelPressure = 1013.25, sealevelPressure = 1013.25,
@ -311,11 +314,11 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
arrowBitmap = baseBitmap, arrowBitmap = baseBitmap,
current = interpretation, current = interpretation,
windBearing = data.current.windDirection.roundToInt(), windBearing = data.current.windDirection.roundToInt(),
windSpeed = data.current.windSpeed.roundToInt(), windSpeed = msInUserUnit(data.current.windSpeed, settingsAndProfile.isImperial).roundToInt(),
windGusts = data.current.windGusts.roundToInt(), windGusts = msInUserUnit(data.current.windGusts, settingsAndProfile.isImperial).roundToInt(),
precipitation = data.current.precipitation, precipitation = millimetersInUserUnit(data.current.precipitation, settingsAndProfile.isImperial),
precipitationProbability = null, precipitationProbability = null,
temperature = data.current.temperature.roundToInt(), temperature = celciusInUserUnit(data.current.temperature, settingsAndProfile.isImperialTemperature).roundToInt(),
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS, temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
timeLabel = formattedTime, timeLabel = formattedTime,
dateLabel = if (hasNewDate) formattedDate else null, dateLabel = if (hasNewDate) formattedDate else null,
@ -337,11 +340,11 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
arrowBitmap = baseBitmap, arrowBitmap = baseBitmap,
current = interpretation, current = interpretation,
windBearing = weatherData?.windDirection?.roundToInt() ?: 0, windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0, windSpeed = msInUserUnit(weatherData?.windSpeed ?: 0.0, settingsAndProfile.isImperial).roundToInt(),
windGusts = weatherData?.windGusts?.roundToInt() ?: 0, windGusts = msInUserUnit(weatherData?.windGusts ?: 0.0, settingsAndProfile.isImperial).roundToInt(),
precipitation = weatherData?.precipitation ?: 0.0, precipitation = millimetersInUserUnit(weatherData?.precipitation ?: 0.0, settingsAndProfile.isImperial),
precipitationProbability = weatherData?.precipitationProbability?.toInt(), precipitationProbability = weatherData?.precipitationProbability?.toInt(),
temperature = weatherData?.temperature?.roundToInt() ?: 0, temperature = celciusInUserUnit(weatherData?.temperature ?: 0.0, settingsAndProfile.isImperialTemperature).roundToInt(),
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS, temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
timeLabel = formattedTime, timeLabel = formattedTime,
dateLabel = if (hasNewDate) formattedDate else null, dateLabel = if (hasNewDate) formattedDate else null,

View File

@ -44,7 +44,6 @@ fun GraphicalForecast(
provider = ImageProvider(getWeatherIcon(current, isNight)), provider = ImageProvider(getWeatherIcon(current, isNight)),
contentDescription = "Current weather information", contentDescription = "Current weather information",
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
) )
} }

View File

@ -14,7 +14,9 @@ import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamDatatypeIsVisible import de.timklge.karooheadwind.streamDatatypeIsVisible
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.msInUserUnit
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
import io.hammerhead.karooext.internal.Emitter import io.hammerhead.karooext.internal.Emitter
@ -24,6 +26,7 @@ import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.HardwareType import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -63,7 +66,7 @@ class HeadwindDirectionDataType(
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
var returnValue = 0.0 var returnValue = 0.0
if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){ if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
var errorCode = 1.0 var errorCode = 1.0
var headingResponse = streamData.headingResponse var headingResponse = streamData.headingResponse
@ -71,7 +74,7 @@ class HeadwindDirectionDataType(
headingResponse = HeadingResponse.NoWeatherData headingResponse = HeadingResponse.NoWeatherData
} }
if (streamData.settings?.welcomeDialogAccepted == false){ if (streamData.settings.welcomeDialogAccepted == false){
errorCode = ERROR_APP_NOT_SET_UP.toDouble() errorCode = ERROR_APP_NOT_SET_UP.toDouble()
} else if (headingResponse is HeadingResponse.NoGps){ } else if (headingResponse is HeadingResponse.NoGps){
errorCode = ERROR_NO_GPS.toDouble() errorCode = ERROR_NO_GPS.toDouble()
@ -106,7 +109,12 @@ class HeadwindDirectionDataType(
} }
} }
data class DirectionAndSpeed(val bearing: Double, val speed: Double?, val isVisible: Boolean) data class DirectionAndSpeed(
val bearing: Double,
val speed: Double?,
val isVisible: Boolean,
val isImperial: Boolean
)
private fun previewFlow(): Flow<DirectionAndSpeed> { private fun previewFlow(): Flow<DirectionAndSpeed> {
return flow { return flow {
@ -114,7 +122,12 @@ class HeadwindDirectionDataType(
val bearing = (0..360).random().toDouble() val bearing = (0..360).random().toDouble()
val windSpeed = (0..20).random() val windSpeed = (0..20).random()
emit(DirectionAndSpeed(bearing, windSpeed.toDouble(), true)) emit(DirectionAndSpeed(
bearing,
windSpeed.toDouble(),
true,
true
))
delay(2_000) delay(2_000)
} }
@ -144,8 +157,8 @@ class HeadwindDirectionDataType(
emitAll(UserWindSpeedDataType.streamValues(context, karooSystem)) emitAll(UserWindSpeedDataType.streamValues(context, karooSystem))
} }
combine(directionFlow, speedFlow, karooSystem.streamDatatypeIsVisible(dataTypeId)) { direction, speed, isVisible -> combine(directionFlow, speedFlow, karooSystem.streamDatatypeIsVisible(dataTypeId), karooSystem.streamUserProfile()) { direction, speed, isVisible, profile ->
DirectionAndSpeed(direction, speed, isVisible) DirectionAndSpeed(direction, speed, isVisible, profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
} }
} }
@ -162,14 +175,15 @@ class HeadwindDirectionDataType(
} }
val windDirection = streamData.bearing val windDirection = streamData.bearing
val windSpeed = streamData.speed val windSpeed = streamData.speed ?: 0.0
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
val result = glance.compose(context, DpSize.Unspecified) { val result = glance.compose(context, DpSize.Unspecified) {
HeadwindDirection( HeadwindDirection(
baseBitmap, baseBitmap,
windDirection.roundToInt(), windDirection.roundToInt(),
config.textSize, config.textSize,
windSpeed?.toInt()?.toString() ?: "", windSpeedUserUnit.roundToInt().toString(),
preview = config.preview, preview = config.preview,
wideMode = false wideMode = false
) )

View File

@ -11,9 +11,12 @@ import de.timklge.karooheadwind.throttle
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
import io.hammerhead.karooext.internal.Emitter import io.hammerhead.karooext.internal.Emitter
import io.hammerhead.karooext.internal.ViewEmitter
import io.hammerhead.karooext.models.DataPoint import io.hammerhead.karooext.models.DataPoint
import io.hammerhead.karooext.models.DataType import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.ViewConfig
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
@ -49,5 +52,9 @@ class HeadwindSpeedDataType(
job.cancel() job.cancel()
} }
} }
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
emitter.onNext(UpdateGraphicConfig(formatDataTypeId = DataType.Type.SPEED))
}
} }

View File

@ -1,11 +1,13 @@
package de.timklge.karooheadwind.datatypes package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){ class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){
override fun getValue(data: WeatherData): Double { override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.precipitation return millimetersInUserUnit(data.precipitation, userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
} }
} }

View File

@ -129,29 +129,8 @@ class RelativeGradeDataType(private val karooSystemService: KarooSystemService,
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(context) val refreshRate = karooSystemService.getRefreshRateInMilliseconds(context)
val windSpeedFlow = combine(context.streamSettings(karooSystemService), karooSystemService.streamUserProfile(), context.streamCurrentWeatherData(karooSystemService).filterNotNull()) { settings, profile, weatherData -> val windSpeedFlow = context.streamCurrentWeatherData(karooSystemService).filterNotNull().map { weatherData ->
val isOpenMeteo = settings.weatherProvider == WeatherDataProvider.OPEN_METEO weatherData.windSpeed
val profileIsImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
if (isOpenMeteo) {
if (profileIsImperial) { // OpenMeteo returns wind speed in mph
val windSpeedInMilesPerHour = weatherData.windSpeed
windSpeedInMilesPerHour * 0.44704
} else { // Wind speed reported by openmeteo is in km/h
val windSpeedInKmh = weatherData.windSpeed
windSpeedInKmh * 0.277778
}
} else {
if (profileIsImperial) { // OpenWeatherMap returns wind speed in mph
val windSpeedInMilesPerHour = weatherData.windSpeed
windSpeedInMilesPerHour * 0.44704
} else { // Wind speed reported by openweathermap is in m/s
weatherData.windSpeed
}
}
} }
data class StreamValues( data class StreamValues(

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){ class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){
override fun getValue(data: WeatherData): Double? { override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
return data.relativeHumidity return data.relativeHumidity.toDouble()
} }
} }

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){ class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){
override fun getValue(data: WeatherData): Double? { override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
return data.sealevelPressure return data.sealevelPressure
} }
} }

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){ class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){
override fun getValue(data: WeatherData): Double? { override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
return data.surfacePressure return data.surfacePressure
} }
} }

View File

@ -25,6 +25,7 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
@ -133,11 +134,7 @@ class TailwindAndRideSpeedDataType(
val absoluteWindDirection = weatherData?.windDirection val absoluteWindDirection = weatherData?.windDirection
val windSpeed = weatherData?.windSpeed val windSpeed = weatherData?.windSpeed
val gustSpeed = weatherData?.windGusts val gustSpeed = weatherData?.windGusts
val rideSpeed = if (isImperial){ val rideSpeed = rideSpeedInMs
rideSpeedInMs * 2.23694
} else {
rideSpeedInMs * 3.6
}
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible) StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
} }
@ -169,15 +166,21 @@ class TailwindAndRideSpeedDataType(
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180 WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
} }
val text = streamData.rideSpeed?.let { String.format(Locale.current.platformLocale, "%.1f", it) } ?: "" val rideSpeedInUserUnit = msInUserUnit(streamData.rideSpeed ?: 0.0, streamData.isImperial)
val text = String.format(Locale.current.platformLocale, "%.1f", rideSpeedInUserUnit)
val wideMode = config.gridSize.first == 60 val wideMode = config.gridSize.first == 60
val gustSpeedInUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
val gustSpeedAddon = if (wideMode) { val gustSpeedAddon = if (wideMode) {
"-${streamData.gustSpeed?.roundToInt() ?: 0}" "-${gustSpeedInUserUnit.roundToInt()}"
} else { } else {
"" ""
} }
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
val subtextWithSign = when (streamData.settings.windDirectionIndicatorTextSetting) { val subtextWithSign = when (streamData.settings.windDirectionIndicatorTextSetting) {
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> { WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
@ -186,9 +189,12 @@ class TailwindAndRideSpeedDataType(
val sign = if (headwindSpeed < 0) "+" else { val sign = if (headwindSpeed < 0) "+" else {
if (headwindSpeed > 0) "-" else "" if (headwindSpeed > 0) "-" else ""
} }
"$sign${headwindSpeed.roundToInt().absoluteValue} ${windSpeed.roundToInt()}${gustSpeedAddon}"
val headwindSpeedUserUnit = msInUserUnit(headwindSpeed, streamData.isImperial)
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue} ${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}"
} }
WindDirectionIndicatorTextSetting.WIND_SPEED -> "${windSpeed.roundToInt()}${gustSpeedAddon}" WindDirectionIndicatorTextSetting.WIND_SPEED -> "${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}"
WindDirectionIndicatorTextSetting.NONE -> "" WindDirectionIndicatorTextSetting.NONE -> ""
} }
@ -197,11 +203,7 @@ class TailwindAndRideSpeedDataType(
if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) { if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
val windSpeedInKmh = if (streamData.isImperial == true){ val windSpeedInKmh = headwindSpeed * 3.6
headwindSpeed / 2.23694 * 3.6
} else {
headwindSpeed
}
dayColor = interpolateWindColor(windSpeedInKmh, false, context) dayColor = interpolateWindColor(windSpeedInKmh, false, context)
nightColor = interpolateWindColor(windSpeedInKmh, true, context) nightColor = interpolateWindColor(windSpeedInKmh, true, context)
} }

View File

@ -21,6 +21,7 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl import io.hammerhead.karooext.extension.DataTypeImpl
@ -120,13 +121,8 @@ class TailwindDataType(
val absoluteWindDirection = weatherData?.windDirection val absoluteWindDirection = weatherData?.windDirection
val windSpeed = weatherData?.windSpeed val windSpeed = weatherData?.windSpeed
val gustSpeed = weatherData?.windGusts val gustSpeed = weatherData?.windGusts
val rideSpeed = if (isImperial){
rideSpeedInMs * 2.23694
} else {
rideSpeedInMs * 3.6
}
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible) StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeedInMs, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
} }
} }
@ -164,24 +160,26 @@ class TailwindDataType(
val sign = if (headwindSpeed < 0) "+" else { val sign = if (headwindSpeed < 0) "+" else {
if (headwindSpeed > 0) "-" else "" if (headwindSpeed > 0) "-" else ""
} }
"$sign${headwindSpeed.roundToInt().absoluteValue}"
val headwindSpeedUserUnit = msInUserUnit(headwindSpeed, streamData.isImperial)
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue}"
} }
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString() WindDirectionIndicatorTextSetting.WIND_SPEED -> msInUserUnit(windSpeed, streamData.isImperial).roundToInt().toString()
WindDirectionIndicatorTextSetting.NONE -> "" WindDirectionIndicatorTextSetting.NONE -> ""
} }
val subtext = "${windSpeed.roundToInt()}-${streamData.gustSpeed?.roundToInt()}" val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
val gustSpeedUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
val subtext = "${windSpeedUserUnit.roundToInt()}-${gustSpeedUserUnit.roundToInt()}"
var dayColor = Color(ContextCompat.getColor(context, R.color.black)) var dayColor = Color(ContextCompat.getColor(context, R.color.black))
var nightColor = Color(ContextCompat.getColor(context, R.color.white)) var nightColor = Color(ContextCompat.getColor(context, R.color.white))
if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) { if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
val windSpeedInKmh = if (streamData.isImperial){ val windSpeedInKmh = headwindSpeed * 3.6
headwindSpeed / 2.23694 * 3.6
} else {
headwindSpeed
}
dayColor = interpolateWindColor(windSpeedInKmh, false, context) dayColor = interpolateWindColor(windSpeedInKmh, false, context)
nightColor = interpolateWindColor(windSpeedInKmh, true, context) nightColor = interpolateWindColor(windSpeedInKmh, true, context)
} }

View File

@ -3,9 +3,15 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.UserProfile
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){ class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
override fun getValue(data: WeatherData): Double { override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.temperature return data.temperature
} }
override fun getFormatDataType(): String? {
return DataType.Type.TEMPERATURE
}
} }

View File

@ -24,6 +24,9 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
import de.timklge.karooheadwind.streamSettings import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.celciusInUserUnit
import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
@ -85,7 +88,7 @@ class WeatherDataType(
emit(StreamData( emit(StreamData(
WeatherData( WeatherData(
Instant.now().epochSecond, 0.0, Instant.now().epochSecond, 0.0,
20.0, 50.0, 3.0, 0.0, 1013.25, 980.0, 15.0, 30.0, 30.0, 20, 50.0, 3.0, 0.0, 1013.25, 980.0, 15.0, 30.0, 30.0,
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false, WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false,
isNight = listOf(true, false).random() isNight = listOf(true, false).random()
), HeadwindSettings(), isVisible = true)) ), HeadwindSettings(), isVisible = true))
@ -147,11 +150,11 @@ class WeatherDataType(
baseBitmap, baseBitmap,
current = interpretation, current = interpretation,
windBearing = data.windDirection.roundToInt(), windBearing = data.windDirection.roundToInt(),
windSpeed = data.windSpeed.roundToInt(), windSpeed = msInUserUnit(data.windSpeed, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
windGusts = data.windGusts.roundToInt(), windGusts = msInUserUnit(data.windGusts, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
precipitation = data.precipitation, precipitation = millimetersInUserUnit(data.precipitation, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
precipitationProbability = null, precipitationProbability = null,
temperature = data.temperature.roundToInt(), temperature = celciusInUserUnit(data.temperature, userProfile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT, temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedTime, timeLabel = formattedTime,
rowAlignment = when (config.alignment){ rowAlignment = when (config.alignment){

View File

@ -26,6 +26,7 @@ import io.hammerhead.karooext.internal.ViewEmitter
import io.hammerhead.karooext.models.ShowCustomStreamState import io.hammerhead.karooext.models.ShowCustomStreamState
import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -51,7 +52,7 @@ class WindDirectionDataType(val karooSystem: KarooSystemService, context: Contex
) )
} }
override fun getValue(data: WeatherData): Double { override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.windDirection return data.windDirection
} }

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){ class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){
override fun getValue(data: WeatherData): Double { override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.windGusts return data.windGusts
} }
} }

View File

@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){ class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){
override fun getValue(data: WeatherData): Double { override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.windSpeed return data.windSpeed
} }
} }

View File

@ -37,6 +37,9 @@ import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamStats import de.timklge.karooheadwind.streamStats
import de.timklge.karooheadwind.streamUpcomingRoute import de.timklge.karooheadwind.streamUpcomingRoute
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.util.celciusInUserUnit
import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.util.msInUserUnit
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
import java.time.Instant import java.time.Instant
@ -110,10 +113,10 @@ fun WeatherScreen(onFinish: () -> Unit) {
baseBitmap = baseBitmap, baseBitmap = baseBitmap,
current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode), current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode),
windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0, windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0,
windSpeed = currentWeatherData?.windSpeed?.roundToInt() ?: 0, windSpeed = msInUserUnit(currentWeatherData?.windSpeed ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
windGusts = currentWeatherData?.windGusts?.roundToInt() ?: 0, windGusts = msInUserUnit(currentWeatherData?.windGusts ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
precipitation = currentWeatherData?.precipitation ?: 0.0, precipitation = millimetersInUserUnit(currentWeatherData?.precipitation ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
temperature = currentWeatherData?.temperature?.toInt() ?: 0, temperature = celciusInUserUnit(currentWeatherData?.temperature ?: 0.0, profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT, temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedTime, timeLabel = formattedTime,
dateLabel = formattedDate, dateLabel = formattedDate,
@ -230,10 +233,10 @@ fun WeatherScreen(onFinish: () -> Unit) {
baseBitmap, baseBitmap,
current = interpretation, current = interpretation,
windBearing = weatherData?.windDirection?.roundToInt() ?: 0, windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0, windSpeed = msInUserUnit(currentWeatherData?.windSpeed ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
windGusts = weatherData?.windGusts?.roundToInt() ?: 0, windGusts = msInUserUnit(currentWeatherData?.windGusts ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
precipitation = weatherData?.precipitation ?: 0.0, precipitation = millimetersInUserUnit(weatherData?.precipitation ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
temperature = weatherData?.temperature?.toInt() ?: 0, temperature = celciusInUserUnit(weatherData?.temperature ?: 0.0, profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT, temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
timeLabel = formattedForecastTime, timeLabel = formattedForecastTime,
dateLabel = formattedForecastDate, dateLabel = formattedForecastDate,

View File

@ -50,7 +50,7 @@ fun WeatherWidget(
isImperial: Boolean, isImperial: Boolean,
isNight: Boolean isNight: Boolean
) { ) {
val fontSize = 20.sp val fontSize = 18.sp
Row( Row(
modifier = Modifier.fillMaxWidth().padding(5.dp), modifier = Modifier.fillMaxWidth().padding(5.dp),
@ -106,7 +106,6 @@ fun WeatherWidget(
) )
Column(horizontalAlignment = Alignment.End) { Column(horizontalAlignment = Alignment.End) {
// Temperature (larger)
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {

View File

@ -0,0 +1,26 @@
package de.timklge.karooheadwind.util
fun celciusInUserUnit(celcius: Double, isImperial: Boolean): Double {
return if (isImperial) {
celcius * 9.0 / 5 + 32.0
} else {
celcius
}
}
fun millimetersInUserUnit(millimeters: Double, isImperial: Boolean): Double {
return if (isImperial) {
millimeters / 25.4
} else {
millimeters
}
}
// Returns the given speed value (m / s) in user unit (km/h or mph)
fun msInUserUnit(ms: Double, isImperial: Boolean): Double {
return if (isImperial) {
ms * 2.2369362920544
} else {
ms * 3.6
}
}

View File

@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable
data class WeatherData( data class WeatherData(
val time: Long, val time: Long,
val temperature: Double, val temperature: Double,
val relativeHumidity: Double? = null, val relativeHumidity: Int,
val precipitation: Double, val precipitation: Double,
val precipitationProbability: Double? = null, val precipitationProbability: Double? = null,
val cloudCover: Double? = null, val cloudCover: Double,
val sealevelPressure: Double? = null, val sealevelPressure: Double,
val surfacePressure: Double? = null, val surfacePressure: Double,
val windSpeed: Double, val windSpeed: Double,
val windDirection: Double, val windDirection: Double,
val windGusts: Double, val windGusts: Double,

View File

@ -12,7 +12,7 @@ data class OpenMeteoWeatherData(
@SerialName("precipitation") val precipitation: Double, @SerialName("precipitation") val precipitation: Double,
@SerialName("cloud_cover") val cloudCover: Int, @SerialName("cloud_cover") val cloudCover: Int,
@SerialName("surface_pressure") val surfacePressure: Double, @SerialName("surface_pressure") val surfacePressure: Double,
@SerialName("pressure_msl") val sealevelPressure: Double? = null, @SerialName("pressure_msl") val sealevelPressure: Double,
@SerialName("wind_speed_10m") val windSpeed: Double, @SerialName("wind_speed_10m") val windSpeed: Double,
@SerialName("wind_direction_10m") val windDirection: Double, @SerialName("wind_direction_10m") val windDirection: Double,
@SerialName("wind_gusts_10m") val windGusts: Double, @SerialName("wind_gusts_10m") val windGusts: Double,
@ -21,7 +21,7 @@ data class OpenMeteoWeatherData(
) { ) {
fun toWeatherData(): WeatherData = WeatherData( fun toWeatherData(): WeatherData = WeatherData(
temperature = temperature, temperature = temperature,
relativeHumidity = relativeHumidity.toDouble(), relativeHumidity = relativeHumidity,
precipitation = precipitation, precipitation = precipitation,
cloudCover = cloudCover.toDouble(), cloudCover = cloudCover.toDouble(),
surfacePressure = surfacePressure, surfacePressure = surfacePressure,
@ -32,7 +32,7 @@ data class OpenMeteoWeatherData(
weatherCode = weatherCode, weatherCode = weatherCode,
time = time, time = time,
isForecast = false, isForecast = false,
isNight = isDay == 0 isNight = isDay == 0,
) )
} }

View File

@ -14,7 +14,11 @@ data class OpenMeteoWeatherForecastData(
@SerialName("wind_speed_10m") val windSpeed: List<Double>, @SerialName("wind_speed_10m") val windSpeed: List<Double>,
@SerialName("wind_direction_10m") val windDirection: List<Double>, @SerialName("wind_direction_10m") val windDirection: List<Double>,
@SerialName("wind_gusts_10m") val windGusts: List<Double>, @SerialName("wind_gusts_10m") val windGusts: List<Double>,
@SerialName("cloud_cover") val cloudCover: List<Double>,
@SerialName("surface_pressure") val surfacePressure: List<Double>,
@SerialName("pressure_msl") val sealevelPressure: List<Double>,
@SerialName("is_day") val isDay: List<Int>, @SerialName("is_day") val isDay: List<Int>,
@SerialName("relative_humidity_2m") val relativeHumidity: List<Int>,
) { ) {
fun toWeatherData(): List<WeatherData> { fun toWeatherData(): List<WeatherData> {
return time.mapIndexed { index, t -> return time.mapIndexed { index, t ->
@ -29,6 +33,10 @@ data class OpenMeteoWeatherForecastData(
isNight = isDay[index] == 0, isNight = isDay[index] == 0,
time = t, time = t,
isForecast = true, isForecast = true,
cloudCover = cloudCover[index],
surfacePressure = surfacePressure[index],
sealevelPressure = sealevelPressure[index],
relativeHumidity = relativeHumidity[index],
) )
} }
} }

View File

@ -3,10 +3,7 @@ package de.timklge.karooheadwind.weatherprovider.openmeteo
import android.util.Log import android.util.Log
import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.PrecipitationUnit
import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.WeatherDataProvider import de.timklge.karooheadwind.WeatherDataProvider
import de.timklge.karooheadwind.WindUnit
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.jsonWithUnknownKeys import de.timklge.karooheadwind.jsonWithUnknownKeys
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
@ -28,16 +25,12 @@ import kotlin.time.Duration.Companion.seconds
class OpenMeteoWeatherProvider : WeatherProvider { class OpenMeteoWeatherProvider : WeatherProvider {
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private suspend fun makeOpenMeteoWeatherRequest(karooSystemService: KarooSystemService, gpsCoordinates: List<GpsCoordinates>, settings: HeadwindSettings, profile: UserProfile?): HttpResponseState.Complete { private suspend fun makeOpenMeteoWeatherRequest(karooSystemService: KarooSystemService, gpsCoordinates: List<GpsCoordinates>): 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
val windUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR
val response = callbackFlow { val response = callbackFlow {
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12 // https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12
val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) } val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) }
val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) } val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) }
val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}&current=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,is_day&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=${windUnit.id}&precipitation_unit=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}" val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}&current=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,is_day,surface_pressure,pressure_msl,relative_humidity_2m,cloud_cover&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=ms"
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...") Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
@ -84,7 +77,7 @@ class OpenMeteoWeatherProvider : WeatherProvider {
settings: HeadwindSettings, settings: HeadwindSettings,
profile: UserProfile? profile: UserProfile?
): WeatherDataResponse { ): WeatherDataResponse {
val openMeteoResponse = makeOpenMeteoWeatherRequest(karooSystem, coordinates, settings, profile) val openMeteoResponse = makeOpenMeteoWeatherRequest(karooSystem, coordinates)
val responseBody = openMeteoResponse.body?.let { String(it) } ?: throw WeatherProviderException(500, "Null response from OpenMeteo") val responseBody = openMeteoResponse.body?.let { String(it) } ?: throw WeatherProviderException(500, "Null response from OpenMeteo")
val weatherData = if (coordinates.size == 1) { val weatherData = if (coordinates.size == 1) {

View File

@ -33,7 +33,7 @@ data class OpenWeatherMapForecastData(
return WeatherData( return WeatherData(
temperature = temp, temperature = temp,
relativeHumidity = humidity.toDouble(), relativeHumidity = humidity,
precipitation = rain?.h1 ?: 0.0, precipitation = rain?.h1 ?: 0.0,
cloudCover = clouds.toDouble(), cloudCover = clouds.toDouble(),
surfacePressure = pressure.toDouble(), surfacePressure = pressure.toDouble(),

View File

@ -23,7 +23,7 @@ data class OpenWeatherMapWeatherData(
fun toWeatherData(): WeatherData = WeatherData( fun toWeatherData(): WeatherData = WeatherData(
temperature = temp, temperature = temp,
relativeHumidity = humidity.toDouble(), relativeHumidity = humidity,
precipitation = rain?.h1 ?: 0.0, precipitation = rain?.h1 ?: 0.0,
cloudCover = clouds.toDouble(), cloudCover = clouds.toDouble(),
surfacePressure = pressure.toDouble(), surfacePressure = pressure.toDouble(),

View File

@ -69,7 +69,7 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
profile: UserProfile? profile: UserProfile?
): WeatherDataResponse { ): WeatherDataResponse {
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey, profile) val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap") val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
val responses = mutableListOf<WeatherDataForLocation>() val responses = mutableListOf<WeatherDataForLocation>()
@ -89,21 +89,15 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
private suspend fun makeOpenWeatherMapRequest( private suspend fun makeOpenWeatherMapRequest(
service: KarooSystemService, service: KarooSystemService,
coordinates: List<GpsCoordinates>, coordinates: List<GpsCoordinates>,
apiKey: String, apiKey: String
profile: UserProfile?
): HttpResponseState.Complete { ): HttpResponseState.Complete {
val response = callbackFlow { val response = callbackFlow {
// OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature // OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature
val unitsString = if (profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL || profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) {
"imperial"
} else {
"metric"
}
val coordinate = coordinates.first() val coordinate = coordinates.first()
// URL API 3.0 with onecall endpoint // URL API 3.0 with onecall endpoint
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" + val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" +
"&appid=$apiKey&exclude=minutely,daily,alerts&units=${unitsString}" "&appid=$apiKey&exclude=minutely,daily,alerts&units=metric"
Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url") Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url")