diff --git a/src/main.rs b/src/main.rs index f2905a9..eaaa95a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,32 @@ mod weather_data; -use chrono::{Local}; use crate::weather_data::{DwdWeather, MucWeather}; +use chrono::Local; +use dotenv::dotenv; use rumqttc::{AsyncClient, EventLoop, MqttOptions, QoS}; use scraper::{Html as HtmlScraper, Selector}; +use std::env; use std::time::Duration; use tokio::{task, time}; -use dotenv::dotenv; -use std::env; const METEO_MUNICH_API_URL: &str = "https://www.meteorologie.lmu.de/~quicklooks/aktuelle_messwerte/messwerte_stadt.html"; const LATITUDE_MUC: f64 = 48.163142; - const LONGITUDE_MUC: f64 = 11.542922; - const PUBLISH_COUNT: i32 = 5; #[tokio::main(flavor = "current_thread")] async fn main() { - dotenv().ok(); let (client, mut eventloop) = create_conn(); - match meteo_weather().await { Ok(weather) => { task::spawn(async move { publish(client.clone(), &weather).await; }); - + println!("test{}", 100); let mut count = 0; loop { let event = eventloop.poll().await; @@ -39,7 +35,7 @@ async fn main() { if count == PUBLISH_COUNT * 2 { break; } - count = count + 1; + count += 1; } Err(e) => { println!("Error = {e:?}"); @@ -53,17 +49,13 @@ async fn main() { } } -pub async fn meteo_weather() -> Result { - - let response = trpl::get(METEO_MUNICH_API_URL).await; - let response_text = response.text().await; - - let fragment = HtmlScraper::parse_fragment(&response_text); +pub fn parse_html_weather(html_content: &str) -> (f64, f64, f64, f64, String) { + let fragment = HtmlScraper::parse_fragment(html_content); let mut temperature = 999.0; let mut humidity = 0.0; let mut pressure = 0.0; let mut precipitation = 0.0; - + let profile_selector = Selector::parse("#messwerte-profilwerte").unwrap(); let pressure_selector = Selector::parse("#messwerte-winddruck").unwrap(); let precipitation_selector = Selector::parse("#messwerte-niederschlag").unwrap(); @@ -71,15 +63,13 @@ pub async fn meteo_weather() -> Result { let cell_selector = Selector::parse("td").unwrap(); let h4_selector = Selector::parse("h4").unwrap(); - let html_holder = fragment - .select(&h4_selector) - .next() - .map(|e| e.inner_html()); + let html_holder = fragment.select(&h4_selector).next().map(|e| e.inner_html()); let date_time = html_holder .as_ref() - .and_then(|html| html.lines().nth(0).and_then(|line| line.get(7..23))) + .and_then(|html| html.lines().next().and_then(|line| line.get(7..23))) .map(|s| s.trim()) - .unwrap_or(""); + .unwrap_or("") + .to_string(); if let Some(table) = fragment.select(&profile_selector).next() { for row in table.select(&row_selector) { @@ -88,11 +78,25 @@ pub async fn meteo_weather() -> Result { let text = cell.text().collect::>().join(" "); row_data.push(text.trim().to_string()); } - if row_data[0] == "Lufttemperatur" { - temperature = row_data[1].replace(" °C", "").trim().parse::().unwrap(); - } - if row_data[0] == "Relative Feuchte" { - humidity = row_data[1].replace(" %", "").trim().parse::().unwrap(); + if row_data.len() >= 2 { + let raw_val = row_data[1].trim(); + match row_data[0].as_str() { + "Lufttemperatur" => { + temperature = raw_val + .replace(" °C", "") + .trim() + .parse::() + .unwrap_or(temperature); + } + "Relative Feuchte" => { + humidity = raw_val + .replace(" %", "") + .trim() + .parse::() + .unwrap_or(humidity); + } + _ => {} + } } } } @@ -104,8 +108,12 @@ pub async fn meteo_weather() -> Result { let text = cell.text().collect::>().join(" "); row_data.push(text.trim().to_string()); } - if row_data[0] == "Luftdruck NN" { - pressure = row_data[1].replace(" hPa", "").trim().parse::().unwrap(); + if row_data.len() >= 2 && row_data[0] == "Luftdruck NN" { + pressure = row_data[1] + .replace(" hPa", "") + .trim() + .parse::() + .unwrap_or(pressure); } } } @@ -117,12 +125,26 @@ pub async fn meteo_weather() -> Result { let text = cell.text().collect::>().join(" "); row_data.push(text.trim().to_string()); } - if row_data.len() > 1 && row_data[1].contains(" mm") { - precipitation = row_data[1].replace(" mm", "").trim().parse::().unwrap(); + if row_data.len() >= 2 && row_data[1].contains(" mm") { + precipitation = row_data[1] + .replace(" mm", "") + .trim() + .parse::() + .unwrap_or(precipitation); } } } + (temperature, humidity, pressure, precipitation, date_time) +} + +pub async fn meteo_weather() -> Result { + let response = trpl::get(METEO_MUNICH_API_URL).await; + let response_text = response.text().await; + + let (mut temperature, mut humidity, mut pressure, mut precipitation, date_time) = + parse_html_weather(&response_text); + if temperature == 999.0 { let url = format!( "https://api.brightsky.dev/current_weather?lat={}&lon={}", @@ -130,11 +152,12 @@ pub async fn meteo_weather() -> Result { ); println!("Fetching DWD {}", url); let result = trpl::get(&url).await.text().await; - let dwd_weather: DwdWeather = serde_json::from_str(&result).unwrap(); - temperature = dwd_weather.weather.temperature.unwrap(); - pressure = dwd_weather.weather.pressure_msl; - humidity = dwd_weather.weather.relative_humidity as f64; - precipitation = dwd_weather.weather.precipitation; + if let Ok(dwd_weather) = serde_json::from_str::(&result) { + temperature = dwd_weather.weather.temperature.unwrap_or(0.0); + pressure = dwd_weather.weather.pressure_msl.unwrap_or(0.0); + humidity = dwd_weather.weather.relative_humidity.unwrap_or(0.0); + precipitation = dwd_weather.weather.precipitation.unwrap_or(0.0); + } } let precipitation = (precipitation * 100.0).round() / 100.00; @@ -146,47 +169,145 @@ pub async fn meteo_weather() -> Result { humidity, precipitation, date: cur_date_formatted, - meteo_date: date_time.to_string() + meteo_date: date_time, }; println!("{:?}", weather); Ok(weather) } async fn publish(client: AsyncClient, muc: &MucWeather) { - - let json = serde_json::to_string(&muc); - publish_topic(client.clone(), "munich/weather", json.unwrap()).await; - publish_topic(client.clone(), "munich/temperature", muc.temperature.to_string()).await; + if let Ok(json) = serde_json::to_string(&muc) { + publish_topic(client.clone(), "munich/weather", json).await; + } + publish_topic( + client.clone(), + "munich/temperature", + muc.temperature.to_string(), + ) + .await; publish_topic(client.clone(), "munich/pressure", muc.pressure.to_string()).await; publish_topic(client.clone(), "munich/humidity", muc.humidity.to_string()).await; - publish_topic(client.clone(), "munich/precipitation", muc.precipitation.to_string()).await; + publish_topic( + client.clone(), + "munich/precipitation", + muc.precipitation.to_string(), + ) + .await; time::sleep(Duration::from_secs(1)).await; } async fn publish_topic(client: AsyncClient, topic: &str, value: String) { - client - .publish( - topic, - QoS::AtLeastOnce, - false, - value, - ) - .await - .unwrap(); + let _ = client.publish(topic, QoS::AtLeastOnce, false, value).await; } fn create_conn() -> (AsyncClient, EventLoop) { - let mqtt_client = env::var("MQTT_CLIENT"); - let mqtt_broker = env::var("MQTT_BROKER"); - let mqtt_user = env::var("MQTT_USER"); - let mqtt_password = env::var("MQTT_PASSWORD"); + let mqtt_client = env::var("MQTT_CLIENT").unwrap_or_else(|_| "rust_weather".to_string()); + let mqtt_broker = env::var("MQTT_BROKER").unwrap_or_else(|_| "localhost".to_string()); + let mqtt_user = env::var("MQTT_USER").unwrap_or_default(); + let mqtt_password = env::var("MQTT_PASSWORD").unwrap_or_default(); - let mut mqttoptions = MqttOptions::new(mqtt_client.unwrap(), mqtt_broker.unwrap(), 1883); + println!("MQTT Broker: {}", mqtt_broker); + let mut mqttoptions = MqttOptions::new(mqtt_client, mqtt_broker, 1883); mqttoptions .set_keep_alive(Duration::from_secs(5)) .set_manual_acks(false) - .set_credentials(mqtt_user.unwrap(), mqtt_password.unwrap()) .set_clean_session(false); + + if !mqtt_user.is_empty() { + mqttoptions.set_credentials(mqtt_user, mqtt_password); + } + AsyncClient::new(mqttoptions, 10) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_html_weather_valid() { + let sample_html = r#" + + +

