use axum::Json;
use axum::extract::{Path, State};
use ocpp_core::OcppVersion;
use serde::Deserialize;
use serde_json::{Value, json};
use crate::error::AppError;
#[cfg(test)]
use crate::ocpp::{v16, v21, v201};
use crate::state::App;
async fn execute(
app: &App,
identity: &str,
version: OcppVersion,
action: &str,
payload: Value,
) -> Result<Json<Value>, AppError> {
let response = crate::calls::call(app, identity, version, action, payload).await?;
Ok(Json(
json!({ "ocpp_version": version.to_string(), "action": action, "response": response }),
))
}
pub async fn raw(
State(app): State<App>,
Path((identity, action)): Path<(String, String)>,
Json(payload): Json<Value>,
) -> Result<Json<Value>, AppError> {
let version = crate::calls::connected_version(&app, &identity).await?;
execute(&app, &identity, version, &action, payload).await
}
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct GenericCommand {
pub r#type: Option<String>,
pub evse_id: Option<i64>,
pub connector_id: Option<i64>,
pub id_token: Option<String>,
pub id_token_type: Option<String>,
pub remote_start_id: Option<i64>,
pub transaction_id: Option<String>,
pub operative: Option<bool>,
pub message: Option<String>,
pub charging_profile: Option<Value>,
}
fn required<T>(v: Option<T>, name: &str) -> Result<T, AppError> {
v.ok_or_else(|| AppError::BadRequest(format!("'{name}' is required")))
}
fn translate(
command: &str,
version: OcppVersion,
c: GenericCommand,
) -> Result<(&'static str, Value), AppError> {
let v16 = version == OcppVersion::V16;
Ok(match command {
"reset" => {
let hard = match c.r#type.as_deref().unwrap_or("Soft") {
"Hard" | "Immediate" => true,
"Soft" | "OnIdle" => false,
other => {
return Err(AppError::BadRequest(format!(
"unknown reset type '{other}'"
)));
}
};
if v16 {
(
"Reset",
json!({ "type": if hard { "Hard" } else { "Soft" } }),
)
} else {
let mut p = json!({ "type": if hard { "Immediate" } else { "OnIdle" } });
if let Some(e) = c.evse_id {
p["evseId"] = json!(e);
}
("Reset", p)
}
}
"remote-start" => {
let token = required(c.id_token, "id_token")?;
if let Some(cp) = &c.charging_profile
&& (cp.get("chargingProfilePurpose").and_then(Value::as_str) != Some("TxProfile")
|| cp.get("transactionId").is_some())
{
return Err(AppError::BadRequest(
"charging_profile of a remote start must be a TxProfile without transactionId"
.into(),
));
}
if v16 {
let mut p = json!({ "idTag": token });
if let Some(e) = c.evse_id {
p["connectorId"] = json!(e);
}
if let Some(cp) = c.charging_profile {
p["chargingProfile"] = cp;
}
("RemoteStartTransaction", p)
} else {
let remote_start_id = c.remote_start_id.unwrap_or_else(|| {
(ocpp_core::DateTime::now().unix_micros() % i32::MAX as i64).abs()
});
let mut p = json!({
"idToken": { "idToken": token, "type": c.id_token_type.unwrap_or_else(|| "Central".into()) },
"remoteStartId": remote_start_id,
});
if let Some(e) = c.evse_id {
p["evseId"] = json!(e);
}
if let Some(cp) = c.charging_profile {
p["chargingProfile"] = cp;
}
("RequestStartTransaction", p)
}
}
"remote-stop" => {
let tx = required(c.transaction_id, "transaction_id")?;
if v16 {
let id: i64 = tx.parse().map_err(|_| {
AppError::BadRequest("OCPP 1.6 transaction_id must be an integer".into())
})?;
("RemoteStopTransaction", json!({ "transactionId": id }))
} else {
("RequestStopTransaction", json!({ "transactionId": tx }))
}
}
"unlock-connector" => {
let evse = required(c.evse_id, "evse_id")?;
if v16 {
("UnlockConnector", json!({ "connectorId": evse }))
} else {
(
"UnlockConnector",
json!({ "evseId": evse, "connectorId": c.connector_id.unwrap_or(1) }),
)
}
}
"change-availability" => {
let operative = required(c.operative, "operative")?;
let status = if operative {
"Operative"
} else {
"Inoperative"
};
if v16 {
(
"ChangeAvailability",
json!({ "connectorId": c.evse_id.unwrap_or(0), "type": status }),
)
} else {
let mut p = json!({ "operationalStatus": status });
if let Some(e) = c.evse_id {
p["evse"] = json!({ "id": e });
if let Some(conn) = c.connector_id {
p["evse"]["connectorId"] = json!(conn);
}
}
("ChangeAvailability", p)
}
}
"trigger-message" => {
let msg = required(c.message, "message")?;
let mut p = json!({ "requestedMessage": msg });
if let Some(e) = c.evse_id {
if v16 {
p["connectorId"] = json!(e);
} else {
p["evse"] = json!({ "id": e });
}
}
("TriggerMessage", p)
}
other => return Err(AppError::NotFound(format!("unknown command '{other}'"))),
})
}
pub async fn generic(
State(app): State<App>,
Path((identity, command)): Path<(String, String)>,
body: Option<Json<GenericCommand>>,
) -> Result<Json<Value>, AppError> {
let version = crate::calls::connected_version(&app, &identity).await?;
let (action, payload) = translate(&command, version, body.map(|b| b.0).unwrap_or_default())?;
execute(&app, &identity, version, action, payload).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translations_validate_for_each_version() {
let cases: Vec<(&str, GenericCommand)> = vec![
(
"reset",
GenericCommand {
r#type: Some("Hard".into()),
..Default::default()
},
),
(
"remote-start",
GenericCommand {
id_token: Some("TAG1".into()),
evse_id: Some(1),
..Default::default()
},
),
(
"remote-stop",
GenericCommand {
transaction_id: Some("42".into()),
..Default::default()
},
),
(
"unlock-connector",
GenericCommand {
evse_id: Some(1),
..Default::default()
},
),
(
"change-availability",
GenericCommand {
operative: Some(false),
evse_id: Some(2),
..Default::default()
},
),
(
"trigger-message",
GenericCommand {
message: Some("StatusNotification".into()),
evse_id: Some(1),
..Default::default()
},
),
];
for version in OcppVersion::ALL {
for (cmd, c) in &cases {
let c = GenericCommand { ..serde_json::from_value(serde_json::to_value(json!({
"type": c.r#type, "evse_id": c.evse_id, "id_token": c.id_token, "transaction_id": c.transaction_id,
"operative": c.operative, "message": c.message,
})).unwrap()).unwrap() };
let (action, payload) = translate(cmd, version, c).unwrap();
let ok = match version {
OcppVersion::V16 => v16::validate_command(action, payload.clone()),
OcppVersion::V201 => v201::validate_command(action, payload.clone()),
OcppVersion::V21 => v21::validate_command(action, payload.clone()),
};
assert!(ok.is_ok(), "{cmd} {version}: {payload} -> {ok:?}");
}
}
}
}