mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-23 11:33:42 -05:00
add DWD weather provider including DwdApi, DwdRepository, and DwdJsonMapper
This commit is contained in:
1 parent
e9cfbacb93
commit
d9c195219b
6 files changed
+408
No files matched your search
+56
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+103
@@ -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())
|
||||
}
|
||||
|
||||
+19
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -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
|
||||
)
|
||||
+9
@@ -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
|
||||
)
|
||||
+180
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user