Merge branch 'staging' into show-scheckbuch-info
This commit is contained in:
@ -8,6 +8,8 @@ pub mod tera;
|
||||
#[cfg(feature = "rest")]
|
||||
pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
use rot::rest;
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
|
||||
@ -26,7 +27,7 @@ async fn rocket() -> _ {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rocket = rocket::build().manage(db);
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
let rocket = tera::config(rocket);
|
||||
@ -34,5 +35,11 @@ async fn rocket() -> _ {
|
||||
#[cfg(feature = "rest")]
|
||||
let rocket = rest::config(rocket);
|
||||
|
||||
let config: Config = rocket
|
||||
.figment()
|
||||
.extract()
|
||||
.expect("Config extraction failed");
|
||||
scheduled::schedule(&db, &config);
|
||||
|
||||
rocket
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ use sqlx::SqlitePool;
|
||||
use self::{
|
||||
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
waterlevel::Waterlevel,
|
||||
weather::Weather,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
@ -27,6 +29,8 @@ pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
pub mod waterlevel;
|
||||
pub mod weather;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Day {
|
||||
@ -34,6 +38,8 @@ pub struct Day {
|
||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
max_waterlevel: Option<i64>,
|
||||
weather: Option<Weather>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
@ -44,6 +50,8 @@ impl Day {
|
||||
planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
|
||||
trips: Trip::get_pinned_for_day(db, day).await,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
@ -51,6 +59,8 @@ impl Day {
|
||||
planned_events: PlannedEvent::get_for_day(db, day).await,
|
||||
trips: Trip::get_for_day(db, day).await,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
src/model/waterlevel.rs
Normal file
72
src/model/waterlevel.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Waterlevel {
|
||||
pub id: i64,
|
||||
pub day: NaiveDate,
|
||||
pub time: String,
|
||||
pub max: i64,
|
||||
pub min: i64,
|
||||
pub mittel: i64,
|
||||
pub tumax: i64,
|
||||
pub tumin: i64,
|
||||
pub tumittel: i64,
|
||||
}
|
||||
|
||||
impl Waterlevel {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
day: NaiveDate,
|
||||
time: String,
|
||||
max: i64,
|
||||
min: i64,
|
||||
mittel: i64,
|
||||
tumax: i64,
|
||||
tumin: i64,
|
||||
tumittel: i64,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
|
||||
day, time, max, min, mittel, tumax, tumin, tumittel
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<i64> {
|
||||
sqlx::query!(
|
||||
"SELECT MAX(mittel) as max FROM waterlevel WHERE day = ?",
|
||||
day
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.max
|
||||
}
|
||||
|
||||
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM waterlevel;")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
56
src/model/weather.rs
Normal file
56
src/model/weather.rs
Normal 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();
|
||||
}
|
||||
}
|
43
src/scheduled/mod.rs
Normal file
43
src/scheduled/mod.rs
Normal file
@ -0,0 +1,43 @@
|
||||
mod waterlevel;
|
||||
mod weather;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use job_scheduler_ng::{Job, JobScheduler};
|
||||
use rocket::tokio::{self, task, time};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::Config;
|
||||
|
||||
pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let db = db.clone();
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
waterlevel::update(&db).await.unwrap();
|
||||
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
// Every hour
|
||||
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
|
||||
let db_clone = db.clone();
|
||||
// Use block_in_place to run async code in the synchronous function; TODO: Make it
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
waterlevel::update(&db_clone).await.unwrap();
|
||||
weather::update(&db_clone, &openweathermap_key)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
sched.tick();
|
||||
interval.tick().await;
|
||||
}
|
||||
});
|
||||
}
|
112
src/scheduled/waterlevel.rs
Normal file
112
src/scheduled/waterlevel.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::waterlevel::Waterlevel;
|
||||
|
||||
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete water levels starting from yesterday
|
||||
Waterlevel::delete_all(&mut tx).await;
|
||||
|
||||
// 2. Fetch
|
||||
let station = fetch()?;
|
||||
for d in station.data {
|
||||
let (Some(max), Some(min), Some(mittel), Some(tumax), Some(tumin), Some(tumittel)) =
|
||||
(d.max, d.min, d.mittel, d.tumax, d.tumin, d.tumittel)
|
||||
else {
|
||||
println!("Ignored invalid values: {d:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(datetime): Result<DateTime<FixedOffset>, _> = d.timestamp.parse() else {
|
||||
return Err("Failed to parse datetime from hydro json".into());
|
||||
};
|
||||
let date: NaiveDate = datetime.naive_utc().date();
|
||||
|
||||
// Extract time component and format as string
|
||||
let time: NaiveTime = datetime.naive_utc().time();
|
||||
let time_str = time.format("%H:%M").to_string();
|
||||
|
||||
Waterlevel::create(
|
||||
&mut tx, date, time_str, max, min, mittel, tumax, tumin, tumittel,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Station {
|
||||
station_no: String,
|
||||
station_latitude: String,
|
||||
station_longitude: String,
|
||||
parametertype_name: String,
|
||||
ts_shortname: String,
|
||||
ts_name: String,
|
||||
ts_unitname: String,
|
||||
ts_unitsymbol: String,
|
||||
ts_precision: String,
|
||||
rows: String,
|
||||
columns: String,
|
||||
data: Vec<Data>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Data {
|
||||
timestamp: String,
|
||||
max: Option<i64>,
|
||||
min: Option<i64>,
|
||||
mittel: Option<i64>,
|
||||
tumax: Option<i64>,
|
||||
tumin: Option<i64>,
|
||||
tumittel: Option<i64>,
|
||||
}
|
||||
|
||||
fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
return Ok(data[0].clone());
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
|
||||
data.len()
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Failed to parse the json received by {url}: {}",
|
||||
forecast.err().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(format!(
|
||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test {
|
||||
// use crate::testdb;
|
||||
//
|
||||
// use super::*;
|
||||
// #[sqlx::test]
|
||||
// fn test_fetch_succ() {
|
||||
// let pool = testdb!();
|
||||
// fetch();
|
||||
// }
|
||||
//}
|
120
src/scheduled/weather.rs
Normal file
120
src/scheduled/weather.rs
Normal file
@ -0,0 +1,120 @@
|
||||
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 rain_mm = d.rain.unwrap_or(0.);
|
||||
|
||||
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?"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -166,6 +166,7 @@ pub struct Config {
|
||||
rss_key: String,
|
||||
smtp_pw: String,
|
||||
usage_log_path: String,
|
||||
pub openweathermap_key: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
|
Reference in New Issue
Block a user