use axum::Json;
use axum::extract::{Path, Query, State};
use ocpp_core::{DateTime, OcppVersion};
use serde::Deserialize;
use serde_json::{Value, json};
use super::stations::load;
use crate::calls;
use crate::error::AppError;
use crate::smart::{self, model::Unit};
use crate::state::App;
#[derive(Debug, Deserialize)]
pub struct SetProfile {
pub evse_id: i64,
pub charging_profile: Value,
}
pub async fn set(
State(app): State<App>,
Path(identity): Path<String>,
Json(body): Json<SetProfile>,
) -> Result<Json<Value>, AppError> {
let version = calls::connected_version(&app, &identity).await?;
let station = load(&app, &identity).await?;
let outcome =
smart::install(&app, &station, version, body.evse_id, body.charging_profile).await?;
Ok(Json(json!(outcome)))
}
#[derive(Debug, Deserialize)]
pub struct ListQuery {
pub status: Option<String>,
}
pub async fn list(
State(app): State<App>,
Path(identity): Path<String>,
Query(q): Query<ListQuery>,
) -> Result<Json<Value>, AppError> {
let station = load(&app, &identity).await?;
Ok(Json(json!(
crate::db::charging::list(&app.db, station.id, q.status.as_deref()).await?
)))
}
pub async fn clear(
State(app): State<App>,
Path(identity): Path<String>,
Json(req): Json<smart::ClearRequest>,
) -> Result<Json<Value>, AppError> {
let version = calls::connected_version(&app, &identity).await?;
let station = load(&app, &identity).await?;
Ok(Json(
json!({ "response": smart::clear(&app, &station, version, &req).await? }),
))
}
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct SyncRequest {
pub evse_id: Option<i64>,
pub purpose: Option<String>,
pub stack_level: Option<i64>,
pub sources: Option<Vec<String>>,
}
pub async fn sync(
State(app): State<App>,
Path(identity): Path<String>,
body: Option<Json<SyncRequest>>,
) -> Result<Json<Value>, AppError> {
let version = calls::connected_version(&app, &identity).await?;
if version == OcppVersion::V16 {
return Err(AppError::BadRequest(
"OCPP 1.6 has no GetChargingProfiles message".into(),
));
}
let r = body.map(|b| b.0).unwrap_or_default();
let request_id = (DateTime::now().unix_micros() / 1000 % i32::MAX as i64) as i32;
let mut criterion = json!({ "chargingProfilePurpose": r.purpose, "stackLevel": r.stack_level, "chargingLimitSource": r.sources });
if criterion
.as_object()
.is_some_and(|o| o.values().all(Value::is_null))
{
criterion = json!({ "chargingLimitSource": ["EMS", "Other", "SO", "CSO"] });
}
let payload = smart::strip_nulls(
json!({ "requestId": request_id, "evseId": r.evse_id, "chargingProfile": criterion }),
);
let response = calls::call(&app, &identity, version, "GetChargingProfiles", payload).await?;
Ok(Json(
json!({ "request_id": request_id, "response": response }),
))
}
#[derive(Debug, Deserialize)]
pub struct CompositeQuery {
pub evse_id: i64,
pub duration: Option<i64>,
pub unit: Option<String>,
pub source: Option<String>,
}
pub async fn composite(
State(app): State<App>,
Path(identity): Path<String>,
Query(q): Query<CompositeQuery>,
) -> Result<Json<Value>, AppError> {
let duration = q.duration.unwrap_or(86_400).clamp(1, 7 * 86_400);
let unit = match q.unit.as_deref().unwrap_or("W") {
"A" => Unit::A,
"W" => Unit::W,
other => {
return Err(AppError::BadRequest(format!(
"unit must be A or W, not {other}"
)));
}
};
let unit_str = if unit == Unit::A { "A" } else { "W" };
if q.source.as_deref() == Some("station") {
let version = calls::connected_version(&app, &identity).await?;
let payload = match version {
OcppVersion::V16 => {
json!({ "connectorId": q.evse_id, "duration": duration, "chargingRateUnit": unit_str })
}
_ => json!({ "evseId": q.evse_id, "duration": duration, "chargingRateUnit": unit_str }),
};
return Ok(Json(
json!({ "source": "station", "response": calls::call(&app, &identity, version, "GetCompositeSchedule", payload).await? }),
));
}
let station = load(&app, &identity).await?;
let start = DateTime::now();
let periods = smart::composite_for(&app, &station, q.evse_id, start, duration, unit).await?;
Ok(Json(json!({
"source": "csms",
"evse_id": q.evse_id,
"schedule_start": start,
"duration": duration,
"charging_rate_unit": unit_str,
"charging_schedule_period": periods,
})))
}
#[derive(Debug, Deserialize)]
pub struct DynamicUpdate {
pub schedule_update: Value,
#[serde(default = "yes")]
pub push: bool,
}
fn yes() -> bool {
true
}
pub async fn dynamic_update(
State(app): State<App>,
Path((identity, profile_id)): Path<(String, i64)>,
Json(body): Json<DynamicUpdate>,
) -> Result<Json<Value>, AppError> {
let station = load(&app, &identity).await?;
let version = if body.push {
calls::connected_version(&app, &identity).await?
} else {
station
.ocpp_version
.as_deref()
.unwrap_or("2.1")
.parse()
.map_err(AppError::BadRequest)?
};
Ok(Json(
smart::dynamic_update(
&app,
&station,
version,
profile_id,
body.schedule_update,
body.push,
)
.await?,
))
}
pub async fn limits(
State(app): State<App>,
Path(identity): Path<String>,
) -> Result<Json<Value>, AppError> {
let station = load(&app, &identity).await?;
Ok(Json(json!(
crate::db::charging::external_limits(&app.db, station.id).await?
)))
}