show-waterlevel #496

Merged
philipp merged 4 commits from show-waterlevel into staging 2024-05-16 14:59:07 +02:00
9 changed files with 224 additions and 6 deletions

View File

@ -4,3 +4,4 @@ rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"} limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "8kIjlLH79Ky6D3jQ" smtp_pw = "8kIjlLH79Ky6D3jQ"
usage_log_path = "./usage.txt" usage_log_path = "./usage.txt"
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"

View File

@ -186,3 +186,11 @@ CREATE TABLE IF NOT EXISTS "waterlevel" (
"tumin" INTEGER NOT NULL, "tumin" INTEGER NOT NULL,
"tumittel" INTEGER NOT NULL "tumittel" INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS "weather" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"day" DATE NOT NULL,
"max_temp" FLOAT NOT NULL,
"wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL
);

View File

@ -4,9 +4,9 @@ use std::str::FromStr;
#[cfg(feature = "rest")] #[cfg(feature = "rest")]
use rot::rest; use rot::rest;
use rot::scheduled;
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
use rot::tera; use rot::tera;
use rot::{scheduled, tera::Config};
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
@ -27,9 +27,7 @@ async fn rocket() -> _ {
.await .await
.unwrap(); .unwrap();
scheduled::schedule(&db); let rocket = rocket::build().manage(db.clone());
let rocket = rocket::build().manage(db);
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
let rocket = tera::config(rocket); let rocket = tera::config(rocket);
@ -37,5 +35,11 @@ async fn rocket() -> _ {
#[cfg(feature = "rest")] #[cfg(feature = "rest")]
let rocket = rest::config(rocket); let rocket = rest::config(rocket);
let config: Config = rocket
.figment()
.extract()
.expect("Config extraction failed");
scheduled::schedule(&db, &config);
rocket rocket
} }

View File

@ -6,6 +6,7 @@ use self::{
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype}, planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
trip::{Trip, TripWithUserAndType}, trip::{Trip, TripWithUserAndType},
waterlevel::Waterlevel, waterlevel::Waterlevel,
weather::Weather,
}; };
pub mod boat; pub mod boat;
@ -29,6 +30,7 @@ pub mod triptype;
pub mod user; pub mod user;
pub mod usertrip; pub mod usertrip;
pub mod waterlevel; pub mod waterlevel;
pub mod weather;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct Day { pub struct Day {
@ -37,6 +39,7 @@ pub struct Day {
trips: Vec<TripWithUserAndType>, trips: Vec<TripWithUserAndType>,
is_pinned: bool, is_pinned: bool,
max_waterlevel: Option<i64>, max_waterlevel: Option<i64>,
weather: Option<Weather>,
} }
impl Day { impl Day {
@ -48,6 +51,7 @@ impl Day {
trips: Trip::get_pinned_for_day(db, day).await, trips: Trip::get_pinned_for_day(db, day).await,
is_pinned, is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
} }
} else { } else {
Self { Self {
@ -56,6 +60,7 @@ impl Day {
trips: Trip::get_for_day(db, day).await, trips: Trip::get_for_day(db, day).await,
is_pinned, is_pinned,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
} }
} }
} }

56
src/model/weather.rs Normal file
View File

@ -0,0 +1,56 @@
use std::ops::DerefMut;
use chrono::NaiveDate;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Weather {
pub id: i64,
pub day: NaiveDate,
pub max_temp: f64,
pub wind_gust: f64,
pub rain_mm: f64,
}
impl Weather {
pub async fn find_by_day(db: &SqlitePool, day: NaiveDate) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, day: NaiveDate) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn create(
db: &mut Transaction<'_, Sqlite>,
day: NaiveDate,
max_temp: f64,
wind_gust: f64,
rain_mm: f64,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO weather(day, max_temp, wind_gust, rain_mm) VALUES (?,?,?,?)",
day,
max_temp,
wind_gust,
rain_mm
)
.execute(db.deref_mut())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
sqlx::query!("DELETE FROM weather;")
.execute(db.deref_mut())
.await
.unwrap();
}
}

View File

