Add weatherapi collector

This commit is contained in:
2022-08-23 19:48:36 +02:00
parent e7baf561bd
commit d2054caf69
6 changed files with 247 additions and 7 deletions

58
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -9,6 +9,12 @@ markdown_web_links = [
"https://example.org/lmao.md"
]
#weatherapi_key = ""
#weatherapi_locations = [
# "London",
#]
[[bulbs]]
id = "light/bedroom"

View File

@ -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>,
}

View 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)
}

View File

@ -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}");
}
}
}
}
}