test
This commit is contained in:
+176
-55
@@ -1,36 +1,32 @@
|
|||||||
mod weather_data;
|
mod weather_data;
|
||||||
|
|
||||||
use chrono::{Local};
|
|
||||||
use crate::weather_data::{DwdWeather, MucWeather};
|
use crate::weather_data::{DwdWeather, MucWeather};
|
||||||
|
use chrono::Local;
|
||||||
|
use dotenv::dotenv;
|
||||||
use rumqttc::{AsyncClient, EventLoop, MqttOptions, QoS};
|
use rumqttc::{AsyncClient, EventLoop, MqttOptions, QoS};
|
||||||
use scraper::{Html as HtmlScraper, Selector};
|
use scraper::{Html as HtmlScraper, Selector};
|
||||||
|
use std::env;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::{task, time};
|
use tokio::{task, time};
|
||||||
use dotenv::dotenv;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
const METEO_MUNICH_API_URL: &str =
|
const METEO_MUNICH_API_URL: &str =
|
||||||
"https://www.meteorologie.lmu.de/~quicklooks/aktuelle_messwerte/messwerte_stadt.html";
|
"https://www.meteorologie.lmu.de/~quicklooks/aktuelle_messwerte/messwerte_stadt.html";
|
||||||
|
|
||||||
const LATITUDE_MUC: f64 = 48.163142;
|
const LATITUDE_MUC: f64 = 48.163142;
|
||||||
|
|
||||||
const LONGITUDE_MUC: f64 = 11.542922;
|
const LONGITUDE_MUC: f64 = 11.542922;
|
||||||
|
|
||||||
const PUBLISH_COUNT: i32 = 5;
|
const PUBLISH_COUNT: i32 = 5;
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
let (client, mut eventloop) = create_conn();
|
let (client, mut eventloop) = create_conn();
|
||||||
|
|
||||||
match meteo_weather().await {
|
match meteo_weather().await {
|
||||||
Ok(weather) => {
|
Ok(weather) => {
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
publish(client.clone(), &weather).await;
|
publish(client.clone(), &weather).await;
|
||||||
});
|
});
|
||||||
|
println!("test{}", 100);
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
loop {
|
loop {
|
||||||
let event = eventloop.poll().await;
|
let event = eventloop.poll().await;
|
||||||
@@ -39,7 +35,7 @@ async fn main() {
|
|||||||
if count == PUBLISH_COUNT * 2 {
|
if count == PUBLISH_COUNT * 2 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
count = count + 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error = {e:?}");
|
println!("Error = {e:?}");
|
||||||
@@ -53,12 +49,8 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
pub fn parse_html_weather(html_content: &str) -> (f64, f64, f64, f64, String) {
|
||||||
|
let fragment = HtmlScraper::parse_fragment(html_content);
|
||||||
let response = trpl::get(METEO_MUNICH_API_URL).await;
|
|
||||||
let response_text = response.text().await;
|
|
||||||
|
|
||||||
let fragment = HtmlScraper::parse_fragment(&response_text);
|
|
||||||
let mut temperature = 999.0;
|
let mut temperature = 999.0;
|
||||||
let mut humidity = 0.0;
|
let mut humidity = 0.0;
|
||||||
let mut pressure = 0.0;
|
let mut pressure = 0.0;
|
||||||
@@ -71,15 +63,13 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
let cell_selector = Selector::parse("td").unwrap();
|
let cell_selector = Selector::parse("td").unwrap();
|
||||||
let h4_selector = Selector::parse("h4").unwrap();
|
let h4_selector = Selector::parse("h4").unwrap();
|
||||||
|
|
||||||
let html_holder = fragment
|
let html_holder = fragment.select(&h4_selector).next().map(|e| e.inner_html());
|
||||||
.select(&h4_selector)
|
|
||||||
.next()
|
|
||||||
.map(|e| e.inner_html());
|
|
||||||
let date_time = html_holder
|
let date_time = html_holder
|
||||||
.as_ref()
|
.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())
|
.map(|s| s.trim())
|
||||||
.unwrap_or("");
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
if let Some(table) = fragment.select(&profile_selector).next() {
|
if let Some(table) = fragment.select(&profile_selector).next() {
|
||||||
for row in table.select(&row_selector) {
|
for row in table.select(&row_selector) {
|
||||||
@@ -88,11 +78,25 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
let text = cell.text().collect::<Vec<_>>().join(" ");
|
let text = cell.text().collect::<Vec<_>>().join(" ");
|
||||||
row_data.push(text.trim().to_string());
|
row_data.push(text.trim().to_string());
|
||||||
}
|
}
|
||||||
if row_data[0] == "Lufttemperatur" {
|
if row_data.len() >= 2 {
|
||||||
temperature = row_data[1].replace(" °C", "").trim().parse::<f64>().unwrap();
|
let raw_val = row_data[1].trim();
|
||||||
|
match row_data[0].as_str() {
|
||||||
|
"Lufttemperatur" => {
|
||||||
|
temperature = raw_val
|
||||||
|
.replace(" °C", "")
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or(temperature);
|
||||||
|
}
|
||||||
|
"Relative Feuchte" => {
|
||||||
|
humidity = raw_val
|
||||||
|
.replace(" %", "")
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or(humidity);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
if row_data[0] == "Relative Feuchte" {
|
|
||||||
humidity = row_data[1].replace(" %", "").trim().parse::<f64>().unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,8 +108,12 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
let text = cell.text().collect::<Vec<_>>().join(" ");
|
let text = cell.text().collect::<Vec<_>>().join(" ");
|
||||||
row_data.push(text.trim().to_string());
|
row_data.push(text.trim().to_string());
|
||||||
}
|
}
|
||||||
if row_data[0] == "Luftdruck NN" {
|
if row_data.len() >= 2 && row_data[0] == "Luftdruck NN" {
|
||||||
pressure = row_data[1].replace(" hPa", "").trim().parse::<f64>().unwrap();
|
pressure = row_data[1]
|
||||||
|
.replace(" hPa", "")
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or(pressure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,12 +125,26 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
let text = cell.text().collect::<Vec<_>>().join(" ");
|
let text = cell.text().collect::<Vec<_>>().join(" ");
|
||||||
row_data.push(text.trim().to_string());
|
row_data.push(text.trim().to_string());
|
||||||
}
|
}
|
||||||
if row_data.len() > 1 && row_data[1].contains(" mm") {
|
if row_data.len() >= 2 && row_data[1].contains(" mm") {
|
||||||
precipitation = row_data[1].replace(" mm", "").trim().parse::<f64>().unwrap();
|
precipitation = row_data[1]
|
||||||
|
.replace(" mm", "")
|
||||||
|
.trim()
|
||||||
|
.parse::<f64>()
|
||||||
|
.unwrap_or(precipitation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(temperature, humidity, pressure, precipitation, date_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
||||||
|
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 {
|
if temperature == 999.0 {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://api.brightsky.dev/current_weather?lat={}&lon={}",
|
"https://api.brightsky.dev/current_weather?lat={}&lon={}",
|
||||||
@@ -130,11 +152,12 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
);
|
);
|
||||||
println!("Fetching DWD {}", url);
|
println!("Fetching DWD {}", url);
|
||||||
let result = trpl::get(&url).await.text().await;
|
let result = trpl::get(&url).await.text().await;
|
||||||
let dwd_weather: DwdWeather = serde_json::from_str(&result).unwrap();
|
if let Ok(dwd_weather) = serde_json::from_str::<DwdWeather>(&result) {
|
||||||
temperature = dwd_weather.weather.temperature.unwrap();
|
temperature = dwd_weather.weather.temperature.unwrap_or(0.0);
|
||||||
pressure = dwd_weather.weather.pressure_msl;
|
pressure = dwd_weather.weather.pressure_msl.unwrap_or(0.0);
|
||||||
humidity = dwd_weather.weather.relative_humidity as f64;
|
humidity = dwd_weather.weather.relative_humidity.unwrap_or(0.0);
|
||||||
precipitation = dwd_weather.weather.precipitation;
|
precipitation = dwd_weather.weather.precipitation.unwrap_or(0.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let precipitation = (precipitation * 100.0).round() / 100.00;
|
let precipitation = (precipitation * 100.0).round() / 100.00;
|
||||||
@@ -146,47 +169,145 @@ pub async fn meteo_weather() -> Result<MucWeather, reqwest::Error> {
|
|||||||
humidity,
|
humidity,
|
||||||
precipitation,
|
precipitation,
|
||||||
date: cur_date_formatted,
|
date: cur_date_formatted,
|
||||||
meteo_date: date_time.to_string()
|
meteo_date: date_time,
|
||||||
};
|
};
|
||||||
println!("{:?}", weather);
|
println!("{:?}", weather);
|
||||||
Ok(weather)
|
Ok(weather)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn publish(client: AsyncClient, muc: &MucWeather) {
|
async fn publish(client: AsyncClient, muc: &MucWeather) {
|
||||||
|
if let Ok(json) = serde_json::to_string(&muc) {
|
||||||
let json = serde_json::to_string(&muc);
|
publish_topic(client.clone(), "munich/weather", json).await;
|
||||||
publish_topic(client.clone(), "munich/weather", json.unwrap()).await;
|
}
|
||||||
publish_topic(client.clone(), "munich/temperature", muc.temperature.to_string()).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/pressure", muc.pressure.to_string()).await;
|
||||||
publish_topic(client.clone(), "munich/humidity", muc.humidity.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;
|
time::sleep(Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn publish_topic(client: AsyncClient, topic: &str, value: String) {
|
async fn publish_topic(client: AsyncClient, topic: &str, value: String) {
|
||||||
client
|
let _ = client.publish(topic, QoS::AtLeastOnce, false, value).await;
|
||||||
.publish(
|
|
||||||
topic,
|
|
||||||
QoS::AtLeastOnce,
|
|
||||||
false,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_conn() -> (AsyncClient, EventLoop) {
|
fn create_conn() -> (AsyncClient, EventLoop) {
|
||||||
let mqtt_client = env::var("MQTT_CLIENT");
|
let mqtt_client = env::var("MQTT_CLIENT").unwrap_or_else(|_| "rust_weather".to_string());
|
||||||
let mqtt_broker = env::var("MQTT_BROKER");
|
let mqtt_broker = env::var("MQTT_BROKER").unwrap_or_else(|_| "localhost".to_string());
|
||||||
let mqtt_user = env::var("MQTT_USER");
|
let mqtt_user = env::var("MQTT_USER").unwrap_or_default();
|
||||||
let mqtt_password = env::var("MQTT_PASSWORD");
|
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
|
mqttoptions
|
||||||
.set_keep_alive(Duration::from_secs(5))
|
.set_keep_alive(Duration::from_secs(5))
|
||||||
.set_manual_acks(false)
|
.set_manual_acks(false)
|
||||||
.set_credentials(mqtt_user.unwrap(), mqtt_password.unwrap())
|
|
||||||
.set_clean_session(false);
|
.set_clean_session(false);
|
||||||
|
|
||||||
|
if !mqtt_user.is_empty() {
|
||||||
|
mqttoptions.set_credentials(mqtt_user, mqtt_password);
|
||||||
|
}
|
||||||
|
|
||||||
AsyncClient::new(mqttoptions, 10)
|
AsyncClient::new(mqttoptions, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_html_weather_valid() {
|
||||||
|
let sample_html = r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h4>Datum: 15.08.2026 14:00 Uhr</h4>
|
||||||
|
<table id="messwerte-profilwerte">
|
||||||
|
<tr><th>Parameter</th><th>Wert</th></tr>
|
||||||
|
<tr><td>Lufttemperatur</td><td>23.5 °C</td></tr>
|
||||||
|
<tr><td>Relative Feuchte</td><td>55.0 %</td></tr>
|
||||||
|
</table>
|
||||||
|
<table id="messwerte-winddruck">
|
||||||
|
<tr><td>Luftdruck NN</td><td>1015.2 hPa</td></tr>
|
||||||
|
</table>
|
||||||
|
<table id="messwerte-niederschlag">
|
||||||
|
<tr><td>Niederschlag Tag</td><td>0.5 mm</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
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#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<table id="messwerte-profilwerte">
|
||||||
|
<tr></tr>
|
||||||
|
<tr><th>Header Only</th></tr>
|
||||||
|
</table>
|
||||||
|
<table id="messwerte-winddruck">
|
||||||
|
<tr></tr>
|
||||||
|
</table>
|
||||||
|
<table id="messwerte-niederschlag">
|
||||||
|
<tr></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
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<DwdWeather, _> = 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-11
@@ -26,32 +26,32 @@ pub struct DwdWeather {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Weather {
|
pub struct Weather {
|
||||||
#[serde(rename = "source_id")]
|
#[serde(rename = "source_id")]
|
||||||
pub source_id: i64,
|
pub source_id: Option<i64>,
|
||||||
pub timestamp: String,
|
pub timestamp: Option<String>,
|
||||||
#[serde(rename = "cloud_cover")]
|
#[serde(rename = "cloud_cover")]
|
||||||
pub cloud_cover: i64,
|
pub cloud_cover: Option<f64>,
|
||||||
pub condition: String,
|
pub condition: Option<String>,
|
||||||
#[serde(rename = "dew_point")]
|
#[serde(rename = "dew_point")]
|
||||||
pub dew_point: Option<f64>,
|
pub dew_point: Option<f64>,
|
||||||
#[serde(rename = "solar_60")]
|
#[serde(rename = "solar_60")]
|
||||||
pub solar: Option<f64>,
|
pub solar: Option<f64>,
|
||||||
#[serde(rename = "precipitation_60")]
|
#[serde(rename = "precipitation_60")]
|
||||||
pub precipitation: f64,
|
pub precipitation: Option<f64>,
|
||||||
#[serde(rename = "pressure_msl")]
|
#[serde(rename = "pressure_msl")]
|
||||||
pub pressure_msl: f64,
|
pub pressure_msl: Option<f64>,
|
||||||
#[serde(rename = "relative_humidity")]
|
#[serde(rename = "relative_humidity")]
|
||||||
pub relative_humidity: i64,
|
pub relative_humidity: Option<f64>,
|
||||||
pub visibility: i64,
|
pub visibility: Option<f64>,
|
||||||
#[serde(rename = "wind_direction_60")]
|
#[serde(rename = "wind_direction_60")]
|
||||||
pub wind_direction: i64,
|
pub wind_direction: Option<f64>,
|
||||||
#[serde(rename = "wind_speed_60")]
|
#[serde(rename = "wind_speed_60")]
|
||||||
pub wind_speed: Option<f64>,
|
pub wind_speed: Option<f64>,
|
||||||
#[serde(rename = "wind_gust_direction_60")]
|
#[serde(rename = "wind_gust_direction_60")]
|
||||||
pub wind_gust_direction: i64,
|
pub wind_gust_direction: Option<f64>,
|
||||||
#[serde(rename = "wind_gust_speed_60")]
|
#[serde(rename = "wind_gust_speed_60")]
|
||||||
pub wind_gust_speed: Option<f64>,
|
pub wind_gust_speed: Option<f64>,
|
||||||
#[serde(rename = "sunshine_60")]
|
#[serde(rename = "sunshine_60")]
|
||||||
pub sunshine: Option<f64>,
|
pub sunshine: Option<f64>,
|
||||||
pub temperature: Option<f64>,
|
pub temperature: Option<f64>,
|
||||||
pub icon: String,
|
pub icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user