forked from Ruderverein-Donau-Linz/rowt
		
	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