Add UV Index to current weather and forecast (#159)

This commit is contained in:
Julien B. 2025-07-07 17:41:55 +02:00 committed by GitHub
parent be7ca192b2
commit 5169048143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 69 additions and 26 deletions

View File

@ -294,9 +294,9 @@ fun lerpWeather(
weatherCode = closestWeatherData.weatherCode, weatherCode = closestWeatherData.weatherCode,
isForecast = closestWeatherData.isForecast, isForecast = closestWeatherData.isForecast,
isNight = closestWeatherData.isNight, isNight = closestWeatherData.isNight,
uvi = start.uvi + (end.uvi - start.uvi) * factor
) )
} }
fun lerpWeatherTime( fun lerpWeatherTime(
weatherData: List<WeatherData>?, weatherData: List<WeatherData>?,
currentWeatherData: WeatherData currentWeatherData: WeatherData

View File

@ -17,6 +17,7 @@ import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
import de.timklge.karooheadwind.datatypes.TemperatureDataType import de.timklge.karooheadwind.datatypes.TemperatureDataType
import de.timklge.karooheadwind.datatypes.UviDataType
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
@ -83,7 +84,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
WindDirectionAndSpeedDataType(karooSystem, applicationContext), WindDirectionAndSpeedDataType(karooSystem, applicationContext),
RelativeGradeDataType(karooSystem, applicationContext), RelativeGradeDataType(karooSystem, applicationContext),
RelativeElevationGainDataType(karooSystem, applicationContext), RelativeElevationGainDataType(karooSystem, applicationContext),
TemperatureDataType(karooSystem, applicationContext) TemperatureDataType(karooSystem, applicationContext),
UviDataType(karooSystem, applicationContext)
) )
} }

View File

@ -65,14 +65,14 @@ 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.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) { abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
@Composable @Composable
abstract fun RenderWidget(arrowBitmap: Bitmap, abstract fun RenderWidget(
arrowBitmap: Bitmap,
current: WeatherInterpretation, current: WeatherInterpretation,
windBearing: Int, windBearing: Int,
windSpeed: Int, windSpeed: Int,
@ -85,7 +85,9 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
dateLabel: String?, dateLabel: String?,
distance: Double?, distance: Double?,
isImperial: Boolean, isImperial: Boolean,
isNight: Boolean) isNight: Boolean,
uvi: Double
)
@OptIn(ExperimentalGlanceRemoteViewsApi::class) @OptIn(ExperimentalGlanceRemoteViewsApi::class)
private val glance = GlanceRemoteViews() private val glance = GlanceRemoteViews()
@ -107,6 +109,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
val weatherData = (0..<12).map { val weatherData = (0..<12).map {
val forecastTime = timeAtFullHour + it * 60 * 60 val forecastTime = timeAtFullHour + it * 60 * 60
val forecastTemperature = 20.0 + (-20..20).random() val forecastTemperature = 20.0 + (-20..20).random()
val forecastUvi = 0.0 + (0..12).random().toDouble()
val forecastPrecipitation = 0.0 + (0..10).random() val forecastPrecipitation = 0.0 + (0..10).random()
val forecastPrecipitationProbability = (0..100).random() val forecastPrecipitationProbability = (0..100).random()
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random() val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
@ -127,7 +130,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
windGusts = forecastWindGusts, windGusts = forecastWindGusts,
weatherCode = forecastWeatherCode, weatherCode = forecastWeatherCode,
isForecast = true, isForecast = true,
isNight = it < 2 isNight = it < 2,
uvi = forecastUvi
) )
} }
@ -149,7 +153,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
windGusts = 10.0, windGusts = 10.0,
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(), weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
isForecast = false, isForecast = false,
isNight = false isNight = false,
uvi = 2.0
), ),
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour), coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
timezone = "UTC", timezone = "UTC",
@ -333,7 +338,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
dateLabel = if (hasNewDate) formattedDate else null, dateLabel = if (hasNewDate) formattedDate else null,
distance = null, distance = null,
isImperial = settingsAndProfile.isImperial, isImperial = settingsAndProfile.isImperial,
isNight = data.current.isNight isNight = data.current.isNight,
uvi = data.current.uvi
) )
previousDate = formattedDate previousDate = formattedDate
@ -359,7 +365,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
dateLabel = if (hasNewDate) formattedDate else null, dateLabel = if (hasNewDate) formattedDate else null,
distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null, distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null,
isImperial = settingsAndProfile.isImperial, isImperial = settingsAndProfile.isImperial,
isNight = weatherData?.isNight == true isNight = weatherData?.isNight == true,
uvi = weatherData?.uvi ?: 0.0
) )
previousDate = formattedDate previousDate = formattedDate

