Show preview in weather, weather forecast widgets (#26)

This commit is contained in:
timklge 2025-01-17 18:19:32 +01:00 committed by GitHub
parent a98fcb875a
commit 2968cc0eef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 87 additions and 20 deletions

View File

@ -45,6 +45,8 @@ enum class WeatherInterpretation {
else -> UNKNOWN else -> UNKNOWN
} }
} }
fun getKnownWeatherCodes(): Set<Int> = setOf(0, 1, 2, 3, 45, 48, 61, 63, 65, 66, 67, 80, 81, 82, 71, 73, 75, 77, 85, 86, 51, 53, 55, 56, 57, 95, 96, 99)
} }
} }
@ -56,5 +58,5 @@ data class OpenMeteoCurrentWeatherResponse(
val timezone: String, val timezone: String,
val elevation: Double, val elevation: Double,
@SerialName("utc_offset_seconds") val utfOffsetSeconds: Int, @SerialName("utc_offset_seconds") val utfOffsetSeconds: Int,
@SerialName("hourly") val forecastData: OpenMeteoForecastData @SerialName("hourly") val forecastData: OpenMeteoForecastData?
) )

View File

@ -13,6 +13,7 @@ import androidx.glance.layout.fillMaxSize
import de.timklge.karooheadwind.HeadingResponse 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.OpenMeteoData
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.getHeadingFlow import de.timklge.karooheadwind.getHeadingFlow
import de.timklge.karooheadwind.screens.HeadwindSettings import de.timklge.karooheadwind.screens.HeadwindSettings
@ -34,7 +35,10 @@ 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.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
@ -69,6 +73,23 @@ class WeatherDataType(
} }
} }
data class StreamData(val data: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings,
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
private fun previewFlow(): Flow<StreamData> = flow {
while (true){
emit(StreamData(
OpenMeteoCurrentWeatherResponse(
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
0.0, 0.0, "Europe/Berlin", 30.0, 0,
null
), HeadwindSettings()))
delay(5_000)
}
}
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) { override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
Log.d(KarooHeadwindExtension.TAG, "Starting weather view with $emitter") Log.d(KarooHeadwindExtension.TAG, "Starting weather view with $emitter")
val configJob = CoroutineScope(Dispatchers.IO).launch { val configJob = CoroutineScope(Dispatchers.IO).launch {
@ -81,14 +102,18 @@ 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,
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
val viewJob = CoroutineScope(Dispatchers.IO).launch { val dataFlow = if (config.preview){
previewFlow()
} else {
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(karooSystem.getHeadingFlow(context)) { data, heading -> data.copy(headingResponse = heading) } .combine(karooSystem.getHeadingFlow(context)) { data, heading -> data.copy(headingResponse = heading) }
}
val viewJob = CoroutineScope(Dispatchers.IO).launch {
dataFlow
.collect { (data, settings, userProfile, headingResponse) -> .collect { (data, settings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view") Log.d(KarooHeadwindExtension.TAG, "Updating weather view")

View File

@ -25,7 +25,10 @@ import androidx.glance.layout.width
import de.timklge.karooheadwind.HeadingResponse 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.OpenMeteoData
import de.timklge.karooheadwind.OpenMeteoForecastData
import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.WeatherInterpretation
import de.timklge.karooheadwind.datatypes.WeatherDataType.StreamData
import de.timklge.karooheadwind.getHeadingFlow 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
@ -49,12 +52,16 @@ 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.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.time.format.FormatStyle import java.time.format.FormatStyle
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -70,7 +77,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 (data == null || hourOffset >= data.forecastData.weatherCode.size) { if (data == null || hourOffset >= ((data.forecastData?.weatherCode?.size) ?: 0)) {
hourOffset = 0 hourOffset = 0
} }
@ -106,6 +113,37 @@ class WeatherForecastDataType(
} }
} }
data class StreamData(val data: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings,
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null)
private fun previewFlow(): Flow<StreamData> = flow {
while (true){
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
val forecastWeatherCodes = (0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
emit(
StreamData(
OpenMeteoCurrentWeatherResponse(
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
0.0, 0.0, "Europe/Berlin", 30.0, 0,
OpenMeteoForecastData(forecastTimes, forecastTemperatures, forecastPrecipitationPropability,
forecastPrecipitation, forecastWeatherCodes, forecastWindSpeed, forecastWindDirection,
forecastWindGusts)
), HeadwindSettings())
)
delay(5_000)
}
}
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) { override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter") Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
val configJob = CoroutineScope(Dispatchers.IO).launch { val configJob = CoroutineScope(Dispatchers.IO).launch {
@ -118,16 +156,18 @@ 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, val dataFlow = if (config.preview){
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null) previewFlow()
} else {
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) }
.combine(karooSystem.getHeadingFlow(context)) { data, headingResponse -> data.copy(headingResponse = headingResponse) } .combine(karooSystem.getHeadingFlow(context)) { data, headingResponse -> data.copy(headingResponse = headingResponse) }
.collect { (data, settings, widgetSettings, userProfile, headingResponse) -> }
val viewJob = CoroutineScope(Dispatchers.IO).launch {
dataFlow.collect { (data, settings, widgetSettings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view") Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
if (data == null){ if (data == null){
@ -141,14 +181,14 @@ class WeatherForecastDataType(
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0 val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
var previousDate: String? = let { var previousDate: String? = let {
val unixTime = data.forecastData.time.firstOrNull() val unixTime = data.forecastData?.time?.firstOrNull()
val formattedDate = unixTime?.let { Instant.ofEpochSecond(it).atZone(ZoneId.systemDefault()).toLocalDate().toString() } val formattedDate = unixTime?.let { Instant.ofEpochSecond(it).atZone(ZoneId.systemDefault()).toLocalDate().toString() }
formattedDate formattedDate
} }
for (index in hourOffset..hourOffset + 2){ for (index in hourOffset..hourOffset + 2){
if (index >= data.forecastData.weatherCode.size) { if (index >= (data.forecastData?.weatherCode?.size ?: 0)) {
break break
} }
@ -160,22 +200,22 @@ class WeatherForecastDataType(
) )
} }
val interpretation = WeatherInterpretation.fromWeatherCode(data.forecastData.weatherCode[index]) val interpretation = WeatherInterpretation.fromWeatherCode(data.forecastData?.weatherCode?.get(index) ?: 0)
val unixTime = data.forecastData.time[index] val unixTime = data.forecastData?.time?.get(index) ?: 0
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime)) val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
val formattedDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) val formattedDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
val hasNewDate = formattedDate != previousDate || index == 0 val hasNewDate = formattedDate != previousDate || index == 0
Weather(baseBitmap, Weather(baseBitmap,
current = interpretation, current = interpretation,
windBearing = data.forecastData.windDirection[index].roundToInt(), windBearing = data.forecastData?.windDirection?.get(index)?.roundToInt() ?: 0,
windSpeed = data.forecastData.windSpeed[index].roundToInt(), windSpeed = data.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0,
windGusts = data.forecastData.windGusts[index].roundToInt(), windGusts = data.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0,
windSpeedUnit = settings.windUnit, windSpeedUnit = settings.windUnit,
precipitation = data.forecastData.precipitation[index], precipitation = data.forecastData?.precipitation?.get(index) ?: 0.0,
precipitationProbability = data.forecastData.precipitationProbability[index], precipitationProbability = data.forecastData?.precipitationProbability?.get(index) ?: 0,
precipitationUnit = if (userProfile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH, precipitationUnit = if (userProfile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
temperature = data.forecastData.temperature[index].roundToInt(), temperature = data.forecastData?.temperature?.get(index)?.roundToInt() ?: 0,
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,
dateLabel = if (hasNewDate) formattedDate else null dateLabel = if (hasNewDate) formattedDate else null