diff --git a/Cargo.lock b/Cargo.lock index dfae9d2..e6e8005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,6 +954,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -965,6 +966,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", @@ -1225,7 +1227,6 @@ dependencies = [ "tokio", "tower", "tower-http", - "tower-livereload", "tracing", "tracing-subscriber", ] @@ -1440,20 +1441,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" -[[package]] -name = "tower-livereload" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa6b29b17d4540f2bd9ec304ad39d280c4bdf291d0ea6c4123eeba10939af84" -dependencies = [ - "bytes", - "http", - "http-body", - "pin-project-lite", - "tokio", - "tower", -] - [[package]] name = "tower-service" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index ab015af..7f692cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" axum = "0.8.6" chrono = "0.4.42" dotenv = "0.15.0" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.12", features = ["json", "multipart"] } serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.47", features = ["full"] } tower-http = { version = "0.6", features = ["fs"] } @@ -15,4 +15,4 @@ tower = "0.5.0" tracing = "0.1.41" tracing-subscriber = "0.3.20" anyhow = "1.0" -tower-livereload = "0.9.6" +#tower-livereload = "0.9.6" diff --git a/src/main.rs b/src/main.rs index 22fabdf..bee86cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,51 @@ +use std::sync::{Arc, RwLock}; + use axum::{ - Extension, Json, Router, + Json, Router, + extract::State, response::{ErrorResponse, Html}, routing::get, }; -use chrono::prelude::*; +use chrono::{DateTime, Days, Timelike, Utc}; use dotenv::dotenv; use serde::{Deserialize, Serialize}; use tower::ServiceBuilder; use tower_http::services::ServeDir; -use tower_livereload::LiveReloadLayer; +// use tower_livereload::LiveReloadLayer; + +type CachedAppState = Arc>; + +#[derive(Debug, Clone)] +struct AppState { + day_checked: bool, + cache_reset: DateTime, + values: EnergyResponse, +} #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv().ok(); + let app_state: CachedAppState = Arc::new(RwLock::new(AppState { + values: EnergyResponse { + energy: Energy { + timeUnit: "".to_string(), + unit: "".to_string(), + values: vec![], + }, + }, + day_checked: false, + cache_reset: Utc::now() - Days::new(1), + })); let app = Router::new() .route("/api/energy", get(energy)) + .with_state(app_state) .route("/", get(index)) .nest_service( "/static", ServiceBuilder::new().service(ServeDir::new("static")), - ) - .layer(LiveReloadLayer::new()); + ); + // .layer(LiveReloadLayer::new()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("server on {}", listener.local_addr().unwrap()); @@ -43,50 +67,117 @@ async fn index() -> Html { Html(html) } -async fn energy() -> axum::response::Result, ErrorResponse> { - let site_id = std::env::var("SITE_ID").unwrap(); - let api_key = std::env::var("API_KEY").unwrap(); - let utc_now = Utc::now().date_naive(); +async fn energy( + State(state): State, +) -> axum::response::Result, ErrorResponse> { + let energy_response = fetch_energy_response(state.clone()).await?; + check_energy(state, &energy_response).await?; - let url = format!( - "https://monitoringapi.solaredge.com/site/{}/energy?timeUnit=QUARTER_OF_AN_HOUR&endDate={}&startDate={}&api_key={}", - site_id, utc_now, utc_now, api_key, - ); - - let mut energy_response = reqwest::get(url) - .await - .map_err(|e| ErrorResponse::from(e.to_string()))? - .json::() - .await - .map_err(|e| ErrorResponse::from(e.to_string()))?; - - let values: Vec = energy_response - .energy - .values - .iter() - .map(|v| EnergyValue { - date: format!("{}+02:00", v.date.replace(' ', "T")).to_string(), - value: v.value, - }) - .collect(); - - energy_response.energy.values = values; Ok(Json(energy_response)) } -#[derive(Debug, Serialize, Deserialize)] +async fn check_energy( + state: CachedAppState, + energy_response: &EnergyResponse, +) -> axum::response::Result<(), ErrorResponse> { + let now = Utc::now(); + let hour = now.hour(); + let is_checked_today = state.read().unwrap().day_checked; + if hour == 12 && !is_checked_today { + let energy_at_1200 = energy_response + .energy + .values + .iter() + .find(|v| v.date.ends_with("12:00:00+02:00")) + .map(|v| v.value) + .flatten(); + if let Some(energy_at_1200) = energy_at_1200 { + if energy_at_1200 == 0.0 { + report().await?; + } + } + state.write().unwrap().day_checked = true; + } + //reset at 00:00 + if hour == 0 && is_checked_today { + state.write().unwrap().day_checked = false; + } + Ok(()) +} +async fn report() -> axum::response::Result<(), ErrorResponse> { + let user_id = std::env::var("PUSHOVER_USER_ID").unwrap(); + let api_key = std::env::var("PUSHOVER_API_KEY").unwrap(); + + let url = "https://api.pushover.net/1/messages.json"; + let form = reqwest::multipart::Form::new() + .text("token", api_key) + .text("user", user_id) + .text("message", "No energy measured on the solar panels"); + + let client = reqwest::Client::new(); + let _ = client + .post(url) + .multipart(form) + .send() + .await + .map_err(|e| ErrorResponse::from(e.to_string()))?; + Ok(()) +} + +async fn fetch_energy_response(state: CachedAppState) -> axum::response::Result { + let reset_ts = state.read().unwrap().cache_reset; + let now = Utc::now(); + if now.signed_duration_since(reset_ts).as_seconds_f32() > 300.0 { + state.write().unwrap().cache_reset = now; + let site_id = std::env::var("SOLAREDGE_SITE_ID").unwrap(); + let api_key = std::env::var("SOLAREDGE_API_KEY").unwrap(); + + let url = format!( + "https://monitoringapi.solaredge.com/site/{}/energy?timeUnit=QUARTER_OF_AN_HOUR&endDate={}&startDate={}&api_key={}", + site_id, + now.date_naive(), + now.date_naive(), + api_key, + ); + + let mut energy_response = reqwest::get(url) + .await + .map_err(|e| ErrorResponse::from(e.to_string()))? + .json::() + .await + .map_err(|e| ErrorResponse::from(e.to_string()))?; + + let values: Vec = energy_response + .energy + .values + .iter() + .map(|v| EnergyValue { + date: format!("{}+02:00", v.date.replace(' ', "T")).to_string(), + value: v.value, + }) + .collect(); + + energy_response.energy.values = values; + state.write().unwrap().values = energy_response.clone(); + Ok(energy_response) + } else { + Ok(state.read().unwrap().values.clone()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] struct EnergyResponse { energy: Energy, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct Energy { timeUnit: String, unit: String, values: Vec, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct EnergyValue { date: String, value: Option, diff --git a/static/index.html b/static/index.html index aea7ee4..b9037b6 100644 --- a/static/index.html +++ b/static/index.html @@ -2,6 +2,7 @@ +