tesla-charge-controller/charge-controller-supervisor/src/web.rs

207 lines
5.3 KiB
Rust
Raw Normal View History

use rocket::{get, post, routes, serde::json::Json, State};
2024-12-28 18:54:21 +11:00
2024-12-30 20:30:45 +11:00
mod static_handler;
2024-12-28 20:39:07 +11:00
pub struct ServerState {
primary_name: String,
2024-12-28 20:39:07 +11:00
map: std::collections::HashMap<
String,
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
>,
tx_to_controllers: crate::controller::MultiTx,
2024-12-28 20:39:07 +11:00
}
impl ServerState {
pub fn new(
primary_name: &impl ToString,
2024-12-28 20:39:07 +11:00
map: std::collections::HashMap<
String,
std::sync::Arc<tokio::sync::RwLock<crate::controller::CommonData>>,
>,
tx_to_controllers: crate::controller::MultiTx,
2024-12-28 20:39:07 +11:00
) -> Self {
let primary_name = primary_name.to_string();
Self {
primary_name,
map,
tx_to_controllers,
}
2024-12-28 20:39:07 +11:00
}
}
pub fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
2024-12-30 20:30:45 +11:00
// serve the html from disk if running in a debug build
// this allows editing the webpage without having to rebuild the executable
// but in release builds, bundle the entire webapp folder into the exe
let fileserver: Vec<rocket::Route> = if cfg!(debug_assertions) {
rocket::fs::FileServer::from(format!(
"{}/webapp",
std::env::var("CARGO_MANIFEST_DIR").unwrap()
))
.into()
} else {
static_handler::UiStatic {}.into()
};
rocket::build()
.attach(Cors)
.manage(state)
.mount("/", fileserver)
.mount(
"/",
routes![
metrics,
interfaces,
all_interfaces,
primary_interface,
interface,
get_control,
enable_control,
disable_control
],
)
2024-12-28 20:39:07 +11:00
}
#[get("/interfaces")]
fn interfaces(state: &State<ServerState>) -> Json<Vec<String>> {
Json(state.map.keys().cloned().collect())
}
#[get("/interfaces/primary")]
async fn primary_interface(
state: &State<ServerState>,
) -> Result<Json<crate::controller::CommonData>, ServerError> {
let s = state
.map
.get(&state.primary_name)
.ok_or(ServerError::InvalidPrimaryName)?
.read()
.await
.clone();
Ok(Json(s))
}
2024-12-28 20:39:07 +11:00
#[get("/interfaces/data")]
async fn all_interfaces(
state: &State<ServerState>,
) -> Json<Vec<(String, crate::controller::CommonData)>> {
let mut data = Vec::new();
for (k, v) in &state.map {
data.push((k.clone(), v.read().await.clone()));
}
Json(data)
}
#[get("/interface/<name>")]
async fn interface(
name: &str,
state: &State<ServerState>,
) -> Result<Json<crate::controller::CommonData>, ServerError> {
let data = state
.map
.get(name)
.ok_or(ServerError::NotFound)?
.read()
.await
.clone();
Ok(Json(data))
2024-12-28 18:54:21 +11:00
}
#[get("/metrics")]
fn metrics() -> Result<String, ServerError> {
Ok(
prometheus::TextEncoder::new()
.encode_to_string(&prometheus::default_registry().gather())?,
)
}
2024-12-30 20:30:45 +11:00
#[derive(serde::Serialize)]
struct ControlState {
enabled: bool,
}
#[get("/control")]
async fn get_control() -> Json<ControlState> {
let enabled = crate::config::access_config()
.await
.enable_secondary_control;
Json(ControlState { enabled })
}
#[post("/control/enable")]
async fn enable_control() {
log::warn!("enabling control");
crate::config::write_to_config()
.await
.enable_secondary_control = true;
}
2024-12-30 20:30:45 +11:00
#[post("/control/disable")]
async fn disable_control(state: &State<ServerState>) {
log::warn!("disabling control");
crate::config::write_to_config()
.await
.enable_secondary_control = false;
state
.tx_to_controllers
.send_to_all(crate::controller::VoltageCommand::Set(-1.0));
}
2024-12-28 18:54:21 +11:00
enum ServerError {
Prometheus,
2024-12-28 20:39:07 +11:00
NotFound,
InvalidPrimaryName,
2024-12-28 18:54:21 +11:00
}
impl From<prometheus::Error> for ServerError {
fn from(_: prometheus::Error) -> Self {
Self::Prometheus
}
}
impl<'a> rocket::response::Responder<'a, 'a> for ServerError {
fn respond_to(self, _: &'a rocket::Request<'_>) -> rocket::response::Result<'a> {
Err(match self {
Self::Prometheus => rocket::http::Status::InternalServerError,
2024-12-28 20:39:07 +11:00
Self::NotFound => rocket::http::Status::NotFound,
Self::InvalidPrimaryName => rocket::http::Status::ServiceUnavailable,
2024-12-28 18:54:21 +11:00
})
}
}
2024-12-30 20:30:45 +11:00
pub struct Cors;
#[rocket::async_trait]
impl rocket::fairing::Fairing for Cors {
fn info(&self) -> rocket::fairing::Info {
rocket::fairing::Info {
name: "Add CORS headers to responses",
kind: rocket::fairing::Kind::Response,
}
}
async fn on_response<'r>(
&self,
_request: &'r rocket::Request<'_>,
response: &mut rocket::Response<'r>,
) {
response.set_header(rocket::http::Header::new(
"Access-Control-Allow-Origin",
"*",
));
response.set_header(rocket::http::Header::new(
"Access-Control-Allow-Methods",
"POST, GET, PATCH, OPTIONS",
));
response.set_header(rocket::http::Header::new(
"Access-Control-Allow-Headers",
"*",
));
response.set_header(rocket::http::Header::new(
"Access-Control-Allow-Credentials",
"true",
));
}
}