summaryrefslogtreecommitdiff
path: root/src/weather_poller.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-14 20:35:00 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-14 20:35:00 +0300
commit1c2873b3059f3e4d6bd02307ec5b22f761ce1c80 (patch)
treede196a57b76fcacbbc842bbb5bf2641c8f82be91 /src/weather_poller.rs
parent50ce8cb96b2b218751c2fc2a6b19372f51846acc (diff)
feat: Update routes and fix issues
Diffstat (limited to 'src/weather_poller.rs')
-rw-r--r--src/weather_poller.rs177
1 files changed, 138 insertions, 39 deletions
diff --git a/src/weather_poller.rs b/src/weather_poller.rs
index 056cef8..4fd418d 100644
--- a/src/weather_poller.rs
+++ b/src/weather_poller.rs
@@ -1,13 +1,13 @@
-use crate::users::UserRepository;
use crate::locations::LocationRepository;
-use crate::weather_thresholds::{WeatherThresholdRepository, WeatherThreshold};
-use crate::notifications::{NtfySettingsRepository, SmtpSettingsRepository, NtfySettings, SmtpSettings};
-use serde_json::Value;
-use tera::{Tera, Context};
+use crate::notifications::{
+ NtfySettings, NtfySettingsRepository, SmtpSettings, SmtpSettingsRepository,
+};
+use crate::users::UserRepository;
+use crate::weather_thresholds::{WeatherThreshold, WeatherThresholdRepository};
use reqwest::Client;
+use serde_json::Value;
use std::sync::{Arc, Mutex};
-use tokio_task_scheduler::{Scheduler, Task};
-use tokio::time::Duration;
+use tera::{Context, Tera};
const OWM_API_URL: &str = "https://api.openweathermap.org/data/2.5/forecast";
const ALERT_WINDOW_HOURS: i64 = 24;
@@ -24,8 +24,15 @@ impl WeatherPoller {
let loc_repo = LocationRepository { db: &self.db };
let users = user_repo.list_users().await.unwrap_or_default();
for user in users {
- if let Some(location) = loc_repo.list_locations().await.unwrap_or_default().into_iter().find(|l| l.user_id == user.id) {
- self.check_user_weather(user.id, location.latitude, location.longitude).await;
+ if let Some(location) = loc_repo
+ .list_locations()
+ .await
+ .unwrap_or_default()
+ .into_iter()
+ .find(|l| l.user_id == user.id)
+ {
+ self.check_user_weather(user.id, location.latitude, location.longitude)
+ .await;
}
}
}
@@ -33,16 +40,27 @@ impl WeatherPoller {
pub async fn check_user_weather(&self, user_id: i64, lat: f64, lon: f64) {
if let Ok(Some(forecast)) = self.fetch_forecast(lat, lon).await {
let threshold_repo = WeatherThresholdRepository { db: &self.db };
- let thresholds = threshold_repo.list_thresholds(user_id).await.unwrap_or_default().into_iter().filter(|t| t.enabled).collect::<Vec<_>>();
+ let thresholds = threshold_repo
+ .list_thresholds(user_id)
+ .await
+ .unwrap_or_default()
+ .into_iter()
+ .filter(|t| t.enabled)
+ .collect::<Vec<_>>();
if let Some(entry) = find_first_alert_entry(&forecast, &thresholds) {
self.send_notifications(user_id, &entry).await;
}
}
}
- pub async fn fetch_forecast(&self, lat: f64, lon: f64) -> Result<Option<Vec<Value>>, reqwest::Error> {
+ pub async fn fetch_forecast(
+ &self,
+ lat: f64,
+ lon: f64,
+ ) -> Result<Option<Vec<Value>>, reqwest::Error> {
let client = Client::new();
- let resp = client.get(OWM_API_URL)
+ let resp = client
+ .get(OWM_API_URL)
.query(&[
("lat", lat.to_string()),
("lon", lon.to_string()),
@@ -73,7 +91,7 @@ impl WeatherPoller {
}
fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) -> Option<Value> {
- use chrono::{Utc, TimeZone};
+ use chrono::{TimeZone, Utc};
let now = Utc::now();
for entry in forecast {
if let Some(ts) = entry["dt"].as_i64() {
@@ -91,9 +109,21 @@ fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) -
fn threshold_triggered(threshold: &WeatherThreshold, entry: &Value) -> bool {
let value = match threshold.condition_type.as_str() {
- "wind_speed" => entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6,
- "rain" => entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0),
- "temp_min" | "temp_max" => entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0),
+ "wind_speed" => {
+ entry
+ .pointer("/wind/speed")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0)
+ * 3.6
+ }
+ "rain" => entry
+ .pointer("/rain/3h")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
+ "temp_min" | "temp_max" => entry
+ .pointer("/main/temp")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
_ => return false,
};
compare(value, &threshold.operator, threshold.threshold_value)
@@ -105,28 +135,35 @@ fn compare(value: f64, op: &str, threshold: f64) -> bool {
">=" => value >= threshold,
"<" => value < threshold,
"<=" => value <= threshold,
- "==" => (value - threshold).abs() < std::f64::EPSILON,
+ "==" => (value - threshold).abs() < f64::EPSILON,
_ => false,
}
}
-async fn send_ntfy_notification(ntfy: &NtfySettings, weather_entry: &Value, tera: Arc<Mutex<Tera>>) {
+async fn send_ntfy_notification(
+ ntfy: &NtfySettings,
+ weather_entry: &Value,
+ tera: Arc<Mutex<Tera>>,
+) {
let mut ctx = Context::new();
add_weather_context(&mut ctx, weather_entry);
let title = if let Some(tpl) = &ntfy.title_template {
let mut tera = tera.lock().unwrap();
- tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather Alert".to_string())
+ tera.render_str(tpl, &ctx)
+ .unwrap_or_else(|_| "🚨 Weather Alert".to_string())
} else {
"🚨 Weather Alert".to_string()
};
let message = if let Some(tpl) = &ntfy.message_template {
let mut tera = tera.lock().unwrap();
- tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather alert for your location".to_string())
+ tera.render_str(tpl, &ctx)
+ .unwrap_or_else(|_| "🚨 Weather alert for your location".to_string())
} else {
default_weather_message(weather_entry)
};
let client = Client::new();
- let _ = client.post(&format!("{}/{}", ntfy.server_url, ntfy.topic))
+ let _ = client
+ .post(&format!("{}/{}", ntfy.server_url, ntfy.topic))
.header("Priority", ntfy.priority.to_string())
.header("Title", title)
.body(message)
@@ -134,38 +171,56 @@ async fn send_ntfy_notification(ntfy: &NtfySettings, weather_entry: &Value, tera
.await;
}
-async fn send_smtp_notification(smtp: &SmtpSettings, weather_entry: &Value, tera: Arc<Mutex<Tera>>) {
+async fn send_smtp_notification(
+ smtp: &SmtpSettings,
+ weather_entry: &Value,
+ tera: Arc<Mutex<Tera>>,
+) {
use lettre::{Message, SmtpTransport, Transport, transport::smtp::authentication::Credentials};
let mut ctx = Context::new();
add_weather_context(&mut ctx, weather_entry);
let subject = if let Some(tpl) = &smtp.subject_template {
let mut tera = tera.lock().unwrap();
- tera.render_str(tpl, &ctx).unwrap_or_else(|_| "⚠️ Weather Alert for Your Location".to_string())
+ tera.render_str(tpl, &ctx)
+ .unwrap_or_else(|_| "⚠️ Weather Alert for Your Location".to_string())
} else {
"⚠️ Weather Alert for Your Location".to_string()
};
let body = if let Some(tpl) = &smtp.body_template {
let mut tera = tera.lock().unwrap();
- tera.render_str(tpl, &ctx).unwrap_or_else(|_| default_weather_message(weather_entry))
+ tera.render_str(tpl, &ctx)
+ .unwrap_or_else(|_| default_weather_message(weather_entry))
} else {
default_weather_message(weather_entry)
};
- let from = smtp.from_email.clone().unwrap_or_else(|| smtp.email.clone());
- let from_name = smtp.from_name.clone().unwrap_or_else(|| "Silmätaivas Alerts".to_string());
+ let from = smtp
+ .from_email
+ .clone()
+ .unwrap_or_else(|| smtp.email.clone());
+ let from_name = smtp
+ .from_name
+ .clone()
+ .unwrap_or_else(|| "Silmätaivas Alerts".to_string());
let email = Message::builder()
.from(format!("{} <{}>", from_name, from).parse().unwrap())
.to(smtp.email.parse().unwrap())
.subject(subject)
.body(body)
.unwrap();
- let creds = smtp.username.as_ref().and_then(|u| smtp.password.as_ref().map(|p| Credentials::new(u.clone(), p.clone())));
+ let creds = smtp.username.as_ref().and_then(|u| {
+ smtp.password
+ .as_ref()
+ .map(|p| Credentials::new(u.clone(), p.clone()))
+ });
let mailer = if let Some(creds) = creds {
- SmtpTransport::relay(&smtp.smtp_server).unwrap()
+ SmtpTransport::relay(&smtp.smtp_server)
+ .unwrap()
.port(smtp.smtp_port as u16)
.credentials(creds)
.build()
} else {
- SmtpTransport::relay(&smtp.smtp_server).unwrap()
+ SmtpTransport::relay(&smtp.smtp_server)
+ .unwrap()
.port(smtp.smtp_port as u16)
.build()
};
@@ -173,20 +228,64 @@ async fn send_smtp_notification(smtp: &SmtpSettings, weather_entry: &Value, tera
}
fn add_weather_context(ctx: &mut Context, entry: &Value) {
- ctx.insert("temp", &entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0));
- ctx.insert("wind_speed", &(entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6));
- ctx.insert("rain", &entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0));
+ ctx.insert(
+ "temp",
+ &entry
+ .pointer("/main/temp")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
+ );
+ ctx.insert(
+ "wind_speed",
+ &(entry
+ .pointer("/wind/speed")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0)
+ * 3.6),
+ );
+ ctx.insert(
+ "rain",
+ &entry
+ .pointer("/rain/3h")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
+ );
ctx.insert("time", &entry["dt_txt"].as_str().unwrap_or("N/A"));
- ctx.insert("humidity", &entry.pointer("/main/humidity").and_then(|v| v.as_f64()).unwrap_or(0.0));
- ctx.insert("pressure", &entry.pointer("/main/pressure").and_then(|v| v.as_f64()).unwrap_or(0.0));
+ ctx.insert(
+ "humidity",
+ &entry
+ .pointer("/main/humidity")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
+ );
+ ctx.insert(
+ "pressure",
+ &entry
+ .pointer("/main/pressure")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0),
+ );
}
fn default_weather_message(entry: &Value) -> String {
- let temp = entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0);
- let wind = entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6;
- let rain = entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0);
+ let temp = entry
+ .pointer("/main/temp")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0);
+ let wind = entry
+ .pointer("/wind/speed")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0)
+ * 3.6;
+ let rain = entry
+ .pointer("/rain/3h")
+ .and_then(|v| v.as_f64())
+ .unwrap_or(0.0);
let time = entry["dt_txt"].as_str().unwrap_or("N/A");
- format!("🚨 Weather alert for your location ({}):\n\n🌬️ Wind: {:.1} km/h\n🌧️ Rain: {:.1} mm\n🌡️ Temperature: {:.1} °C\n\nStay safe,\n— Silmätaivas", time, wind, rain, temp)
+ format!(
+ "🚨 Weather alert for your location ({}):\n\n🌬️ Wind: {:.1} km/h\n🌧️ Rain: {:.1} mm\n🌡️ Temperature: {:.1} °C\n\nStay safe,\n— Silmätaivas",
+ time, wind, rain, temp
+ )
}
-// Unit tests for threshold logic and template rendering can be added here. \ No newline at end of file
+// Unit tests for threshold logic and template rendering can be added here.