Files
WeatherTool/src/main/scala/fetch/dmi/FetchService.scala
T

162 lines
5.8 KiB
Scala
Raw Normal View History

package fetch.dmi
2025-02-01 20:19:58 +02:00
import cats.effect._
2025-02-01 21:20:27 +02:00
import cats.implicits.toTraverseOps
import data.DataService
2025-02-01 21:20:27 +02:00
import fs2.io.file.{CopyFlag, CopyFlags, Files, Path}
import io.circe.Json
import io.circe.parser.decode
2025-02-01 20:19:58 +02:00
import org.http4s._
import org.http4s.ember.client.EmberClientBuilder
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
import parse.grib.GribParser
2025-02-01 20:19:58 +02:00
import java.time.ZonedDateTime
import scala.concurrent.duration.DurationInt
2025-02-08 22:31:23 +02:00
import scala.util.Try
2025-02-01 20:19:58 +02:00
final case class HarmonieServerConfig(
api_key: String,
url: String,
)
object FetchService {
2025-02-13 16:11:50 +02:00
def of(dataService: DataService): IO[FetchService] = {
Slf4jLogger.create[IO].map(logger => new FetchService(dataService, logger))
2025-02-01 20:19:58 +02:00
}
}
2025-02-13 16:11:50 +02:00
class FetchService(dataService: DataService, log: Logger[IO]) {
2025-02-08 22:31:23 +02:00
private val edrConfig: HarmonieServerConfig = (
2025-02-01 20:19:58 +02:00
sys.env.get("HARMONIE_EDR_API_KEY"),
sys.env.get("HARMONIE_EDR_URL"),
) match {
case (Some(api_key), Some(url)) =>
HarmonieServerConfig(api_key, url)
case _ =>
2025-02-13 16:11:50 +02:00
throw new RuntimeException("Unable to load harmonie edr config: Missing required environment variables")
2025-02-01 20:19:58 +02:00
}
2025-02-08 22:31:23 +02:00
private val stacConfig: HarmonieServerConfig = (
sys.env.get("HARMONIE_STAC_API_KEY"),
sys.env.get("HARMONIE_STAC_URL"),
) match {
case (Some(api_key), Some(url)) =>
HarmonieServerConfig(api_key, url)
case _ =>
2025-02-13 16:11:50 +02:00
throw new RuntimeException("Unable to load harmonie stac config: Missing required environment variables")
2025-02-08 22:31:23 +02:00
}
2025-02-01 20:19:58 +02:00
/*
https://dmigw.govcloud.dk/v1/forecastedr/collections/harmonie_dini_sf/grib
?parameter-name=temperature-2m
&datetime=2025-01-28T00:00:00Z
&api-key=b12d36c7-d7ba-4dca-9bc9-de0c9c27435f
*/
2025-02-08 22:31:23 +02:00
private val edrBaseUrl: IO[Uri] = IO(Uri.unsafeFromString(edrConfig.url))
private val stacBaseUrl: IO[Uri] = IO(Uri.unsafeFromString(stacConfig.url))
2025-02-01 20:19:58 +02:00
2025-02-01 21:20:27 +02:00
def fetchFromList(timeList: List[ZonedDateTime]): IO[List[String]] = {
timeList.traverse { time =>
fetchFromDateTime(time)
}
2025-02-01 20:19:58 +02:00
}
2025-02-01 21:20:27 +02:00
private def fetchFromDateTime(time: ZonedDateTime): IO[String] = {
val timeout = 60.seconds
EmberClientBuilder.default[IO]
.withTimeout(timeout)
.withIdleConnectionTime(timeout)
.build.use { client =>
2025-02-01 20:19:58 +02:00
for {
2025-02-08 22:31:23 +02:00
base <- edrBaseUrl
2025-02-01 20:19:58 +02:00
queryParams = Query.fromPairs(
2025-02-02 23:33:15 +02:00
// https://opendatadocs.dmi.govcloud.dk/Data/Forecast_Data_Weather_Model_HARMONIE_DINI_EDR
"parameter-name" -> "temperature-2m,total-precipitation,precipitation-type,wind-speed,gust-wind-speed-10m,wind-10m-u,wind-10m-v",
2025-02-01 20:19:58 +02:00
"datetime" -> time.toString,
2025-02-08 22:31:23 +02:00
"api-key" -> edrConfig.api_key,
2025-02-01 20:19:58 +02:00
)
urlWithParams = Uri.unsafeFromString(s"${base.toString}?${queryParams.toString}")
request = Request[IO](Method.GET, urlWithParams)
2025-02-13 16:11:50 +02:00
// TODO proly better to call dataService method than property
2025-02-23 20:21:11 +02:00
tmpPath = Path(s"${dataService.TMP_FOLDER}/tmp.grib")
2025-02-01 20:19:58 +02:00
_ <- client.stream(request)
.flatMap(_.body)
2025-02-01 21:20:27 +02:00
.through(Files[IO].writeAll(tmpPath))
2025-02-01 20:19:58 +02:00
.compile
.drain
2025-02-01 21:20:27 +02:00
gribList <- GribParser.parseFile(tmpPath)
gribTime = gribList.head.time
2025-02-13 16:11:50 +02:00
fileName = Path(s"${dataService.GRIB_FOLDER}/harmonie_${gribTime.referenceTime}_${gribTime.forecastTime}.grib".replace(":", ""))
2025-02-01 21:20:27 +02:00
_ <- Files[IO].move(tmpPath, fileName, CopyFlags.apply(CopyFlag.ReplaceExisting))
fileSizeBytes <- Files[IO].size(fileName)
fileSizeMB = fileSizeBytes.toDouble / (1024 * 1024)
_ <- log.info(s" ${"%.1f".format(fileSizeMB)} MB - ${fileName.fileName}")
} yield fileName.toString
2025-02-01 20:19:58 +02:00
}
}
2025-02-08 22:31:23 +02:00
/**
* get latest model run from STAC API
* check local forecast grib files not to download them again
* fetch those forecasts
*/
2025-02-12 23:32:30 +02:00
def fetchRecentForecasts(): IO[List[String]] = {
for {
dateTimeList <- generateFetchList()
resultList <- fetchFromList(dateTimeList)
2025-02-13 13:15:46 +02:00
_ <- IO.println("finish grib downloads")
2025-02-12 23:32:30 +02:00
} yield resultList
}
def generateFetchList(): IO[List[ZonedDateTime]] = {
for {
availableResult <- fetchAvailableForecasts()
(modelRun, forecastDateList) = availableResult
2025-02-13 16:11:50 +02:00
localForecasts <- dataService.getForecasts()
toFetchList = forecastDateList.filter(dateTime => !localForecasts.contains((modelRun, dateTime)))
} yield toFetchList
}
2025-02-08 22:31:23 +02:00
def fetchAvailableForecasts(): IO[(ZonedDateTime, List[ZonedDateTime])] = {
EmberClientBuilder.default[IO].build.use { client =>
for {
base <- stacBaseUrl
queryParams = Query.fromPairs(
"api-key" -> stacConfig.api_key,
)
urlWithParams = Uri.unsafeFromString(s"${base.toString}?${queryParams.toString}")
request = Request[IO](Method.GET, urlWithParams).withHeaders(org.http4s.headers.Accept(org.http4s.MediaType.application.json))
response <- client.expect[String](request)
json <- IO.fromEither(decode[Json](response))
result <- IO {
val features = json.hcursor.downField("features").values.getOrElse(List.empty)
val dateTimePairs = features.flatMap { feature =>
for {
properties <- feature.hcursor.downField("properties").focus
modelRun <- properties.hcursor.downField("modelRun").as[String].toOption
datetime <- properties.hcursor.downField("datetime").as[String].toOption
parsedModelRun <- Try(ZonedDateTime.parse(modelRun)).toOption
parsedDateTime <- Try(ZonedDateTime.parse(datetime)).toOption
} yield (parsedModelRun, parsedDateTime)
}
val latestModelRun = dateTimePairs.map(_._1).max
val datetimesForLatestRun = dateTimePairs
.filter(_._1 == latestModelRun)
.map(_._2)
.toList
.sorted
(latestModelRun, datetimesForLatestRun)
}
} yield result
}
}
2025-02-01 20:19:58 +02:00
}