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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
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]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "3.2.6"
|
version = "3.2.6"
|
||||||
@ -179,7 +194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"time",
|
"time 0.3.11",
|
||||||
"version_check 0.9.4",
|
"version_check 0.9.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -402,7 +417,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -514,6 +529,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"common",
|
"common",
|
||||||
"futures",
|
"futures",
|
||||||
@ -791,7 +807,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -827,6 +843,25 @@ dependencies = [
|
|||||||
"twoway",
|
"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]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
@ -1399,6 +1434,17 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@ -1726,6 +1772,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ pretty_env_logger = "0.4.0"
|
|||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
warp = "0.3.2"
|
warp = "0.3.2"
|
||||||
ron = "0.7.1"
|
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"
|
async-trait = "0.1.56"
|
||||||
anyhow = "1.0.58"
|
anyhow = "1.0.58"
|
||||||
markdown = "0.3.0"
|
markdown = "0.3.0"
|
||||||
@ -18,6 +18,7 @@ clap = { version = "3.2.6", features = ["derive"] }
|
|||||||
toml = "0.5.9"
|
toml = "0.5.9"
|
||||||
serde = { version = "1.0.138", features = ["derive"] }
|
serde = { version = "1.0.138", features = ["derive"] }
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
|
chrono = { version = "0.4.20", features = ["serde"] }
|
||||||
|
|
||||||
[dependencies.common]
|
[dependencies.common]
|
||||||
path = "../common"
|
path = "../common"
|
||||||
|
|||||||
@ -9,6 +9,12 @@ markdown_web_links = [
|
|||||||
"https://example.org/lmao.md"
|
"https://example.org/lmao.md"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
#weatherapi_key = ""
|
||||||
|
#weatherapi_locations = [
|
||||||
|
# "London",
|
||||||
|
#]
|
||||||
|
|
||||||
|
|
||||||
[[bulbs]]
|
[[bulbs]]
|
||||||
id = "light/bedroom"
|
id = "light/bedroom"
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
mod markdown_web;
|
mod markdown_web;
|
||||||
|
mod weatherapi;
|
||||||
|
|
||||||
pub use markdown_web::MarkdownWeb;
|
pub use markdown_web::MarkdownWeb;
|
||||||
|
pub use weatherapi::WeatherApi;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@ -12,4 +14,7 @@ pub trait Collector {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CollectorConfig {
|
pub struct CollectorConfig {
|
||||||
pub markdown_web_links: Vec<String>,
|
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;
|
mod collector;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use collector::{Collector, CollectorConfig, MarkdownWeb};
|
use collector::{Collector, CollectorConfig, MarkdownWeb, WeatherApi};
|
||||||
use common::{BulbMap, ClientMessage, ServerMessage};
|
use common::{BulbMap, ClientMessage, ServerMessage};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use lighter_manager::{
|
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 mut collectors = collectors.into_boxed_slice();
|
||||||
|
|
||||||
let server_message = &state.server_message;
|
let server_message = &state.server_message;
|
||||||
@ -266,7 +282,17 @@ async fn client_handler(mut socket: WebSocket, state: &State) {
|
|||||||
None => return info!("stream closed"),
|
None => return info!("stream closed"),
|
||||||
Some(Err(e)) => return warn!("client error: {e}"),
|
Some(Err(e)) => return warn!("client error: {e}"),
|
||||||
Some(Ok(message)) => {
|
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 {
|
let request = ClientRequest {
|
||||||
message,
|
message,
|
||||||
@ -277,7 +303,6 @@ async fn client_handler(mut socket: WebSocket, state: &State) {
|
|||||||
return error!("client message handlers error: {e}");
|
return error!("client message handlers error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user