Add weatherapi collector
This commit is contained in:
58
Cargo.lock
generated
58
Cargo.lock
generated
@ -115,6 +115,21 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6127248204b9aba09a362f6c930ef6a78f2c1b2215f8a7b398c06e1083f17af0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.44",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.6"
|
||||
@ -179,7 +194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"time 0.3.11",
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
@ -402,7 +417,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@ -514,6 +529,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"clap",
|
||||
"common",
|
||||
"futures",
|
||||
@ -791,7 +807,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@ -827,6 +843,25 @@ dependencies = [
|
||||
"twoway",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.1"
|
||||
@ -1399,6 +1434,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.11"
|
||||
@ -1726,6 +1772,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
||||
@ -10,7 +10,7 @@ pretty_env_logger = "0.4.0"
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
warp = "0.3.2"
|
||||
ron = "0.7.1"
|
||||
reqwest = { version = "0.11.11", default_features = false, features = ["rustls-tls"] }
|
||||
reqwest = { version = "0.11.11", default_features = false, features = ["rustls-tls", "json"] }
|
||||
async-trait = "0.1.56"
|
||||
anyhow = "1.0.58"
|
||||
markdown = "0.3.0"
|
||||
@ -18,6 +18,7 @@ clap = { version = "3.2.6", features = ["derive"] }
|
||||
toml = "0.5.9"
|
||||
serde = { version = "1.0.138", features = ["derive"] }
|
||||
futures = "0.3.21"
|
||||
chrono = { version = "0.4.20", features = ["serde"] }
|
||||
|
||||
[dependencies.common]
|
||||
path = "../common"
|
||||
|
||||
@ -9,6 +9,12 @@ markdown_web_links = [
|
||||
"https://example.org/lmao.md"
|
||||
]
|
||||
|
||||
#weatherapi_key = ""
|
||||
#weatherapi_locations = [
|
||||
# "London",
|
||||
#]
|
||||
|
||||
|
||||
[[bulbs]]
|
||||
id = "light/bedroom"
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
mod markdown_web;
|
||||
mod weatherapi;
|
||||
|
||||
pub use markdown_web::MarkdownWeb;
|
||||
pub use weatherapi::WeatherApi;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
@ -12,4 +14,7 @@ pub trait Collector {
|
||||
#[derive(Deserialize)]
|
||||
pub struct CollectorConfig {
|
||||
pub markdown_web_links: Vec<String>,
|
||||
|
||||
pub weatherapi_key: Option<String>,
|
||||
pub weatherapi_locations: Vec<String>,
|
||||
}
|
||||
|
||||
151
backend/src/collector/weatherapi.rs
Normal file
151
backend/src/collector/weatherapi.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use crate::collector::Collector;
|
||||
use chrono::{serde::ts_seconds, DateTime, Utc};
|
||||
use reqwest::get;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Response {
|
||||
current: CurrentBody,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Condition {
|
||||
text: String,
|
||||
icon: String,
|
||||
code: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct CurrentBody {
|
||||
#[serde(with = "ts_seconds")]
|
||||
last_updated_epoch: DateTime<Utc>,
|
||||
last_updated: String,
|
||||
temp_c: f64,
|
||||
is_day: i8,
|
||||
condition: Condition,
|
||||
wind_kph: f64,
|
||||
wind_degree: f64,
|
||||
wind_dir: String,
|
||||
pressure_mb: f64,
|
||||
precip_mm: f64,
|
||||
humidity: f64,
|
||||
cloud: f64,
|
||||
feelslike_c: f64,
|
||||
vis_km: f64,
|
||||
uv: f64,
|
||||
gust_kph: f64,
|
||||
}
|
||||
|
||||
pub struct WeatherApi {
|
||||
pub api_key: String,
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Collector for WeatherApi {
|
||||
async fn collect(&mut self) -> anyhow::Result<String> {
|
||||
let path = "https://api.weatherapi.com/v1/current.json";
|
||||
let api_key = &self.api_key;
|
||||
let location = &self.location;
|
||||
let url = format!("{path}?key={api_key}&q={location}&aqi=no");
|
||||
let Response {
|
||||
current:
|
||||
CurrentBody {
|
||||
last_updated_epoch,
|
||||
temp_c,
|
||||
feelslike_c,
|
||||
wind_kph,
|
||||
precip_mm,
|
||||
humidity,
|
||||
cloud,
|
||||
..
|
||||
},
|
||||
..
|
||||
} = get(&url).await?.json().await?;
|
||||
|
||||
let wind_mps = (wind_kph / 3.6).round();
|
||||
let wind = wind_speed_to_beaufort(wind_mps);
|
||||
|
||||
let temp = if feelslike_c != temp_c {
|
||||
format!("**{temp_c}°** (känns som {feelslike_c}°)")
|
||||
} else {
|
||||
format!("**{temp_c}°**")
|
||||
};
|
||||
|
||||
let time_of_day = last_updated_epoch.naive_local().format("%H:%M");
|
||||
|
||||
let markdown = format!(
|
||||
r#"
|
||||
# Väder
|
||||
|
||||
## {location} kl {time_of_day}
|
||||
|
||||
{temp}. **{wind}** ({wind_mps} m/s).
|
||||
|
||||
{cloud}% molntäcke, {precip_mm} mm regn, {humidity}% luftfuktighet.
|
||||
|
||||
"#
|
||||
);
|
||||
|
||||
let html = markdown::to_html(markdown.trim());
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wind speed expressed on the beaufort scale
|
||||
struct BeaufortScale(u8);
|
||||
|
||||
impl Display for BeaufortScale {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let name = match self {
|
||||
BeaufortScale(0) => "Lugnt",
|
||||
BeaufortScale(1 | 2) => "Svag vind",
|
||||
BeaufortScale(3 | 4) => "Måttlig vind",
|
||||
BeaufortScale(5 | 6) => "Frisk vind",
|
||||
BeaufortScale(7 | 8) => "Hård vind",
|
||||
BeaufortScale(9) => "Mycket hård vind",
|
||||
BeaufortScale(10) => "Storm",
|
||||
BeaufortScale(11) => "Svår storm",
|
||||
BeaufortScale(12..) => "Orkan",
|
||||
};
|
||||
|
||||
write!(f, "{name}")
|
||||
}
|
||||
}
|
||||
|
||||
fn wind_speed_to_beaufort(mps: f64) -> BeaufortScale {
|
||||
let beaufort_wind_speeds = [
|
||||
0.0..0.3,
|
||||
0.3..1.6,
|
||||
1.6..3.4,
|
||||
3.4..5.5,
|
||||
5.5..8.8,
|
||||
8.0..10.8,
|
||||
10.8..13.9,
|
||||
13.9..17.2,
|
||||
17.2..20.8,
|
||||
20.8..24.5,
|
||||
24.5..28.5,
|
||||
28.5..32.7,
|
||||
32.7..37.0,
|
||||
37.0..41.5,
|
||||
41.5..46.2,
|
||||
46.2..51.0,
|
||||
51.0..56.1,
|
||||
56.1..61.3,
|
||||
];
|
||||
|
||||
let mps = mps.max(0.0);
|
||||
|
||||
let index = beaufort_wind_speeds
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, range)| range.contains(&mps).then(|| i))
|
||||
.unwrap_or(beaufort_wind_speeds.len());
|
||||
|
||||
BeaufortScale(index as u8)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
mod collector;
|
||||
|
||||
use clap::Parser;
|
||||
use collector::{Collector, CollectorConfig, MarkdownWeb};
|
||||
use collector::{Collector, CollectorConfig, MarkdownWeb, WeatherApi};
|
||||
use common::{BulbMap, ClientMessage, ServerMessage};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use lighter_manager::{
|
||||
@ -195,6 +195,22 @@ async fn info_collector(state: &State) {
|
||||
}));
|
||||
}
|
||||
|
||||
if !state.config.collectors.weatherapi_locations.is_empty() {
|
||||
let api_key = state
|
||||
.config
|
||||
.collectors
|
||||
.weatherapi_key
|
||||
.as_deref()
|
||||
.expect("Missing weatherapi_key");
|
||||
|
||||
for location in state.config.collectors.weatherapi_locations.iter().cloned() {
|
||||
collectors.push(Box::new(WeatherApi {
|
||||
api_key: api_key.to_string(),
|
||||
location,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut collectors = collectors.into_boxed_slice();
|
||||
|
||||
let server_message = &state.server_message;
|
||||
@ -266,7 +282,17 @@ async fn client_handler(mut socket: WebSocket, state: &State) {
|
||||
None => return info!("stream closed"),
|
||||
Some(Err(e)) => return warn!("client error: {e}"),
|
||||
Some(Ok(message)) => {
|
||||
let message = ron::from_str(message.to_str().unwrap()).unwrap();
|
||||
let message = match message.to_str().ok() {
|
||||
Some(text) => text,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let message = match ron::from_str(message) {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
return error!("failed to deserialize websocket message: {e}");
|
||||
}
|
||||
};
|
||||
|
||||
let request = ClientRequest {
|
||||
message,
|
||||
@ -277,7 +303,6 @@ async fn client_handler(mut socket: WebSocket, state: &State) {
|
||||
return error!("client message handlers error: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user