add DWD weather provider including DwdApi, DwdRepository, and DwdJsonMapper

This commit is contained in:
PranshulGG committed 2026-06-05 21:36:36 +05:30
1 parent e9cfbacb93
commit d9c195219b
6 files changed
+408

No files matched your search

@@ -0,0 +1,56 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdCurrentWeatherJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.json.MetNorwayForecastJson
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface DwdApi {
@GET("current_weather")
suspend fun fetchCurrentWeather(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double,
): Response<DwdCurrentWeatherJson>
@GET("weather")
suspend fun fetchWeatherForecast(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double,
@Query("date") date: String,
@Query("last_date") lastDate: String
): Response<DwdWeatherForecastJson>
companion object {
const val BASE_URL = "https://api.brightsky.dev/"
fun create(): DwdApi {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(logging)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(DwdApi::class.java)
}
}
}
@@ -0,0 +1,103 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResultType
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.bundle.DwdWeatherJsonBundle
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayApi
import com.pranshulgg.weather_master_app.core.utils.weather.cache.isWeatherCacheSafe
import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnWeatherCache
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao
import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.dwd.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.metnorway.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toCurrentWeatherEntity
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDailyWeatherEntity
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toHourlyWeatherEntity
import com.pranshulgg.weather_master_app.data.repository.WeatherRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.UnknownHostException
import java.time.LocalDate
import java.time.ZoneId
import javax.inject.Inject
class DwdRepository @Inject constructor(
val dao: LocationsDao,
val weatherDao: WeatherDao,
val api: DwdApi
) : WeatherRepository {
override suspend fun getWeather(
location: Location,
isManualRefresh: Boolean,
isForceRefresh: Boolean
): WeatherResult =
withContext(
Dispatchers.IO
) {
val cache = dao.getWeatherDataForLocation(location.id)
val shouldReturnCache = shouldReturnWeatherCache(cache, isManualRefresh, isForceRefresh)
when (shouldReturnCache) {
WeatherResultType.REFRESH_TOO_EARLY -> return@withContext WeatherResult.RefreshNotAvailable
WeatherResultType.SUCCESS -> return@withContext WeatherResult.Success(cache!!.toDomain())
else -> {}
}
return@withContext try {
val response = api.fetchCurrentWeather(location.latitude, location.longitude)
val dates = getStartEndDate(location)
val forecastResponse = api.fetchWeatherForecast(
location.latitude, location.longitude, dates.first, dates.second
)
val body = response.body()
?: return@withContext WeatherResult.Error(exception = UnknownHostException())
val forecastBody = forecastResponse.body()
?: return@withContext WeatherResult.Error(exception = UnknownHostException())
val final = DwdWeatherJsonBundle(
current = body,
forecastJson = forecastBody
)
val domain = final.toDomain(location)
weatherDao.insertWeather(
domain.current.toCurrentWeatherEntity(location.id),
domain.hourly.toHourlyWeatherEntity(location.id),
domain.daily.toDailyWeatherEntity(location.id),
location.id
)
WeatherResult.Success(domain)
} catch (e: Exception) {
val isCacheSafe = isWeatherCacheSafe(cache)
WeatherResult.Error(
exception = e,
if (isCacheSafe) cache?.toDomain() else null
)
}
}
}
private fun getStartEndDate(location: Location): Pair<String, String> {
val zoneId = ZoneId.of(location.timezone)
val start = LocalDate.now(zoneId)
val end = start.plusDays(5)
return Pair(start.toString(), end.toString())
}
@@ -0,0 +1,19 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
object DwdWeatherConditionMap {
fun getCondition(id: String?): WeatherCondition {
return when (id) {
"clear-day", "clear-night", "wind" -> WeatherCondition.CLEAR_SKY
"partly-cloudy-day", "partly-cloudy-night" -> WeatherCondition.PARTLY_CLOUDY
"cloudy" -> WeatherCondition.OVERCAST
"fog" -> WeatherCondition.FOG_HAZE
"rain" -> WeatherCondition.RAIN
"sleet", "snow", "hail" -> WeatherCondition.SNOW
"thunderstorm" -> WeatherCondition.THUNDERSTORM
else -> WeatherCondition.NO_CONDITION_FOUND
}
}
}
@@ -0,0 +1,41 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json
import com.google.gson.annotations.SerializedName
data class DwdCurrentWeatherJson(
val weather: DwdCurrentWeatherDataJson
)
data class DwdWeatherForecastJson(
val weather: List<DwdWeatherForecastDataJson>
)
data class DwdWeatherForecastDataJson(
val timestamp: String,
@SerializedName("dew_point") val dewPoint: Double?,
@SerializedName("pressure_msl") val pressureMsl: Double?,
@SerializedName("relative_humidity") val humidity: Int?,
val visibility: Int?,
@SerializedName("wind_direction") val windDirection: Int?,
@SerializedName("wind_speed") val windSpeed: Double?,
val temperature: Double,
val icon: String,
val precipitation: Double?,
@SerializedName("precipitation_probability") val precipitationProbability: Int?,
)
data class DwdCurrentWeatherDataJson(
val timestamp: String,
@SerializedName("dew_point") val dewPoint: Double?,
@SerializedName("pressure_msl") val pressureMsl: Double?,
@SerializedName("relative_humidity") val humidity: Int?,
val visibility: Int?,
@SerializedName("wind_direction_10") val windDirection: Int?,
@SerializedName("wind_speed_10") val windSpeed: Double?,
val temperature: Double,
val icon: String
)
@@ -0,0 +1,9 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.bundle
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdCurrentWeatherJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastJson
data class DwdWeatherJsonBundle(
val current: DwdCurrentWeatherJson,
val forecastJson: DwdWeatherForecastJson
)
@@ -0,0 +1,180 @@
package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.dwd
import android.util.Log
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherCurrent
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherDaily
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherHourly
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
import com.pranshulgg.weather_master_app.core.model.weather.wind.WindDirection
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.DwdWeatherConditionMap
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastDataJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.bundle.DwdWeatherJsonBundle
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayWeatherConditionMap
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.normalizeToDay
import com.pranshulgg.weather_master_app.core.utils.weather.astronomy.getMoonTimings
import com.pranshulgg.weather_master_app.core.utils.weather.astronomy.getSunTimings
import com.pranshulgg.weather_master_app.core.utils.weather.calculations.computeApparentTemperature
import com.pranshulgg.weather_master_app.core.utils.weather.computing.computeDailyWeatherCondition
// ---------------------------- JSON TO DOMAIN ----------------------------
fun DwdWeatherJsonBundle.toDomain(location: Location): Weather {
val current = this.current.weather
val forecast = this.forecastJson.weather
val daily = computeDaily(forecast, location)
return Weather(
location = location,
current = WeatherCurrent(
temperature = current.temperature,
humidity = current.humidity?.toDouble() ?: 0.0,
windSpeed = current.windSpeed,
windDirection = WindDirection.toWindDirectionFromDegrees(current.windDirection),
pressureMsl = current.pressureMsl,
visibility = current.visibility,
cloudCover = null, // NOT USED IN THE APP
uvIndex = null,
weatherCondition = DwdWeatherConditionMap.getCondition(current.icon),
feelsLike = computeApparentTemperature(
current.temperature,
current.humidity?.toDouble(),
current.windSpeed?.kmhToMs()
),
time = current.timestamp.iso8601TimestampToMilliseconds(),
dewPoint = current.dewPoint,
utcOffsetSeconds = null,
lastUpdatedInMilli = System.currentTimeMillis()
),
hourly = forecast.map {
WeatherHourly(
temperature = it.temperature,
windSpeed = it.windSpeed,
windDirection = WindDirection.toWindDirectionFromDegrees(it.windDirection),
rain = it.precipitation ?: 0.0,
snowfall = null,
uvIndex = null,
pressureMsl = it.pressureMsl,
visibility = it.visibility,
humidity = it.humidity?.toDouble(),
dewPoint = it.dewPoint,
weatherCondition = DwdWeatherConditionMap.getCondition(it.icon),
time = it.timestamp.iso8601TimestampToMilliseconds(),
precipitationProbability = it.precipitationProbability
)
},
daily = daily
)
}
private fun computeDaily(
data: List<DwdWeatherForecastDataJson>,
location: Location
): List<WeatherDaily> {
val zoneId = location.timezone
val groupedByDay = data.groupBy {
it.timestamp.iso8601TimestampToMilliseconds().normalizeToDay(zoneId)
}
val sunTimings = getSunTimings(
groupedByDay.map {
it.key
},
location.timezone,
location.latitude,
location.longitude
)
val moonTimings = getMoonTimings(
groupedByDay.map {
it.key
},
location.timezone,
location.latitude,
location.longitude
)
return groupedByDay.map { dailyIt ->
val minTemperature = dailyIt.value.minOf { it.temperature }
val maxTemperature = dailyIt.value.maxOf { it.temperature }
val windSpeed = dailyIt.value
.mapNotNull { it.windSpeed }
.maxOrNull()
val windDirection =
dailyIt.value.mapNotNull { it.windDirection }.maxOrNull()
val rainSum =
dailyIt.value.sumOf { it.precipitation ?: 0.0 }
val time = dailyIt.key
val icon = dailyIt.value.map { it.icon }.groupingBy { it }
.eachCount().entries.maxByOrNull { it.value }
Log.d("DWD ICON", "${icon}")
val condition = computeDailyWeatherCondition(
getHourlyConditionsForDay(dailyIt.value, time),
MetNorwayWeatherConditionMap.getCondition(icon?.key)
)
val precipitationProbabilityMax = dailyIt.value.mapNotNull { it.precipitationProbability }
.maxOrNull()
val index = groupedByDay.keys.indexOf(dailyIt.key)
WeatherDaily(
temperatureMin = minTemperature,
temperatureMax = maxTemperature,
windSpeed = windSpeed,
windDirection = WindDirection.toWindDirectionFromDegrees(windDirection),
rainSum = rainSum,
snowfallSum = null,
uvIndexMax = null,
weatherCondition = condition,
time = time,
precipitationProbabilityMax = precipitationProbabilityMax,
sunrise = sunTimings[index].sunrise ?: -0L,
sunset = sunTimings[index].sunset ?: -0L,
moonrise = moonTimings[index].moonrise ?: -0L,
moonset = moonTimings[index].moonset ?: -0L,
moonPhase = moonTimings[index].phase,
dawn = sunTimings[index].dawn ?: 0L,
dusk = sunTimings[index].dusk ?: 0L
)
}
}
private fun getHourlyConditionsForDay(
data: List<DwdWeatherForecastDataJson>,
time: Long
): List<WeatherCondition> {
val startIndex =
data.indexOfFirst { it.timestamp.iso8601TimestampToMilliseconds() >= time }
.takeIf { it != -1 } ?: 0
val conditions = data.drop(maxOf(0, startIndex - 1))
.take(WeatherSource.DWD.hourlyAggregationLimitHours)
.map {
DwdWeatherConditionMap.getCondition(it.icon)
}
return conditions
}
private fun Double.kmhToMs(): Double {
return (this / 3.6)
}