Datum: 15.08.2026 14:00 Uhr

+ + + + +
ParameterWert
Lufttemperatur23.5 °C
Relative Feuchte55.0 %
+ + +
Luftdruck NN1015.2 hPa
+ + +
Niederschlag Tag0.5 mm
+ + + "#; + + let (temp, hum, press, precip, date_time) = parse_html_weather(sample_html); + assert_eq!(temp, 23.5); + assert_eq!(hum, 55.0); + assert_eq!(press, 1015.2); + assert_eq!(precip, 0.5); + assert_eq!(date_time, "15.08.2026 14:00"); + } + + #[test] + fn test_parse_html_weather_empty_rows_no_panic() { + let sample_html = r#" + + + + + +
Header Only
+ + +
+ + +
+ + + "#; + + let (temp, hum, press, precip, _date_time) = parse_html_weather(sample_html); + assert_eq!(temp, 999.0); + assert_eq!(hum, 0.0); + assert_eq!(press, 0.0); + assert_eq!(precip, 0.0); + } + + #[test] + fn test_parse_brightsky_json_with_nulls() { + let json_data = r#"{ + "weather": { + "source_id": 1234, + "timestamp": "2026-08-15T12:00:00+00:00", + "cloud_cover": null, + "condition": "dry", + "dew_point": 12.3, + "solar_60": null, + "precipitation_60": 0.0, + "pressure_msl": 1014.5, + "relative_humidity": 60.0, + "visibility": null, + "wind_direction_60": 180.0, + "wind_speed_60": 15.2, + "wind_gust_direction_60": null, + "wind_gust_speed_60": null, + "sunshine_60": 45.0, + "temperature": 22.1, + "icon": "partly-cloudy-day" + } + }"#; + + let dwd: Result = serde_json::from_str(json_data); + assert!(dwd.is_ok()); + let weather = dwd.unwrap().weather; + assert_eq!(weather.temperature, Some(22.1)); + assert_eq!(weather.cloud_cover, None); + assert_eq!(weather.pressure_msl, Some(1014.5)); + } +} diff --git a/src/weather_data.rs b/src/weather_data.rs index 93082ce..a611425 100644 --- a/src/weather_data.rs +++ b/src/weather_data.rs @@ -26,32 +26,32 @@ pub struct DwdWeather { #[serde(rename_all = "camelCase")] pub struct Weather { #[serde(rename = "source_id")] - pub source_id: i64, - pub timestamp: String, + pub source_id: Option, + pub timestamp: Option, #[serde(rename = "cloud_cover")] - pub cloud_cover: i64, - pub condition: String, + pub cloud_cover: Option, + pub condition: Option, #[serde(rename = "dew_point")] pub dew_point: Option, #[serde(rename = "solar_60")] pub solar: Option, #[serde(rename = "precipitation_60")] - pub precipitation: f64, + pub precipitation: Option, #[serde(rename = "pressure_msl")] - pub pressure_msl: f64, + pub pressure_msl: Option, #[serde(rename = "relative_humidity")] - pub relative_humidity: i64, - pub visibility: i64, + pub relative_humidity: Option, + pub visibility: Option, #[serde(rename = "wind_direction_60")] - pub wind_direction: i64, + pub wind_direction: Option, #[serde(rename = "wind_speed_60")] pub wind_speed: Option, #[serde(rename = "wind_gust_direction_60")] - pub wind_gust_direction: i64, + pub wind_gust_direction: Option, #[serde(rename = "wind_gust_speed_60")] pub wind_gust_speed: Option, #[serde(rename = "sunshine_60")] pub sunshine: Option, pub temperature: Option, - pub icon: String, + pub icon: Option, }