add weather infos
This commit is contained in:
		| @@ -4,3 +4,4 @@ rss_key = "rss-key-for-ci" | ||||
| limits = { file = "10 MiB", data-form = "10 MiB"} | ||||
| smtp_pw = "8kIjlLH79Ky6D3jQ" | ||||
| usage_log_path = "./usage.txt" | ||||
| openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5" | ||||
|   | ||||
| @@ -186,3 +186,11 @@ CREATE TABLE IF NOT EXISTS "waterlevel" ( | ||||
| 	"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 | ||||
| ); | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -4,9 +4,9 @@ use std::str::FromStr; | ||||
|  | ||||
| #[cfg(feature = "rest")] | ||||
| use rot::rest; | ||||
| use rot::scheduled; | ||||
| #[cfg(feature = "rowing-tera")] | ||||
| use rot::tera; | ||||
| use rot::{scheduled, tera::Config}; | ||||
|  | ||||
| use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions}; | ||||
|  | ||||
| @@ -27,9 +27,7 @@ async fn rocket() -> _ { | ||||
|         .await | ||||
|         .unwrap(); | ||||
|  | ||||
|     scheduled::schedule(&db); | ||||
|  | ||||
|     let rocket = rocket::build().manage(db); | ||||
|     let rocket = rocket::build().manage(db.clone()); | ||||
|  | ||||
|     #[cfg(feature = "rowing-tera")] | ||||
|     let rocket = tera::config(rocket); | ||||
| @@ -37,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 | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ use self::{ | ||||
|     planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype}, | ||||
|     trip::{Trip, TripWithUserAndType}, | ||||
|     waterlevel::Waterlevel, | ||||
|     weather::Weather, | ||||
| }; | ||||
|  | ||||
| pub mod boat; | ||||
| @@ -29,6 +30,7 @@ pub mod triptype; | ||||
| pub mod user; | ||||
| pub mod usertrip; | ||||
| pub mod waterlevel; | ||||
| pub mod weather; | ||||
|  | ||||
| #[derive(Serialize, Debug)] | ||||
| pub struct Day { | ||||
| @@ -37,6 +39,7 @@ pub struct Day { | ||||
|     trips: Vec<TripWithUserAndType>, | ||||
|     is_pinned: bool, | ||||
|     max_waterlevel: Option<i64>, | ||||
|     weather: Option<Weather>, | ||||
| } | ||||
|  | ||||
| impl Day { | ||||
| @@ -48,6 +51,7 @@ impl Day { | ||||
|                 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 { | ||||
| @@ -56,6 +60,7 @@ impl Day { | ||||
|                 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, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										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(); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| mod waterlevel; | ||||
| mod weather; | ||||
|  | ||||
| use std::time::Duration; | ||||
|  | ||||
| @@ -6,11 +7,15 @@ use job_scheduler_ng::{Job, JobScheduler}; | ||||
| use rocket::tokio::{self, task, time}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| pub fn schedule(db: &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(); | ||||
|  | ||||
| @@ -22,6 +27,9 @@ pub fn schedule(db: &SqlitePool) { | ||||
|             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(); | ||||
|                 }); | ||||
|             }); | ||||
|         })); | ||||
|   | ||||
							
								
								
									
										126
									
								
								src/scheduled/weather.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/scheduled/weather.rs
									
									
									
									
									
										Normal 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?" | ||||
|             )); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -174,6 +174,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> { | ||||
|   | ||||
| @@ -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 "> | ||||
|                         {{ 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") }} | ||||
|                             {% 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 %} | ||||
|                             {% 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> | ||||
|                     {% if day.weather %} | ||||
|                         <div class="bg-gray-300 rounded text-center"> | ||||
|                             Max temp: {{ day.weather.max_temp }}° • Windböe: {{ day.weather.wind_gust }} km/h • Regen: {{ day.weather.rain_mm }} mm | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                     {% if day.planned_events | length > 0 or  day.trips | length > 0 %} | ||||
|                         <div class="grid grid-cols-1 gap-3 mb-3"> | ||||
|                             {# --- START Events --- #} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user