add kmi closes #997 closes #381 closes #1122

This commit is contained in:
pranshulgg committed 2026-09-25 21:18:41 +05:30
1 parent 40c48a3330
commit 02ebad26ed
14 files changed
+384 -17

No files matched your search

@@ -25,6 +25,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.Inme
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetForecastApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetObservationApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.KmiApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteofrance.MeteoFranceApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayApi
@@ -189,4 +190,8 @@ object ApiModule {
@Provides
@Singleton
fun provideMgmApi(): MgmApi = MgmApi.create()
@Provides
@Singleton
fun provideKmiApi(): KmiApi = KmiApi.create()
}
@@ -30,6 +30,8 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.jma.JmaApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.jma.JmaRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.KmiApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.KmiRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteofrance.MeteoFranceApi
@@ -299,4 +301,16 @@ object WeatherRepositoryModule {
locationKeysDao: LocationKeysDao,
alertsDao: AlertsDao
): MgmRepository = MgmRepository(dao, weatherDao, api, apiKeysDao, locationKeysDao, alertsDao)
@Provides
@Singleton
fun provideKmiRepository(
dao: WeatherContextDao,
weatherDao: WeatherDao,
api: KmiApi,
apiKeysDao: ApiKeysDao,
locationKeysDao: LocationKeysDao,
alertsDao: AlertsDao
): KmiRepository = KmiRepository(dao, weatherDao, api, apiKeysDao, locationKeysDao, alertsDao)
}
@@ -141,9 +141,7 @@ class WeatherManager @Inject constructor(
&& !location.source.isSourceSupportedFor(
countryCode = location.countryCode?.uppercase()
)
_errors.tryEmit(result.exception.toAppException())
}
is WeatherResult.RefreshNotAvailable -> {
@@ -203,6 +203,13 @@ enum class Source(
countryNameRes = R.string.country_turkey,
capabilities = setOf(Capability.WEATHER, Capability.ALERTS)
),
KMI(
displayName = "KMI",
fullName = "Royal Meteorological Institute of Belgium",
displayLink = "https://www.meteo.be/",
countryNameRes = R.string.country_belgium,
capabilities = setOf(Capability.WEATHER, Capability.ALERTS)
),
OPEN_WEATHER(
displayName = "OpenWeather",
fullName = "OpenWeather",
@@ -250,6 +257,7 @@ private val sourcesByCountry = buildMap {
put("JP", listOf(Source.JMA))
put("BR", listOf(Source.INMET))
put("TR", listOf(Source.MGM))
put("BE", listOf(Source.KMI))
}
@@ -13,7 +13,11 @@ sealed class WeatherResult(
data class RefreshNotAvailable(val weather: Weather) : WeatherResult()
data class Error(val exception: Exception, val weather: Weather?) : WeatherResult()
data class Error(
val exception: Exception,
val weather: Weather?,
val rawException: Exception? = null
) : WeatherResult()
class NotSupported : WeatherResult()
@@ -1,18 +1,29 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.kmi
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
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.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.FinishedWeatherResult
import com.pranshulgg.weather_master_app.core.model.weather.WeatherDataPack
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertsDataPack
import com.pranshulgg.weather_master_app.core.model.weather.alerts.FinishedAlertsResult
import com.pranshulgg.weather_master_app.core.network.calls.safeApiCall
import com.pranshulgg.weather_master_app.core.network.sources.weather.mgm.MgmApi
import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationKeysDao
import com.pranshulgg.weather_master_app.data.local.dao.weather.ApiKeysDao
import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherContextDao
import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.kmi.alerts.toAlertsDomain
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.kmi.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.pirateweather.alerts.toDomain
import com.pranshulgg.weather_master_app.data.repository.alerts.AlertCacheModel
import com.pranshulgg.weather_master_app.data.repository.capability.AirQualityCapability
import com.pranshulgg.weather_master_app.data.repository.capability.AlertCapability
import com.pranshulgg.weather_master_app.data.repository.capability.WeatherCapability
import com.pranshulgg.weather_master_app.data.repository.data.BaseRepository
import com.pranshulgg.weather_master_app.data.repository.data.WeatherAdditionalData
import com.pranshulgg.weather_master_app.data.repository.weather.CacheModel
import java.security.MessageDigest
import java.time.LocalDate
@@ -45,25 +56,90 @@ class KmiRepository @Inject constructor(
isForceRefresh: Boolean,
cacheModel: CacheModel
): WeatherDataPack {
val response = api.fetchWeather(
lat = location.latitude,
lon = location.longitude,
key = generateIrmKmiKey()
)
val response = safeApiCall {
api.fetchWeather(
lat = location.latitude,
lon = location.longitude,
key = generateIrmKmiKey()
)
}.getOrThrow()
return WeatherDataPack(
weather = response.toDomain(location),
additionalData = WeatherAdditionalData(
alerts = AlertsDataPack(
alerts = response.toAlertsDomain(locationId = location.id),
location
)
)
)
}
override suspend fun saveToDb(data: WeatherDataPack, cacheModel: CacheModel) {
TODO("Not yet implemented")
useGenericSaveImplementationForWeather(
existingHourly = cacheModel.cachedHourly,
data = data.weather,
weatherDao = weatherDao
)
}
override suspend fun saveAdditionalDataToDb(pack: WeatherDataPack) {
useGenericSaveImplementationForAlerts(
pack.additionalData?.alerts!!,
alertsDao,
dao
)
}
override fun finishedResult(data: Weather): FinishedWeatherResult {
TODO("Not yet implemented")
return FinishedWeatherResult(weather = data)
}
}
}
override fun alertCapability(): AlertCapability? {
return object : AlertCapability {
override suspend fun fetchAndProcess(
location: Location,
isManualRefresh: Boolean,
isForceRefresh: Boolean,
alertCacheModel: AlertCacheModel
): AlertsDataPack {
if (location.source == location.alertSource) {
return AlertsDataPack(alertCacheModel.cachedAlerts, location)
} else {
val response = safeApiCall {
api.fetchWeather(
lat = location.latitude,
lon = location.longitude,
key = generateIrmKmiKey()
)
}.getOrThrow()
return AlertsDataPack(
alerts = response.toAlertsDomain(location.id), location
)
}
}
override suspend fun saveToDb(
data: AlertsDataPack,
alertCacheModel: AlertCacheModel
) {
useGenericSaveImplementationForAlerts(data, alertsDao, dao)
}
override fun finishedResult(data: List<Alert>): FinishedAlertsResult {
return FinishedAlertsResult(alerts = data)
}
}
}
override fun airQualityCapability(): AirQualityCapability? = null
}
private fun generateIrmKmiKey(): String {
@@ -0,0 +1,21 @@
package com.pranshulgg.weather_master_app.core.network.sources.weather.kmi
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
// Source - https://github.com/jdejaegh/irm-kmi-api/blob/a66c94a649885be3afd3081cb8cf595899b46ede/irm_kmi_api/const.py#L60
object KmiWeatherConditionMap {
fun getCondition(id: Int?): WeatherCondition {
return when (id) {
0, 1 -> WeatherCondition.CLEAR_SKY
2, 5, 7, 10, 13, 17, 18, 21 -> WeatherCondition.RAIN
3 -> WeatherCondition.PARTLY_CLOUDY
4, 6, 16, 19 -> WeatherCondition.LIGHT_RAIN
8, 9, 20 -> WeatherCondition.MIXED_PRECIPITATION
11, 12, 22, 23 -> WeatherCondition.SNOW
14, 15 -> WeatherCondition.OVERCAST
24, 25, 26, 27 -> WeatherCondition.FOG_HAZE
else -> WeatherCondition.NO_CONDITION_FOUND
}
}
}
@@ -4,11 +4,22 @@ import com.google.gson.annotations.SerializedName
data class KmiWeatherJson(
val obs: KmiCurrentWeatherJson,
@SerializedName("for") val forecast: KmiForecastJson
@SerializedName("for") val forecast: KmiForecastJson,
val module: List<KmiModuleJson>
)
data class KmiModuleJson(
val type: String,
val data: KmiModuleDataJson
)
data class KmiModuleDataJson(
val levelValue: Double?,
)
data class KmiCurrentWeatherJson(
val temp: Double?,
val timestamp: String,
val ww: Double?,
)
@@ -27,7 +38,12 @@ data class KmiDayJson(
@SerializedName("ddText") val windDirectionText: KmiDayWindDirectionJson,
val wind: KmiDayWindJson,
val precipChance: Double?,
val precipQuantity: String?
val precipQuantity: String?,
val dayName: KmiDayNameJson?,
)
data class KmiDayNameJson(
val en: String?
)
data class KmiDayWindDirectionJson(
@@ -98,8 +98,7 @@ fun AccuWeatherBundle.toDomain(location: Location): Weather {
val condition =
computeDailyWeatherCondition(List(12) { AccuWeatherConditionMap.getCondition(item.day.icon) } + AccuWeatherConditionMap.getCondition(
item.night.icon
),
WeatherCondition.NO_CONDITION_FOUND)
), WeatherCondition.NO_CONDITION_FOUND)
val precipitationProbabilityMax = listOf(
@@ -0,0 +1,183 @@
package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.kmi
import com.pranshulgg.weather_master_app.core.model.astro.MoonPhase
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.weather.WeatherCondition
import com.pranshulgg.weather_master_app.core.model.weather.wind.WindDirection
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.KmiWeatherConditionMap
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.json.KmiDayJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.json.KmiHourJson
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.json.KmiWeatherJson
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds
import com.pranshulgg.weather_master_app.core.utils.formatters.getCurrentTimeFor
import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId
import com.pranshulgg.weather_master_app.core.utils.formatters.toSafeDouble
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.computing.computeDailyWeatherCondition
import com.pranshulgg.weather_master_app.core.utils.weather.forecast.findHourlyIndexForTime
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
private fun getFixedHourlyTimings(data: List<KmiHourJson>, zoneId: String): List<Long> {
var day = LocalDate.now(safeZoneId(zoneId))
return data.mapIndexed { index, hour ->
if (index > 0 && hour.dateShow != null) {
day = day.plusDays(1)
}
val iso = LocalDateTime
.of(
day,
LocalTime.of(hour.hour.toSafeDouble()!!.toInt(), 0)
)
.atZone(safeZoneId(zoneId))
.withZoneSameInstant(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_INSTANT)
iso.iso8601TimestampToMilliseconds()
}
}
fun KmiWeatherJson.toDomain(location: Location): Weather {
val zoneId = location.timezone
val currentTime = getCurrentTimeFor(zoneId)
val hourlyTimes = getFixedHourlyTimings(data = forecast.hourly, zoneId)
val currentHourlyIndex = forecast.hourly[findHourlyIndexForTime(
hourlyTimes,
startMilli = currentTime
)]
val obsTimestamp = OffsetDateTime.parse(obs.timestamp).toLocalDateTime()
val resolvedDailyList = mutableListOf<KmiDayJson>()
val day1 = forecast.daily.getOrNull(0)
val tonight = forecast.daily.find { it.dayName?.en == "Tonight" }
// Merge today and tonight into 1 day
forecast.daily.forEach {
if (it.period == "1" && tonight != null) {
return@forEach
}
if (it.dayName?.en == "Tonight" && day1 != null) {
val day = KmiDayJson(
period = "1",
tempMin = it.tempMin,
tempMax = day1.tempMax,
ww1 = day1.ww1,
ww2 = it.ww1,
windDirectionText = it.windDirectionText, // BOTH are usually same
wind = it.wind, // BOTH are usually same,
precipChance = listOf(it.precipChance ?: 0.0, day1.precipChance ?: 0.0).max(),
precipQuantity = it.precipQuantity.plus(day1.precipQuantity),
dayName = day1.dayName
)
resolvedDailyList.add(day)
} else {
resolvedDailyList.add(it)
}
}
val dailyTimings = List(resolvedDailyList.size) { index ->
obsTimestamp.plusDays(index.toLong()).atZone(safeZoneId(zoneId))
.withZoneSameInstant(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)
.iso8601TimestampToMilliseconds()
}
val sunTimings =
getSunTimings(dailyTimings, location.timezone, location.latitude, location.longitude)
val moonTimings =
getMoonTimings(dailyTimings, location.timezone, location.latitude, location.longitude)
return Weather(
location = location,
current = WeatherCurrent(
temperature = obs.temp ?: currentHourlyIndex.temp,
humidity = null,
windSpeed = currentHourlyIndex.windSpeedKm,
windDirection = WindDirection.toWindDirectionFromString(currentHourlyIndex.windDirectionText.en),
pressureMsl = currentHourlyIndex.pressure,
visibility = null,
cloudCover = null,
uvIndex = module.firstOrNull { it.type == "uv" }?.data?.levelValue,
weatherCondition = KmiWeatherConditionMap.getCondition(currentHourlyIndex.ww?.toInt()),
feelsLike = null,
dewPoint = null,
utcOffsetSeconds = null,
lastUpdatedInMilli = System.currentTimeMillis()
),
hourly = forecast.hourly.mapIndexed { index, item ->
val time = hourlyTimes[index]
WeatherHourly(
temperature = item.temp,
windSpeed = item.windSpeedKm,
windDirection = WindDirection.toWindDirectionFromString(item.windDirectionText.en),
rain = item.precipQuantity ?: 0.0,
snowfall = null,
uvIndex = null,
pressureMsl = item.pressure,
visibility = null,
humidity = null,
dewPoint = null,
weatherCondition = KmiWeatherConditionMap.getCondition(item.ww?.toInt()),
time = time,
precipitationProbability = item.precipChance.toSafeDouble()?.roundToInt()
)
},
daily = resolvedDailyList.mapIndexed { index, day ->
val time = obsTimestamp.plusDays(index.toLong()).atZone(safeZoneId(zoneId))
.withZoneSameInstant(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)
.iso8601TimestampToMilliseconds()
val condition =
computeDailyWeatherCondition(List(12) { KmiWeatherConditionMap.getCondition(day.ww1?.toInt()) } +
KmiWeatherConditionMap.getCondition(day.ww2?.toInt()),
WeatherCondition.NO_CONDITION_FOUND)
WeatherDaily(
temperatureMin = day.tempMin,
temperatureMax = day.tempMax,
windSpeed = day.wind.speed,
windDirection = WindDirection.toWindDirectionFromString(day.windDirectionText.en),
rainSum = day.precipQuantity.toSafeDouble() ?: 0.0,
snowfallSum = null,
uvIndexMax = null,
weatherCondition = condition,
time = time,
precipitationProbabilityMax = day.precipChance?.roundToInt() ?: 0,
pressureMsl = null,
visibility = null,
humidity = null,
dewPoint = null,
sunrise = sunTimings.getOrNull(index)?.sunrise,
sunset = sunTimings.getOrNull(index)?.sunset,
dawn = sunTimings.getOrNull(index)?.dawn,
dusk = sunTimings.getOrNull(index)?.dusk,
moonrise = moonTimings.getOrNull(index)?.moonrise,
moonset = moonTimings.getOrNull(index)?.moonset,
moonPhase = moonTimings.getOrNull(index)?.phase ?: MoonPhase.UNKNOWN,
)
}
)
}
@@ -0,0 +1,38 @@
package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.kmi.alerts
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.json.KmiWeatherJson
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds
fun KmiWeatherJson.toAlertsDomain(locationId: String): List<Alert> {
val alerts = forecast.warning
return alerts.mapIndexed { index, alert ->
Alert(
locationId = locationId,
event = alert.warningType.en ?: "",
severity = getSeverity(alert.warningLevel),
effective = alert.fromTimestamp?.iso8601TimestampToMilliseconds(),
expires = alert.toTimestamp?.iso8601TimestampToMilliseconds(),
description = alert.text.en ?: "",
source = "Royal Meteorological Institute of Belgium",
lastUpdatedInMilli = System.currentTimeMillis()
)
}
}
// Not tested if the severity is correct or not
private fun getSeverity(waringLevel: Double?): AlertSeverity {
return when (waringLevel) {
3.0 -> AlertSeverity.CRITICAL
2.0 -> AlertSeverity.HIGH
1.0 -> AlertSeverity.MODERATE
0.0 -> AlertSeverity.LOW
else -> AlertSeverity.UNKNOWN
}
}
@@ -17,6 +17,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.imd.ImdRep
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.jma.JmaRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.kmi.KmiRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteofrance.MeteoFranceRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayRepository
@@ -56,6 +57,7 @@ class SourceRepositoryProvider @Inject constructor(
private val inmetRepository: InmetRepository,
private val openWeatherRepository: OpenWeatherRepository,
private val mgmRepository: MgmRepository,
private val kmiRepository: KmiRepository,
// ALERTS
private val alertsWeatherApiRepository: AlertsWeatherApiRepository,
@@ -90,7 +92,8 @@ class SourceRepositoryProvider @Inject constructor(
wmoSevereWeatherRepository,
fpasRepository,
openWeatherRepository,
mgmRepository
mgmRepository,
kmiRepository
)
fun getWeatherRepository(source: Source): WeatherRepository {
@@ -100,7 +100,8 @@ abstract class BaseRepository : WeatherRepository, AlertRepository, AirQualityRe
} catch (e: Exception) {
return@withContext WeatherResult.Error(
exception = e.toAppException(),
weather = cache
weather = cache,
rawException = e
)
}
@@ -152,6 +153,7 @@ abstract class BaseRepository : WeatherRepository, AlertRepository, AirQualityRe
} else AlertsDataPack(alerts = cache, location = location)
} catch (e: Exception) {
return@withContext AlertResult.Error(
exception = e.toAppException(),
alerts = cache
+1 -1
View File
@@ -551,7 +551,7 @@
<string name="weather_refresh_delay_fixed">Please wait 15 minutes before refreshing</string>
<string name="country_turkey">Turkey</string>
<string name="country_belgium">Belgium</string>
</resources>