#![doc = include_str!("../README.md")] #![warn( clippy::all, missing_copy_implementations, missing_debug_implementations, rust_2018_idioms, rustdoc::broken_intra_doc_links, trivial_numeric_casts, renamed_and_removed_lints, unsafe_code, unstable_features, unused_import_braces, unused_qualifications )] #![deny(missing_docs)] mod services; mod update; use std::sync::Mutex; use once_cell::sync::Lazy; use rocket::{ catch, catchers, fairing::AdHoc, get, routes, serde::{json::Json, Deserialize, Serialize}, Build, Request, Rocket, }; use self::update::update_loop; /// The global, concurrently accessible current status. static STATUS: Lazy>> = Lazy::new(|| Mutex::new(None)); /// The configuration loaded additionally by Rocket. #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] struct Config { /// The service-specific configuration service: services::Config, } /// The current photovoltaic invertor status. #[derive(Clone, Copy, Debug, Serialize)] #[serde(crate = "rocket::serde")] struct Status { /// The current power production (W). current_w: f32, /// The total energy produced since installation (kWh). total_kwh: f32, /// The (UNIX) timestamp of when the status was last updated. last_updated: u64, } /// An error used as JSON response. #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] struct Error { /// The error message. error: String, } impl Error { /// Creates a new error result from a message. fn from(message: impl AsRef) -> Self { let error = String::from(message.as_ref()); Self { error } } } /// Returns the current (last known) status. #[get("/", format = "application/json")] async fn status() -> Result, Json> { let status_guard = STATUS.lock().expect("Status mutex was poisoined"); status_guard .map(Json) .ok_or_else(|| Json(Error::from("No status found (yet)"))) } /// Default catcher for any unsuppored request #[catch(default)] fn unsupported(status: rocket::http::Status, _request: &Request<'_>) -> Json { let code = status.code; Json(Error::from(format!( "Unhandled/unsupported API call or path (HTTP {code})" ))) } /// Creates a Rocket and attaches the config parsing and update loop as fairings. pub fn setup() -> Rocket { rocket::build() .mount("/", routes![status]) .register("/", catchers![unsupported]) .attach(AdHoc::config::()) .attach(AdHoc::on_liftoff("Updater", |rocket| { Box::pin(async move { let config = rocket .figment() .extract::() .expect("Invalid configuration"); let service = services::get(config.service).expect("Invalid service"); // We don't care about the join handle nor error results?t let _ = rocket::tokio::spawn(update_loop(service)); }) })) }