use std::collections::BTreeMap;
use anyhow::{Result, anyhow};
use chrono::{SecondsFormat, Utc};
use serde_json::Value;
use crate::ocpp::{ConfigurationKey, OcppVersion, ResponseStatus, StopReason};
use super::{ConfigurationEntry, SimulatorConfig};
pub(in crate::simulator) fn default_configuration_entries(
config: &SimulatorConfig,
) -> BTreeMap<ConfigurationKey, ConfigurationEntry> {
let mut entries = core_configuration_entries(config);
entries.extend(security_configuration_entries(config));
entries
}
fn core_configuration_entries(
config: &SimulatorConfig,
) -> BTreeMap<ConfigurationKey, ConfigurationEntry> {
let heartbeat_interval = config.heartbeat_seconds.unwrap_or(30).to_string();
BTreeMap::from([
(
ConfigurationKey::AllowOfflineTxForUnknownId,
ConfigurationEntry {
value: "false".to_string(),
read_only: false,
},
),
(
ConfigurationKey::AuthorizeRemoteTxRequests,
ConfigurationEntry {
value: "true".to_string(),
read_only: false,
},
),
(
ConfigurationKey::HeartbeatInterval,
ConfigurationEntry {
value: heartbeat_interval,
read_only: false,
},
),
(
ConfigurationKey::MeterValueSampleInterval,
ConfigurationEntry {
value: "60".to_string(),
read_only: false,
},
),
(
ConfigurationKey::NumberOfConnectors,
ConfigurationEntry {
value: config.connectors.to_string(),
read_only: true,
},
),
(
ConfigurationKey::SupportedFeatureProfiles,
ConfigurationEntry {
value: "Core,FirmwareManagement,LocalAuthListManagement,\
SmartCharging,RemoteTrigger,Reservation,Security"
.to_string(),
read_only: true,
},
),
(
ConfigurationKey::WebSocketPingInterval,
ConfigurationEntry {
value: "0".to_string(),
read_only: false,
},
),
])
}
fn security_configuration_entries(
config: &SimulatorConfig,
) -> BTreeMap<ConfigurationKey, ConfigurationEntry> {
BTreeMap::from([
(
ConfigurationKey::AdditionalRootCertificateCheck,
ConfigurationEntry {
value: "false".to_string(),
read_only: config.protocol == OcppVersion::V1_6,
},
),
(
ConfigurationKey::AllowSecurityProfileDowngrade,
ConfigurationEntry {
value: "false".to_string(),
read_only: false,
},
),
(
ConfigurationKey::AuthorizationKey,
ConfigurationEntry {
value: String::new(),
read_only: false,
},
),
(
ConfigurationKey::BasicAuthPassword,
ConfigurationEntry {
value: String::new(),
read_only: false,
},
),
(
ConfigurationKey::CertificateSignedMaxChainSize,
ConfigurationEntry {
value: "10000".to_string(),
read_only: config.protocol == OcppVersion::V1_6,
},
),
(
ConfigurationKey::CertificateStoreMaxLength,
ConfigurationEntry {
value: "10".to_string(),
read_only: true,
},
),
(
ConfigurationKey::CertSigningRepeatTimes,
ConfigurationEntry {
value: "3".to_string(),
read_only: false,
},
),
(
ConfigurationKey::CertSigningWaitMinimum,
ConfigurationEntry {
value: "60".to_string(),
read_only: false,
},
),
(
ConfigurationKey::CpoName,
ConfigurationEntry {
value: config.vendor.clone(),
read_only: false,
},
),
(
ConfigurationKey::MaxCertificateChainSize,
ConfigurationEntry {
value: "10000".to_string(),
read_only: false,
},
),
(
ConfigurationKey::OrganizationName,
ConfigurationEntry {
value: config.vendor.clone(),
read_only: false,
},
),
(
ConfigurationKey::SecurityProfile,
ConfigurationEntry {
value: config.security_profile.unwrap_or(0).to_string(),
read_only: false,
},
),
(
ConfigurationKey::SupportedFileTransferProtocols,
ConfigurationEntry {
value: "HTTP,HTTPS".to_string(),
read_only: false,
},
),
])
}
pub(in crate::simulator) fn now_timestamp() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)
}
pub(in crate::simulator) fn authorize_status(
protocol: OcppVersion,
payload: &Value,
) -> String {
match protocol {
OcppVersion::V1_6 => payload
.get("idTagInfo")
.and_then(Value::as_object)
.and_then(|object| object.get("status"))
.and_then(Value::as_str)
.unwrap_or(ResponseStatus::Unknown.as_str())
.to_string(),
OcppVersion::V2_0_1 | OcppVersion::V2_1 => payload
.get("idTokenInfo")
.and_then(Value::as_object)
.and_then(|object| object.get("status"))
.and_then(Value::as_str)
.unwrap_or(ResponseStatus::Unknown.as_str())
.to_string(),
}
}
pub(in crate::simulator) fn map_stop_reason_v1_6(
reason: Option<&str>,
remote_stop: bool,
) -> StopReason {
if remote_stop {
return StopReason::Remote;
}
reason
.and_then(StopReason::parse_user_input)
.filter(|item| item.as_v1_6().is_some())
.unwrap_or(StopReason::Local)
}
pub(in crate::simulator) fn map_stop_reason_v2_x(
protocol: OcppVersion,
reason: Option<&str>,
remote_stop: bool,
) -> StopReason {
if remote_stop {
return StopReason::Remote;
}
reason
.and_then(StopReason::parse_user_input)
.filter(|item| item.as_v2_x(protocol).is_some())
.unwrap_or(StopReason::Local)
}
pub(in crate::simulator) fn validate_negotiated_subprotocol<'a>(
expected: &str,
negotiated: Option<&'a str>,
) -> Result<&'a str> {
let Some(actual) = negotiated else {
return Err(anyhow!(
"CSMS did not negotiate required WebSocket subprotocol `{expected}`."
));
};
if actual != expected {
return Err(anyhow!(
"CSMS negotiated WebSocket subprotocol `{actual}` \
but `{expected}` was requested."
));
}
Ok(actual)
}
pub(in crate::simulator) fn required_string_field<'a>(
payload: &'a Value,
field: &str,
) -> Result<&'a str> {
payload
.get(field)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow!("{field} is required."))
}
pub(in crate::simulator) fn required_i64_field(
payload: &Value,
field: &str,
) -> Result<i64> {
payload
.get(field)
.and_then(Value::as_i64)
.ok_or_else(|| anyhow!("{field} is required."))
}
pub(in crate::simulator) fn required_u16_field(
payload: &Value,
field: &str,
) -> Result<u16> {
optional_u16_field(payload, field)?.ok_or_else(|| {
anyhow!("{field} is required and must be an unsigned integer.")
})
}
pub(in crate::simulator) fn required_u64_field(
payload: &Value,
field: &str,
) -> Result<u64> {
payload
.get(field)
.and_then(Value::as_u64)
.ok_or_else(|| anyhow!("{field} is required and must be unsigned."))
}
pub(in crate::simulator) fn optional_u16_field(
payload: &Value,
field: &str,
) -> Result<Option<u16>> {
let Some(value) = payload.get(field) else {
return Ok(None);
};
let raw = value
.as_u64()
.ok_or_else(|| anyhow!("{field} must be an unsigned integer."))?;
let parsed = u16::try_from(raw).map_err(|_| {
anyhow!("{field} is outside the supported connector range.")
})?;
Ok(Some(parsed))
}