rearrange weather widget, add uv index

Close #1630
This commit is contained in:
MM20
2026-03-11 22:07:21 +01:00
parent 58f56b3706
commit a2621bee0b
18 changed files with 287 additions and 172 deletions

View File

@@ -65,7 +65,7 @@ object WeatherIconDefaults {
@Composable
fun themedColors(backgroundColor: Color): WeatherIconColors {
val themeColor = MaterialTheme.colorScheme.primary
val themeColor = MaterialTheme.colorScheme.secondary
val neutralColor = MaterialTheme.colorScheme.outline
return remember(backgroundColor, themeColor, neutralColor) {

View File

@@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.widgets.weather
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.text.format.DateUtils
import androidx.appcompat.app.AppCompatActivity
@@ -21,20 +22,25 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
@@ -57,11 +63,13 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -78,7 +86,6 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.WeatherLocationSearchDialog
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.Tooltip
import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon
import de.mm20.launcher2.ui.component.weather.WeatherIcon
import de.mm20.launcher2.ui.component.weather.WeatherIconDefaults
@@ -193,7 +200,7 @@ fun WeatherWidget(widget: WeatherWidget) {
color = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = MaterialTheme.transparency.surface),
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
.padding(start = 8.dp, bottom = 8.dp, end = 8.dp),
shape = MaterialTheme.shapes.small,
) {
Column(
@@ -248,16 +255,13 @@ fun CurrentWeather(
)
}
val useFahrenheit = measurementSystem == MeasurementSystem.UnitedStates
val useInches = measurementSystem == MeasurementSystem.UnitedStates
val useMph =
measurementSystem == MeasurementSystem.UnitedStates || measurementSystem == MeasurementSystem.UnitedKingdom
var bounds by remember { mutableStateOf(Rect.Zero) }
val view = LocalView.current
Column(
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.onPlaced {
val size = it.size
val offset = it.localToRoot(Offset.Zero)
@@ -288,128 +292,154 @@ fun CurrentWeather(
},
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current,
)
),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(
top = 16.dp,
end = 16.dp,
start = 16.dp,
bottom = 8.dp,
)
) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
modifier = Modifier
.weight(1f)
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = forecast.location,
style = MaterialTheme.typography.titleMedium
Text(
text = forecast.location,
style = MaterialTheme.typography.titleMedium
)
Text(
modifier = Modifier.padding(vertical = 2.dp),
text = formatTemperature(
context,
forecast.temperature.toFloat(),
measurementSystem
),
style = MaterialTheme.typography.headlineLarge,
)
Text(
text = forecast.condition,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "${forecast.provider} (${
formatTime(
LocalContext.current,
forecast.updateTime,
timeFormat,
)
}
Tooltip(
tooltipText = stringResource(R.string.preference_weather_provider)
) {
Surface(
shape = MaterialTheme.shapes.extraSmall.copy(
topStart = CornerSize(0),
topEnd = CornerSize(0),
bottomEnd = CornerSize(0)
),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = MaterialTheme.transparency.surface),
) {
Text(
text = "${forecast.provider} (${
formatTime(
LocalContext.current,
forecast.updateTime,
timeFormat,
)
})",
style = MaterialTheme.typography.bodySmall.copy(fontSize = 8.sp),
modifier = Modifier
.clickable(onClick = {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(forecast.providerUrl)
?: return@clickable
}
context.tryStartActivity(intent)
})
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 12.dp)
)
}
}
}
Row(
modifier = Modifier.padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = formatTemperature(
context,
forecast.temperature.toFloat(),
measurementSystem
),
style = MaterialTheme.typography.headlineMedium,
)
Text(
text = forecast.condition,
style = MaterialTheme.typography.labelMedium,
)
AnimatedWeatherIcon(
modifier = Modifier.padding(
start = 8.dp,
end = 8.dp
),
icon = weatherIconById(forecast.icon),
night = forecast.night,
WeatherIconDefaults.colors(MaterialTheme.colorScheme.surface)
)
}
})",
style = MaterialTheme.typography.bodySmall.copy(fontSize = 8.sp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.offset(-16.dp, 4.dp)
.clickable(onClick = {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(forecast.providerUrl)
?: return@clickable
}
context.tryStartActivity(intent)
})
.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
Row(
Column(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
.weight(1f)
.fillMaxHeight()
.padding(
end = 16.dp,
top = 16.dp,
bottom = 16.dp,
),
horizontalAlignment = Alignment.End,
) {
if (forecast.humidity != null) {
Tooltip(
tooltipText = stringResource(R.string.weather_forecast_humidity)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
AnimatedWeatherIcon(
modifier = Modifier.offset(8.dp, -8.dp),
icon = weatherIconById(forecast.icon),
night = forecast.night,
WeatherIconDefaults.colors(MaterialTheme.colorScheme.surface)
)
CurrentWeatherDetails(
modifier = Modifier.weight(1f),
forecast = forecast,
measurementSystem = measurementSystem,
)
}
}
}
@Composable
private fun CurrentWeatherDetails(
modifier: Modifier = Modifier,
forecast: Forecast,
measurementSystem: MeasurementSystem
) {
val context = LocalContext.current
val resources = LocalResources.current
val items = remember(forecast) {
buildList<Pair<@Composable () -> Unit, String>> {
if (
forecast.precipProbability != null && forecast.precipProbability!! >= 20 ||
forecast.precipitation != null && forecast.precipitation!! >= 1
) {
add(
@Composable {
Icon(
painter = painterResource(R.drawable.humidity_percentage_20px),
modifier = Modifier.size(20.dp),
painter = painterResource(R.drawable.rainy_20px),
modifier = Modifier
.size(20.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
contentDescription = null
)
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = formatPercent(forecast.humidity!!.toFloat()),
style = MaterialTheme.typography.bodySmall,
} to if (forecast.precipProbability != null) {
formatPercent(forecast.precipProbability!!.toFloat())
} else {
formatPrecipitation(
context,
forecast.precipitation?.toFloat() ?: 0f,
measurementSystem
)
}
}
)
}
if (forecast.windDirection != null || forecast.windSpeed != null) {
Tooltip(
tooltipText = stringResource(R.string.weather_forecast_wind)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (forecast.uvIndex != null && forecast.uvIndex!! >= 3) {
add(
@Composable {
Icon(
painter = painterResource(R.drawable.sunny_20px),
modifier = Modifier
.size(20.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
} to "UV ${forecast.uvIndex}"
)
}
if (forecast.windSpeed != null && forecast.windSpeed!! >= 3) {
add(
@Composable {
if (forecast.windDirection != null) {
// windDirection is "fromDirection"; Wind (arrow) blows into opposite direction
val angle by animateFloatAsState(forecast.windDirection!!.toFloat() + 180f)
Icon(
painter = painterResource(R.drawable.north_20px),
painter = painterResource(R.drawable.navigation_20px),
modifier = Modifier
.rotate(angle)
.size(20.dp),
@@ -424,50 +454,61 @@ fun CurrentWeather(
tint = MaterialTheme.colorScheme.secondary,
)
}
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = if (forecast.windSpeed != null) {
formatSpeed(
context,
forecast.windSpeed!!.toFloat(),
measurementSystem
)
} else {
windDirectionAsWord(forecast.windDirection!!)
},
style = MaterialTheme.typography.bodySmall,
)
}
}
} to formatSpeed(
context,
forecast.windSpeed!!.toFloat(),
measurementSystem
)
)
}
if (forecast.precipitation != null) {
Tooltip(
tooltipText = stringResource(id = R.string.weather_forecast_precipitation)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (size < 4) {
add(
@Composable {
Icon(
painter = painterResource(R.drawable.rainy_20px),
modifier = Modifier.size(20.dp),
painter = painterResource(R.drawable.humidity_mid_20px),
modifier = Modifier
.size(20.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.padding(3.dp))
Text(
text = if (forecast.precipProbability != null) {
formatPercent(forecast.precipProbability!!.toFloat())
} else {
formatPrecipitation(
context,
forecast.precipitation?.toFloat() ?: 0f,
measurementSystem
)
},
style = MaterialTheme.typography.bodySmall,
} to formatPercent(forecast.humidity!!.toFloat()),
)
}
if (size < 4 && forecast.clouds != null && forecast.clouds!! >= 30) {
add(
@Composable {
Icon(
painter = painterResource(R.drawable.cloud_20px),
modifier = Modifier
.size(20.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
}
}
} to formatPercent(forecast.clouds!!.toFloat()),
)
}
}
}
FlowRow(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.End),
) {
for (item in items) {
Row(
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
item.first()
Text(
textAlign = TextAlign.End,
modifier = Modifier.padding(start = 4.dp),
text = item.second,
style = MaterialTheme.typography.labelSmall,
)
}
}
}
@@ -593,7 +634,7 @@ fun WeatherDaySelector(
val colors = WeatherIconDefaults.colors(MaterialTheme.colorScheme.surfaceBright)
val selectedColors = WeatherIconDefaults.themedColors(
MaterialTheme.colorScheme.secondary,
)
)
val listState = rememberLazyListState()
LazyRow(
@@ -693,9 +734,8 @@ private fun formatTime(context: Context, timestamp: Long, timeFormat: TimeFormat
)
}
@Composable
private fun windDirectionAsWord(direction: Double): String {
return stringResource(
private fun windDirectionAsWord(resources: Resources, direction: Double): String {
return resources.getString(
when (direction) {
in 11.25..33.75 -> R.string.wind_north_north_east
in 33.75..56.25 -> R.string.wind_north_east

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M240,768Q160,768 104,712Q48,656 48,576Q48,500 100,444.5Q152,389 227,384Q250,299 319.5,245.5Q389,192 480,192Q583,192 659,261.5Q735,331 744,432L744,432L744,432Q814,432 863,481Q912,530 912,600Q912,670 863,719Q814,768 744,768L240,768ZM240,696L744,696Q784,696 812,668Q840,640 840,600Q840,560 812,532Q784,504 744,504L678,504L672,439Q665,365 610,314.5Q555,264 480,264Q416,264 365,302.5Q314,341 297,404L283,453L232,456Q184,459 152,493.5Q120,528 120,576Q120,626 155,661Q190,696 240,696ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,816Q360,816 276,732Q192,648 192,528Q192,474 211,425Q230,376 266,336L480,96L694,336Q730,376 749,425Q768,474 768,528Q768,648 684,732Q600,816 480,816ZM264,528L696,528Q696,488 682.5,450.5Q669,413 641,384L480,204L320,384Q292,413 278,450.5Q264,488 264,528Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,696L257,797Q246,802 235.5,800Q225,798 217,790Q209,782 206.5,772Q204,762 209,751L447,176Q452,165 460.5,159.5Q469,154 480,154Q491,154 499.5,159.5Q508,165 513,176L751,751Q756,762 753.5,772Q751,782 743,790Q735,798 724.5,800Q714,802 703,797L480,696ZM311,694L480,617L649,694L480,284L311,694ZM480,617L480,617L480,617L480,617Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M454.5,181.68Q444,171.35 444,156.09L444,84.26Q444,69 454.29,58.5Q464.58,48 479.79,48Q495,48 505.5,58.32Q516,68.65 516,83.91L516,155.74Q516,171 505.71,181.5Q495.42,192 480.21,192Q465,192 454.5,181.68ZM684,276Q673,266.14 673,251.36Q673,236.58 684,225.82L733.9,174.75Q744.59,164 759.3,164Q774,164 785,175Q796,186 796,200.5Q796,215 785,226L734,276Q723,287 709,287Q695,287 684,276ZM804.26,516Q789,516 778.5,505.71Q768,495.42 768,480.21Q768,465 778.32,454.5Q788.65,444 803.91,444L875.74,444Q891,444 901.5,454.29Q912,464.58 912,479.79Q912,495 901.68,505.5Q891.35,516 876.09,516L804.26,516ZM454.5,901.68Q444,891.35 444,876.09L444,804.26Q444,789 454.29,778.5Q464.58,768 479.79,768Q495,768 505.5,778.32Q516,788.65 516,803.91L516,875.74Q516,891 505.71,901.5Q495.42,912 480.21,912Q465,912 454.5,901.68ZM225.82,276L174.75,225.76Q164,215 164,200.5Q164,186 175,175Q186,165 200.5,164.5Q215,164 226,175L276,226Q287,237 287,251Q287,265 276,276Q265.25,287 250.91,287Q236.58,287 225.82,276ZM734,785L684,734Q673,723 673,709Q673,695 684,684Q695,673 709,673Q723,673 734,684L785.22,734.15Q796,744 796,759Q796,774 785.48,785Q774,796 759.5,796Q745,796 734,785ZM84.26,516Q69,516 58.5,505.71Q48,495.42 48,480.21Q48,465 58.32,454.5Q68.65,444 83.91,444L155.74,444Q171,444 181.5,454.29Q192,464.58 192,479.79Q192,495 181.68,505.5Q171.35,516 156.09,516L84.26,516ZM175,784.52Q164,774 164,759.5Q164,745 175,734L226,684Q237,674 251.18,674Q265.37,674 275.68,684.04Q286,695 286,709Q286,723 276,734L225.76,785.06Q215,796 200.5,796Q186,796 175,784.52ZM310,650Q240,580 240,480Q240,380 310,310Q380,240 480,240Q580,240 650,310Q720,380 720,480Q720,580 650,650Q580,720 480,720Q380,720 310,650ZM599,599Q648,550 648,480Q648,410 599,361Q550,312 480,312Q410,312 361,361Q312,410 312,480Q312,550 361,599Q410,648 480,648Q550,648 599,599ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@@ -29,6 +29,7 @@ object WeatherPluginContract {
val Precipitation = column<Double>("precipitation")
val RainProbability = column<Int>("rain_probability")
val Clouds = column<Int>("clouds")
val UvIndex = column<Double>("uv_index")
val Location = column<String>("location")
val Provider = column<String>("provider")
val ProviderUrl = column<String>("provider_url")

View File

@@ -44,6 +44,7 @@ import de.mm20.launcher2.database.migrations.Migration_27_28
import de.mm20.launcher2.database.migrations.Migration_28_29
import de.mm20.launcher2.database.migrations.Migration_29_30
import de.mm20.launcher2.database.migrations.Migration_30_31
import de.mm20.launcher2.database.migrations.Migration_31_32
import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9
@@ -67,7 +68,7 @@ import java.util.UUID
ShapesEntity::class,
TransparenciesEntity::class,
TypographyEntity::class,
], version = 31, exportSchema = true
], version = 32, exportSchema = true
)
@TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() {
@@ -175,6 +176,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_28_29(),
Migration_29_30(),
Migration_30_31(),
Migration_31_32(),
).build()
if (_instance == null) _instance = instance
return instance

View File

@@ -24,7 +24,9 @@ data class ForecastEntity(
val provider: String,
val providerUrl: String = "",
@ColumnInfo(name = "rainProbability") val precipProbability: Int = -1,
@Deprecated("Deprecated")
val snowProbability: Int = -1,
val uvIndex: Double = -1.0,
val updateTime: Long
) {
companion object {

View File

@@ -0,0 +1,17 @@
package de.mm20.launcher2.database.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import de.mm20.launcher2.ktx.toBytes
import de.mm20.launcher2.preferences.WidgetScreenTarget
class Migration_31_32 : Migration(31, 32) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
ALTER TABLE `forecasts` ADD COLUMN `uvIndex` REAL NOT NULL DEFAULT -1.0;
""".trimIndent(),
)
}
}

View File

@@ -45,6 +45,8 @@ data class Forecast(
val windDirection: Double? = null,
/** rain, in mm per hour **/
val precipitation: Double? = null,
/** UV index **/
val uvIndex: Double? = null,
/** whether this forecast is during nighttime (whether a moon icon should be used instead of sun) **/
val night: Boolean = false,
/** Location string **/
@@ -79,7 +81,7 @@ data class Forecast(
windDirection = windDirection ?: -1.0,
windSpeed = windSpeed ?: -1.0,
snow = -1.0,
snowProbability = -1
uvIndex = uvIndex ?: -1.0,
)
}
@@ -102,6 +104,7 @@ data class Forecast(
updateTime = entity.updateTime,
windDirection = entity.windDirection.takeIf { it >= 0.0 },
windSpeed = entity.windSpeed.takeIf { it >= 0.0 },
uvIndex = entity.uvIndex.takeIf { it >= 0.0 },
)
companion object {

View File

@@ -86,6 +86,7 @@ class BreezyWeatherProvider(
windSpeed = hourly.windSpeed?.toDouble()?.div(3.6),
precipProbability = hourly.precipProbability,
windDirection = hourly.windDirection?.toDouble(),
uvIndex = hourly.uvIndex?.toDouble(),
updateTime = lastUpdate,
night = isNight
)

View File

@@ -49,12 +49,12 @@ internal data class LocationForecast(
) {
@Serializable
internal data class Details(
@SerialName("air_temperature") val airTemperature: Double,
@SerialName("cloud_area_fraction") val cloudAreaFraction: Double,
@SerialName("relative_humidity") val relativeHumidity: Double,
@SerialName("wind_from_direction") val windFromDirection: Double,
@SerialName("wind_speed") val windSpeed: Double,
@SerialName("air_pressure_at_sea_level") val airPressureAtSeaLevel: Double,
@SerialName("air_temperature") val airTemperature: Double?,
@SerialName("cloud_area_fraction") val cloudAreaFraction: Double?,
@SerialName("relative_humidity") val relativeHumidity: Double?,
@SerialName("wind_from_direction") val windFromDirection: Double?,
@SerialName("wind_speed") val windSpeed: Double?,
@SerialName("air_pressure_at_sea_level") val airPressureAtSeaLevel: Double?,
)
}
@@ -70,7 +70,8 @@ internal data class LocationForecast(
@Serializable
internal data class Details(
@SerialName("precipitation_amount") val precipitationAmount: Double?
@SerialName("precipitation_amount") val precipitationAmount: Double?,
@SerialName("ultraviolet_index_clear_sky_max") val ultravioletIndexClearSkyMax: Double?,
)
}
}

View File

@@ -97,9 +97,9 @@ internal class MetNoProvider(
forecasts.add(
Forecast(
timestamp = timestamp,
temperature = details.airTemperature + 273.15,
temperature = (details.airTemperature ?: continue) + 273.15,
updateTime = updatedAt,
clouds = details.cloudAreaFraction.roundToInt(),
clouds = details.cloudAreaFraction?.roundToInt(),
humidity = details.relativeHumidity,
windDirection = details.windFromDirection,
windSpeed = details.windSpeed,
@@ -110,6 +110,7 @@ internal class MetNoProvider(
icon = iconForCode(symbolCode),
condition = conditionForCode(symbolCode),
precipitation = precipitationAmount,
uvIndex = nextHours.details?.ultravioletIndexClearSkyMax,
night = isNight(timestamp, lat, lon)
)
@@ -239,7 +240,7 @@ internal class MetNoProvider(
"lightrain", "lightrainshowers" -> Forecast.LIGHT_RAIN
"heavyrain", "heavyrainshowers" -> Forecast.HEAVY_RAIN
"rain" -> Forecast.RAIN
"rain", "rainshowers" -> Forecast.RAIN
"lightsleet", "lightsleetshowers", "heavysleet", "heavysleetshowers", "sleet" -> Forecast.SLEET

View File

@@ -68,14 +68,14 @@ data class WeatherResultClouds(
@Serializable
data class CurrentWeatherResultRain(
@SerialName("1h") val oneHour: Double,
@SerialName("3h") val threeHours: Double,
@SerialName("1h") val oneHour: Double?,
@SerialName("3h") val threeHours: Double?,
)
@Serializable
data class CurrentWeatherResultSnow(
@SerialName("1h") val oneHour: Double,
@SerialName("3h") val threeHours: Double,
@SerialName("1h") val oneHour: Double?,
@SerialName("3h") val threeHours: Double?,
)
@Serializable
@@ -117,7 +117,7 @@ data class ForecastResultSnow(
@Serializable
data class ForecastResultSys(
val pod: String,
val pod: String?,
)
@Serializable

View File

@@ -118,6 +118,7 @@ internal class PluginWeatherProvider(
provider = cursor[ForecastColumns.Provider] ?: continue,
providerUrl = cursor[ForecastColumns.ProviderUrl] ?: "",
clouds = cursor[ForecastColumns.Clouds],
uvIndex = cursor[ForecastColumns.UvIndex],
humidity = cursor[ForecastColumns.Humidity]?.toDouble(),
precipitation = cursor[ForecastColumns.Precipitation],
precipProbability = cursor[ForecastColumns.RainProbability],

View File

@@ -180,6 +180,11 @@ data class Forecast(
* Clouds in percent
*/
val clouds: Int? = null,
/**
* UV index (0-11)
*/
val uvIndex: Double? = null,
/**
* Location name
*/

View File

@@ -86,6 +86,7 @@ abstract class WeatherProvider(
put(ForecastColumns.Precipitation, it.precipitation?.mm)
put(ForecastColumns.RainProbability, it.rainProbability)
put(ForecastColumns.Clouds, it.clouds)
put(ForecastColumns.UvIndex, it.uvIndex)
put(ForecastColumns.Location, it.location)
put(ForecastColumns.Provider, it.provider)
put(ForecastColumns.ProviderUrl, it.providerUrl)