View File

@ -94,6 +94,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
val forecastWindSpeed = 0.0 + (0..10).random() val forecastWindSpeed = 0.0 + (0..10).random()
val forecastWindDirection = 0.0 + (0..360).random() val forecastWindDirection = 0.0 + (0..360).random()
val forecastWindGusts = 0.0 + (0..10).random() val forecastWindGusts = 0.0 + (0..10).random()
val forcastUvi = 0.0 + (0..12).random()
WeatherData( WeatherData(
time = forecastTime, time = forecastTime,
temperature = forecastTemperature, temperature = forecastTemperature,
@ -108,7 +109,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
windGusts = forecastWindGusts, windGusts = forecastWindGusts,
weatherCode = forecastWeatherCode, weatherCode = forecastWeatherCode,
isForecast = true, isForecast = true,
isNight = it < 2 isNight = it < 2,
uvi = forcastUvi
) )
} }
@ -130,7 +132,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
windGusts = 10.0, windGusts = 10.0,
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(), weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
isForecast = false, isForecast = false,
isNight = false isNight = false,
uvi = 2.0
), ),
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour), coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
timezone = "UTC", timezone = "UTC",

View File

@ -0,0 +1,12 @@
package de.timklge.karooheadwind.datatypes
import android.content.Context
import de.timklge.karooheadwind.weatherprovider.WeatherData
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile
class UviDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "uvi"){
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
return data.uvi
}
}

View File

