added caching, reporting to pushover

This commit is contained in:
Shautvast 2025-10-08 13:03:13 +02:00
parent 7d298eb48b
commit 8316bc297a
4 changed files with 131 additions and 52 deletions

17
Cargo.lock generated
View file

@ -954,6 +954,7 @@ dependencies = [
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util",
"h2", "h2",
"http", "http",
"http-body", "http-body",
@ -965,6 +966,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"native-tls", "native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@ -1225,7 +1227,6 @@ dependencies = [
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
"tower-livereload",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -1440,20 +1441,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"

View file

@ -7,7 +7,7 @@ edition = "2024"
axum = "0.8.6" axum = "0.8.6"
chrono = "0.4.42" chrono = "0.4.42"
dotenv = "0.15.0" dotenv = "0.15.0"
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json", "multipart"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.47", features = ["full"] } tokio = { version = "1.47", features = ["full"] }
tower-http = { version = "0.6", features = ["fs"] } tower-http = { version = "0.6", features = ["fs"] }
@ -15,4 +15,4 @@ tower = "0.5.0"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"
anyhow = "1.0" anyhow = "1.0"
tower-livereload = "0.9.6" #tower-livereload = "0.9.6"

View file

@ -1,27 +1,51 @@
use std::sync::{Arc, RwLock};
use axum::{ use axum::{
Extension, Json, Router, Json, Router,
extract::State,
response::{ErrorResponse, Html}, response::{ErrorResponse, Html},
routing::get, routing::get,
}; };
use chrono::prelude::*; use chrono::{DateTime, Days, Timelike, Utc};
use dotenv::dotenv; use dotenv::dotenv;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_livereload::LiveReloadLayer; // use tower_livereload::LiveReloadLayer;
type CachedAppState = Arc<RwLock<AppState>>;
#[derive(Debug, Clone)]
struct AppState {
day_checked: bool,
cache_reset: DateTime<Utc>,
values: EnergyResponse,
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
dotenv().ok(); 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() let app = Router::new()
.route("/api/energy", get(energy)) .route("/api/energy", get(energy))
.with_state(app_state)
.route("/", get(index)) .route("/", get(index))
.nest_service( .nest_service(
"/static", "/static",
ServiceBuilder::new().service(ServeDir::new("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(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("server on {}", listener.local_addr().unwrap()); println!("server on {}", listener.local_addr().unwrap());
@ -43,50 +67,117 @@ async fn index() -> Html<String> {
Html(html) Html(html)
} }
async fn energy() -> axum::response::Result<Json<EnergyResponse>, ErrorResponse> { async fn energy(
let site_id = std::env::var("SITE_ID").unwrap(); State(state): State<CachedAppState>,
let api_key = std::env::var("API_KEY").unwrap(); ) -> axum::response::Result<Json<EnergyResponse>, ErrorResponse> {
let utc_now = Utc::now().date_naive(); 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::<EnergyResponse>()
.await
.map_err(|e| ErrorResponse::from(e.to_string()))?;
let values: Vec<EnergyValue> = 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)) 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<EnergyResponse> {
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::<EnergyResponse>()
.await
.map_err(|e| ErrorResponse::from(e.to_string()))?;
let values: Vec<EnergyValue> = 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 { struct EnergyResponse {
energy: Energy, energy: Energy,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct Energy { struct Energy {
timeUnit: String, timeUnit: String,
unit: String, unit: String,
values: Vec<EnergyValue>, values: Vec<EnergyValue>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct EnergyValue { struct EnergyValue {
date: String, date: String,
value: Option<f32>, value: Option<f32>,

View file

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<meta http-equiv="refresh" content="300" />
</head> </head>
<body> <body>
<script src="/static/d3.v7.min.js"></script> <script src="/static/d3.v7.min.js"></script>