Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1672
Cargo.lock
generated
Normal file
1672
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "temperature-numerics"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Anders Olsson <anders.e.olsson@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
enum-map = "1.1"
|
||||||
|
reqwest = "0.11"
|
||||||
|
scraper = "0.12"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
strum = { version = "0.21", features = ["derive"] }
|
||||||
|
thiserror = "1.0"
|
||||||
|
tokio = { version = "1.7", features = ["macros", "rt-multi-thread", "sync", "time", "signal"] }
|
||||||
|
warp = { version = "0.3", default-features = false }
|
||||||
268
src/main.rs
Normal file
268
src/main.rs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use tokio::signal;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let state = models::State::default();
|
||||||
|
let routes = filters::api(state.clone());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let (_, server) =
|
||||||
|
warp::serve(routes).bind_with_graceful_shutdown(([0, 0, 0, 0], 8080), async {
|
||||||
|
rx.await.ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(tasks::update_temperature(state));
|
||||||
|
tokio::spawn(server);
|
||||||
|
|
||||||
|
signal::ctrl_c().await?;
|
||||||
|
|
||||||
|
let _ = tx.send(());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
mod models {
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use enum_map::EnumMap;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::api::Location;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct State(Arc<RwLock<EnumMap<Location, f32>>>);
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Arc::new(RwLock::new(EnumMap::default())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for State {
|
||||||
|
type Target = Arc<RwLock<EnumMap<Location, f32>>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod filters {
|
||||||
|
use warp::Filter;
|
||||||
|
|
||||||
|
use super::handlers;
|
||||||
|
use super::models::State;
|
||||||
|
|
||||||
|
pub fn api(
|
||||||
|
state: State,
|
||||||
|
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||||
|
location(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /location/:location
|
||||||
|
pub fn location(
|
||||||
|
state: State,
|
||||||
|
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
|
||||||
|
warp::path("location")
|
||||||
|
.and(warp::get())
|
||||||
|
.and(with_state(state))
|
||||||
|
.and(warp::path::param())
|
||||||
|
.and_then(handlers::location)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_state(
|
||||||
|
state: State,
|
||||||
|
) -> impl Filter<Extract = (State,), Error = std::convert::Infallible> + Clone {
|
||||||
|
warp::any().map(move || state.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod handlers {
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
use super::api::Location;
|
||||||
|
use super::models::State;
|
||||||
|
use super::numerics::Label;
|
||||||
|
|
||||||
|
pub async fn location(
|
||||||
|
state: State,
|
||||||
|
location: Location,
|
||||||
|
) -> Result<impl warp::Reply, Infallible> {
|
||||||
|
let locations = state.read().await;
|
||||||
|
|
||||||
|
let temperature = locations[location];
|
||||||
|
|
||||||
|
let label = Label::with_value(format!("{}", temperature));
|
||||||
|
|
||||||
|
Ok(warp::reply::json(&label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tasks {
|
||||||
|
use reqwest::Client;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use tokio::time::{interval, Duration};
|
||||||
|
|
||||||
|
use super::api::*;
|
||||||
|
use super::models::State;
|
||||||
|
|
||||||
|
pub async fn update_temperature(state: State) {
|
||||||
|
let client = Client::new();
|
||||||
|
let mut interval = interval(Duration::from_secs(60 * 5));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
// TODO(anders): fetch in parallell
|
||||||
|
for location in Location::iter() {
|
||||||
|
match temperature_at(client.clone(), location).await {
|
||||||
|
Ok(Some(temperature)) => {
|
||||||
|
let mut write = state.write().await;
|
||||||
|
|
||||||
|
write[location] = temperature;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// TODO(anders): add logging for this
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// TODO(anders): backoff for a while
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod api {
|
||||||
|
use enum_map::Enum;
|
||||||
|
use reqwest::Client;
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use strum::{EnumIter, EnumString};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("reqwest error")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Enum, EnumIter, EnumString, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Location {
|
||||||
|
Mullsjon,
|
||||||
|
Vattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Location {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Mullsjon => "21",
|
||||||
|
Self::Vattern => "22",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn temperature_at(client: Client, location: Location) -> Result<Option<f32>, Error> {
|
||||||
|
let response = client
|
||||||
|
.post("https://portal.loggamera.se/PublicViews/OverviewInside")
|
||||||
|
.form(&[("id", location.id())])
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let html = response.text().await?;
|
||||||
|
|
||||||
|
let fragment = Html::parse_fragment(&html);
|
||||||
|
let selector = Selector::parse(".display-value").unwrap();
|
||||||
|
|
||||||
|
Ok(fragment
|
||||||
|
.select(&selector)
|
||||||
|
.next()
|
||||||
|
.map(|element| element.inner_html())
|
||||||
|
.and_then(|value| {
|
||||||
|
value
|
||||||
|
.split_once('°')
|
||||||
|
.and_then(|(raw, _)| raw.parse::<f32>().ok())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod numerics {
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Color {
|
||||||
|
Red,
|
||||||
|
Blue,
|
||||||
|
Green,
|
||||||
|
Purple,
|
||||||
|
Orange,
|
||||||
|
MidnightBlue,
|
||||||
|
Coffee,
|
||||||
|
Burgundy,
|
||||||
|
Wintergreen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct Label {
|
||||||
|
postfix: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
color: Option<Color>,
|
||||||
|
data: Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Label {
|
||||||
|
pub fn with_value(value: impl Into<Value>) -> Self {
|
||||||
|
Self {
|
||||||
|
postfix: String::new(),
|
||||||
|
color: None,
|
||||||
|
data: Data {
|
||||||
|
value: value.into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_postfix(&mut self, postfix: impl Into<String>) -> &mut Self {
|
||||||
|
self.postfix = postfix.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_color(&mut self, color: Color) -> &mut Self {
|
||||||
|
self.color = Some(color);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct Data {
|
||||||
|
value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Value {
|
||||||
|
String(String),
|
||||||
|
Number(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Value {
|
||||||
|
fn from(string: String) -> Self {
|
||||||
|
Self::String(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for Value {
|
||||||
|
fn from(number: i32) -> Self {
|
||||||
|
Self::Number(number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user