Initial commit

This commit is contained in:
2021-06-28 15:04:00 +02:00
commit b16268420b
4 changed files with 1957 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1672
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View 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
View 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)
}
}
}