@ -1,4 +1,5 @@
mod waterlevel; mod waterlevel;
mod weather;
use std::time::Duration; use std::time::Duration;
@ -6,11 +7,15 @@ use job_scheduler_ng::{Job, JobScheduler};
use rocket::tokio::{self, task, time}; use rocket::tokio::{self, task, time};
use sqlx::SqlitePool; use sqlx::SqlitePool;
pub fn schedule(db: &SqlitePool) { use crate::tera::Config;
pub fn schedule(db: &SqlitePool, config: &Config) {
let db = db.clone(); let db = db.clone();
let openweathermap_key = config.openweathermap_key.clone();
tokio::task::spawn(async { tokio::task::spawn(async {
waterlevel::update(&db).await.unwrap(); waterlevel::update(&db).await.unwrap();
weather::update(&db, &openweathermap_key).await.unwrap();
let mut sched = JobScheduler::new(); let mut sched = JobScheduler::new();
@ -22,6 +27,9 @@ pub fn schedule(db: &SqlitePool) {
task::block_in_place(|| { task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async { tokio::runtime::Handle::current().block_on(async {
waterlevel::update(&db_clone).await.unwrap(); waterlevel::update(&db_clone).await.unwrap();
weather::update(&db_clone, &openweathermap_key)
.await
.unwrap();
}); });
}); });
})); }));

126
src/scheduled/weather.rs Normal file
View File

@ -0,0 +1,126 @@
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use crate::model::weather::Weather;
pub async fn update(db: &SqlitePool, api_key: &str) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
// 1. Delete weather data
Weather::delete_all(&mut tx).await;
// 2. Fetch
let data = fetch(api_key)?;
for d in data.daily {
let Some(date) = DateTime::from_timestamp(d.dt, 0) else {
println!("Skipping {} because convertion to datetime failed", d.dt);
continue;
};
let max_temp = d.temp.max;
let wind_gust = d.wind_gust;
let Some(rain_mm) = d.rain else {
println!(
"Skipping weather import of {} as there's no rain prognosed",
date
);
continue;
};
Weather::create(
&mut tx,
date.naive_utc().into(),
max_temp,
wind_gust,
rain_mm,
)
.await?
}
// 3. Save in DB
tx.commit().await.unwrap();
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Data {
lat: f64,
lon: f64,
timezone: String,
timezone_offset: i64,
daily: Vec<Daily>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Daily {
dt: i64,
sunrise: i64,
sunset: i64,
moonrise: i64,
moonset: i64,
moon_phase: f64,
summary: String,
temp: Temp,
feels_like: FeelsLike,
pressure: i64,
humidity: i64,
dew_point: f64,
wind_speed: f64,
wind_deg: i64,
wind_gust: f64,
weather: Vec<DailyWeather>,
clouds: i64,
pop: f64,
rain: Option<f64>,
uvi: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Temp {
day: f64,
min: f64,
max: f64,
night: f64,
eve: f64,
morn: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct FeelsLike {
day: f64,
night: f64,
eve: f64,
morn: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct DailyWeather {
id: i64,
main: String,
description: String,
icon: String,
}
fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
match ureq::get(&url).call() {
Ok(response) => {
let data: Result<Data, _> = response.into_json();
if let Ok(data) = data {
return Ok(data);
} else {
return Err(format!(
"Failed to parse the json received by {url}: {}",
data.err().unwrap()
));
}
}
Err(_) => {
return Err(format!(
"Could not fetch {url}, do you have internet? Maybe their server is down?"
));
}
}
}

View File

@ -166,6 +166,7 @@ pub struct Config {
rss_key: String, rss_key: String,
smtp_pw: String, smtp_pw: String,
usage_log_path: String, usage_log_path: String,
pub openweathermap_key: String,
} }
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {

View File

@ -92,9 +92,18 @@
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 "> <h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">
{{ day.day| date(format="%d.%m.%Y") }} {{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }} <small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}
{% if day.max_waterlevel %}&bullet; <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand" target="_blank" title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>{% endif %} {% if day.max_waterlevel %}
&bullet; <a href="https://hydro.ooe.gv.at/#/overview/Wasserstand/station/16668/Linz/Wasserstand"
target="_blank"
title="Prognostizierter maximaler Wasserstand am {{ day.day | date(format="%A", locale="de_AT") }}: {{ day.max_waterlevel }} cm">🌊{{ day.max_waterlevel }} cm</a>
{% endif %}
</small> </small>
</h2> </h2>
{% if day.weather %}
<div class="bg-gray-300 rounded text-center">
Max temp: {{ day.weather.max_temp }}° &bullet; Windböe: {{ day.weather.wind_gust }} km/h &bullet; Regen: {{ day.weather.rain_mm }} mm
</div>
{% endif %}
{% if day.planned_events | length > 0 or day.trips | length > 0 %} {% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div class="grid grid-cols-1 gap-3 mb-3"> <div class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #} {# --- START Events --- #}