Merge branch 'staging' into show-scheckbuch-info
This commit is contained in:
		
							
								
								
									
										117
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										117
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -485,6 +485,26 @@ version = "2.4.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "crc32fast" | ||||||
|  | version = "1.4.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "cron" | ||||||
|  | version = "0.12.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" | ||||||
|  | dependencies = [ | ||||||
|  |  "chrono", | ||||||
|  |  "nom", | ||||||
|  |  "once_cell", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "crossbeam-channel" | name = "crossbeam-channel" | ||||||
| version = "0.5.12" | version = "0.5.12" | ||||||
| @@ -774,6 +794,16 @@ version = "1.2.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" | checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "flate2" | ||||||
|  | version = "1.0.30" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" | ||||||
|  | dependencies = [ | ||||||
|  |  "crc32fast", | ||||||
|  |  "miniz_oxide", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "flume" | name = "flume" | ||||||
| version = "0.11.0" | version = "0.11.0" | ||||||
| @@ -1301,6 +1331,17 @@ version = "1.0.11" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "job_scheduler_ng" | ||||||
|  | version = "2.0.5" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "87c252207f323e2996d087759ebdcff8f608cd3eaa9896909a0c2dd3050a3c6a" | ||||||
|  | dependencies = [ | ||||||
|  |  "chrono", | ||||||
|  |  "cron", | ||||||
|  |  "uuid", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "js-sys" | name = "js-sys" | ||||||
| version = "0.3.69" | version = "0.3.69" | ||||||
| @@ -2227,6 +2268,7 @@ dependencies = [ | |||||||
|  "futures", |  "futures", | ||||||
|  "ics", |  "ics", | ||||||
|  "itertools", |  "itertools", | ||||||
|  |  "job_scheduler_ng", | ||||||
|  "lettre", |  "lettre", | ||||||
|  "log", |  "log", | ||||||
|  "openssl", |  "openssl", | ||||||
| @@ -2236,6 +2278,7 @@ dependencies = [ | |||||||
|  "serde_json", |  "serde_json", | ||||||
|  "sqlx", |  "sqlx", | ||||||
|  "tera", |  "tera", | ||||||
|  |  "ureq", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2284,10 +2327,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" | checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "ring", |  "ring", | ||||||
|  "rustls-webpki", |  "rustls-webpki 0.101.7", | ||||||
|  "sct", |  "sct", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "rustls" | ||||||
|  | version = "0.22.4" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" | ||||||
|  | dependencies = [ | ||||||
|  |  "log", | ||||||
|  |  "ring", | ||||||
|  |  "rustls-pki-types", | ||||||
|  |  "rustls-webpki 0.102.3", | ||||||
|  |  "subtle", | ||||||
|  |  "zeroize", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "rustls-pemfile" | name = "rustls-pemfile" | ||||||
| version = "1.0.4" | version = "1.0.4" | ||||||
| @@ -2297,6 +2354,12 @@ dependencies = [ | |||||||
|  "base64 0.21.7", |  "base64 0.21.7", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "rustls-pki-types" | ||||||
|  | version = "1.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "rustls-webpki" | name = "rustls-webpki" | ||||||
| version = "0.101.7" | version = "0.101.7" | ||||||
| @@ -2307,6 +2370,17 @@ dependencies = [ | |||||||
|  "untrusted", |  "untrusted", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "rustls-webpki" | ||||||
|  | version = "0.102.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" | ||||||
|  | dependencies = [ | ||||||
|  |  "ring", | ||||||
|  |  "rustls-pki-types", | ||||||
|  |  "untrusted", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "rustversion" | name = "rustversion" | ||||||
| version = "1.0.15" | version = "1.0.15" | ||||||
| @@ -2590,7 +2664,7 @@ dependencies = [ | |||||||
|  "once_cell", |  "once_cell", | ||||||
|  "paste", |  "paste", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "rustls", |  "rustls 0.21.10", | ||||||
|  "rustls-pemfile", |  "rustls-pemfile", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
| @@ -2603,7 +2677,7 @@ dependencies = [ | |||||||
|  "tokio-stream", |  "tokio-stream", | ||||||
|  "tracing", |  "tracing", | ||||||
|  "url", |  "url", | ||||||
|  "webpki-roots", |  "webpki-roots 0.25.4", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -3232,6 +3306,25 @@ version = "0.9.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "ureq" | ||||||
|  | version = "2.9.7" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" | ||||||
|  | dependencies = [ | ||||||
|  |  "base64 0.22.0", | ||||||
|  |  "flate2", | ||||||
|  |  "log", | ||||||
|  |  "once_cell", | ||||||
|  |  "rustls 0.22.4", | ||||||
|  |  "rustls-pki-types", | ||||||
|  |  "rustls-webpki 0.102.3", | ||||||
|  |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  |  "url", | ||||||
|  |  "webpki-roots 0.26.1", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "url" | name = "url" | ||||||
| version = "2.5.0" | version = "2.5.0" | ||||||
| @@ -3255,6 +3348,15 @@ version = "0.2.1" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "uuid" | ||||||
|  | version = "1.8.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" | ||||||
|  | dependencies = [ | ||||||
|  |  "getrandom", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "valuable" | name = "valuable" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| @@ -3364,6 +3466,15 @@ version = "0.25.4" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" | checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "webpki-roots" | ||||||
|  | version = "0.26.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" | ||||||
|  | dependencies = [ | ||||||
|  |  "rustls-pki-types", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "whoami" | name = "whoami" | ||||||
| version = "1.5.1" | version = "1.5.1" | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ futures = "0.3" | |||||||
| lettre = "0.11" | lettre = "0.11" | ||||||
| csv = "1.3" | csv = "1.3" | ||||||
| itertools = "0.12" | itertools = "0.12" | ||||||
|  | job_scheduler_ng = "2.0" | ||||||
|  | ureq = { version = "2.9", features = ["json"] } | ||||||
|  |  | ||||||
| [target.'cfg(not(windows))'.dependencies] | [target.'cfg(not(windows))'.dependencies] | ||||||
| openssl = { version = "0.10", features = [ "vendored" ] } | openssl = { version = "0.10", features = [ "vendored" ] } | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -175,3 +175,22 @@ CREATE TABLE IF NOT EXISTS "boat_reservation" ( | |||||||
| 	"created_at" datetime not null default CURRENT_TIMESTAMP | 	"created_at" datetime not null default CURRENT_TIMESTAMP | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS "waterlevel" ( | ||||||
|  | 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|  | 	"day" DATE NOT NULL, | ||||||
|  | 	"time" TEXT NOT NULL, | ||||||
|  | 	"max" INTEGER NOT NULL, | ||||||
|  | 	"min" INTEGER NOT NULL, | ||||||
|  | 	"mittel" INTEGER NOT NULL, | ||||||
|  | 	"tumax" INTEGER NOT NULL, | ||||||
|  | 	"tumin" 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 | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ pub mod tera; | |||||||
| #[cfg(feature = "rest")] | #[cfg(feature = "rest")] | ||||||
| pub mod rest; | pub mod rest; | ||||||
|  |  | ||||||
|  | pub mod scheduled; | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| #[macro_export] | #[macro_export] | ||||||
| macro_rules! testdb { | macro_rules! testdb { | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ use std::str::FromStr; | |||||||
| use rot::rest; | use rot::rest; | ||||||
| #[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}; | ||||||
|  |  | ||||||
| @@ -26,7 +27,7 @@ async fn rocket() -> _ { | |||||||
|         .await |         .await | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
|  |  | ||||||
|     let rocket = rocket::build().manage(db); |     let rocket = rocket::build().manage(db.clone()); | ||||||
|  |  | ||||||
|     #[cfg(feature = "rowing-tera")] |     #[cfg(feature = "rowing-tera")] | ||||||
|     let rocket = tera::config(rocket); |     let rocket = tera::config(rocket); | ||||||
| @@ -34,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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ use sqlx::SqlitePool; | |||||||
| use self::{ | use self::{ | ||||||
|     planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype}, |     planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype}, | ||||||
|     trip::{Trip, TripWithUserAndType}, |     trip::{Trip, TripWithUserAndType}, | ||||||
|  |     waterlevel::Waterlevel, | ||||||
|  |     weather::Weather, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| pub mod boat; | pub mod boat; | ||||||
| @@ -27,6 +29,8 @@ pub mod tripdetails; | |||||||
| pub mod triptype; | pub mod triptype; | ||||||
| pub mod user; | pub mod user; | ||||||
| pub mod usertrip; | pub mod usertrip; | ||||||
|  | pub mod waterlevel; | ||||||
|  | pub mod weather; | ||||||
|  |  | ||||||
| #[derive(Serialize, Debug)] | #[derive(Serialize, Debug)] | ||||||
| pub struct Day { | pub struct Day { | ||||||
| @@ -34,6 +38,8 @@ pub struct Day { | |||||||
|     planned_events: Vec<PlannedEventWithUserAndTriptype>, |     planned_events: Vec<PlannedEventWithUserAndTriptype>, | ||||||
|     trips: Vec<TripWithUserAndType>, |     trips: Vec<TripWithUserAndType>, | ||||||
|     is_pinned: bool, |     is_pinned: bool, | ||||||
|  |     max_waterlevel: Option<i64>, | ||||||
|  |     weather: Option<Weather>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Day { | impl Day { | ||||||
| @@ -44,6 +50,8 @@ impl Day { | |||||||
|                 planned_events: PlannedEvent::get_pinned_for_day(db, day).await, |                 planned_events: PlannedEvent::get_pinned_for_day(db, day).await, | ||||||
|                 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, | ||||||
|  |                 weather: Weather::find_by_day(db, day).await, | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             Self { |             Self { | ||||||
| @@ -51,6 +59,8 @@ impl Day { | |||||||
|                 planned_events: PlannedEvent::get_for_day(db, day).await, |                 planned_events: PlannedEvent::get_for_day(db, day).await, | ||||||
|                 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, | ||||||
|  |                 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, |     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> { | ||||||
|   | |||||||
| @@ -96,7 +96,13 @@ | |||||||
|                 <div> |                 <div> | ||||||
|                     <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> |                         <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 %} | ||||||
|  |                                 • <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> | ||||||
|                     </h2> |                     </h2> | ||||||
|                     {% if day.weather %} |                     {% if day.weather %} | ||||||
|                         <div class="bg-gray-300 text-center"> |                         <div class="bg-gray-300 text-center"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user