@ -23,6 +23,7 @@ class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataTyp
distance: Double?, distance: Double?,
isImperial: Boolean, isImperial: Boolean,
isNight: Boolean, isNight: Boolean,
uvi: Double,
) { ) {
Weather( Weather(
arrowBitmap = arrowBitmap, arrowBitmap = arrowBitmap,

View File

@ -17,6 +17,7 @@ data class WeatherData(
val windGusts: Double, val windGusts: Double,
val weatherCode: Int, val weatherCode: Int,
val isForecast: Boolean, val isForecast: Boolean,
val isNight: Boolean val isNight: Boolean,
val uvi: Double,
) )

View File

@ -18,6 +18,7 @@ data class OpenMeteoWeatherData(
@SerialName("wind_gusts_10m") val windGusts: Double, @SerialName("wind_gusts_10m") val windGusts: Double,
@SerialName("weather_code") val weatherCode: Int, @SerialName("weather_code") val weatherCode: Int,
@SerialName("is_day") val isDay: Int, @SerialName("is_day") val isDay: Int,
@SerialName("uv_index") val uvi: Double,
) { ) {
fun toWeatherData(): WeatherData = WeatherData( fun toWeatherData(): WeatherData = WeatherData(
temperature = temperature, temperature = temperature,
@ -33,6 +34,7 @@ data class OpenMeteoWeatherData(
time = time, time = time,
isForecast = false, isForecast = false,
isNight = isDay == 0, isNight = isDay == 0,
uvi = uvi
) )
} }

View File

@ -19,6 +19,7 @@ data class OpenMeteoWeatherForecastData(
@SerialName("pressure_msl") val sealevelPressure: 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>, @SerialName("relative_humidity_2m") val relativeHumidity: List<Int>,
@SerialName("uv_index") val uvi: List<Double>,
) { ) {
fun toWeatherData(): List<WeatherData> { fun toWeatherData(): List<WeatherData> {
return time.mapIndexed { index, t -> return time.mapIndexed { index, t ->
@ -37,6 +38,7 @@ data class OpenMeteoWeatherForecastData(
surfacePressure = surfacePressure[index], surfacePressure = surfacePressure[index],
sealevelPressure = sealevelPressure[index], sealevelPressure = sealevelPressure[index],
relativeHumidity = relativeHumidity[index], relativeHumidity = relativeHumidity[index],
uvi = uvi[index]
) )
} }
} }

View File

@ -30,7 +30,7 @@ class OpenMeteoWeatherProvider : WeatherProvider {
// 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,surface_pressure,pressure_msl,relative_humidity_2m,cloud_cover&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=ms" val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}&current=is_day,surface_pressure,pressure_msl,uv_index,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=uv_index,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}...")

View File

@ -20,7 +20,8 @@ data class OpenWeatherMapForecastData(
val pop: Double, val pop: Double,
val rain: Rain? = null, val rain: Rain? = null,
val snow: Snow? = null, val snow: Snow? = null,
val weather: List<Weather> val weather: List<Weather>,
val uvi: Double,
) { ) {
fun toWeatherData(currentWeatherData: OpenWeatherMapWeatherData): WeatherData { fun toWeatherData(currentWeatherData: OpenWeatherMapWeatherData): WeatherData {
val dtInstant = Instant.ofEpochSecond(dt) val dtInstant = Instant.ofEpochSecond(dt)
@ -32,6 +33,7 @@ data class OpenWeatherMapForecastData(
val sunsetTime = sunsetInstant.atZone(ZoneOffset.UTC).toLocalTime() val sunsetTime = sunsetInstant.atZone(ZoneOffset.UTC).toLocalTime()
return WeatherData( return WeatherData(
uvi = uvi,
temperature = temp, temperature = temp,
relativeHumidity = humidity, relativeHumidity = humidity,
precipitation = rain?.h1 ?: 0.0, precipitation = rain?.h1 ?: 0.0,

View File

@ -19,9 +19,11 @@ data class OpenWeatherMapWeatherData(
val wind_gust: Double? = null, val wind_gust: Double? = null,
val rain: Rain? = null, val rain: Rain? = null,
val snow: Snow? = null, val snow: Snow? = null,
val uvi: Double,
val weather: List<Weather>){ val weather: List<Weather>){
fun toWeatherData(): WeatherData = WeatherData( fun toWeatherData(): WeatherData = WeatherData(
uvi = uvi,
temperature = temp, temperature = temp,
relativeHumidity = humidity, relativeHumidity = humidity,
precipitation = rain?.h1 ?: 0.0, precipitation = rain?.h1 ?: 0.0,

View File

@ -22,6 +22,8 @@
<string name="sealevelPressure">Sealevel pressure</string> <string name="sealevelPressure">Sealevel pressure</string>
<string name="sealevelPressure_description">Atmospheric pressure at sea level in configured unit</string> <string name="sealevelPressure_description">Atmospheric pressure at sea level in configured unit</string>
<string name="weather_forecast">Weather Forecast</string> <string name="weather_forecast">Weather Forecast</string>
<string name="uvi">UV Index</string>
<string name="uvi_description">Current UV Index at current location</string>
<string name="weather_forecast_description">Current hourly weather forecast</string> <string name="weather_forecast_description">Current hourly weather forecast</string>
<string name="temperature_forecast">Temperature Forecast</string> <string name="temperature_forecast">Temperature Forecast</string>
<string name="temperature_forecast_description">Current hourly temperature forecast</string> <string name="temperature_forecast_description">Current hourly temperature forecast</string>

View File

@ -131,6 +131,13 @@
icon="@drawable/thermometer" icon="@drawable/thermometer"
typeId="temperature" /> typeId="temperature" />
<DataType
description="@string/uvi_description"
displayName="@string/uvi"
graphical="false"
icon="@drawable/thermometer"
typeId="uvi" />
<DataType <DataType
description="@string/relativeGrade_description" description="@string/relativeGrade_description"
displayName="@string/relativeGrade" displayName="@string/relativeGrade"