use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use ocpp_core::OcppVersion;
use crate::tls::TlsSettings;
#[derive(Debug, Clone)]
pub struct Config {
pub ocpp_listen: Option<SocketAddr>,
pub ocpp_tls_listen: Option<SocketAddr>,
pub tls: Option<TlsSettings>,
pub cso_name: Option<String>,
pub max_connections: usize,
pub api_listen: SocketAddr,
pub database_url: String,
pub database_max_connections: u32,
pub node_id: String,
pub versions: Vec<OcppVersion>,
pub heartbeat_interval: Duration,
pub call_timeout: Duration,
pub ping_interval: Duration,
pub max_message_size: usize,
pub ws_deflate: bool,
pub ws_force_client_no_context_takeover: bool,
pub require_password: bool,
pub authorize_unknown_tokens: bool,
pub api_token: Option<String>,
pub command_queue_size: usize,
pub auth_concurrency: usize,
pub nominal_voltage: f64,
pub ev_charging_needs: bool,
pub allow_bidirectional: bool,
pub reservation_sweep_interval: Duration,
pub default_timezone: String,
pub zoneinfo_dir: PathBuf,
}
fn env_addr(name: &str, default: Option<&str>) -> Result<Option<SocketAddr>, String> {
let v = std::env::var(name).ok().filter(|v| !v.trim().is_empty());
match v.as_deref().or(default) {
None => Ok(None),
Some(v) if v.trim().eq_ignore_ascii_case("none") => Ok(None),
Some(v) => v
.trim()
.parse()
.map(Some)
.map_err(|_| format!("invalid address for {name}: {v}")),
}
}
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var(name)
.ok()
.filter(|v| !v.trim().is_empty())
.map(PathBuf::from)
}
fn tls_settings() -> Result<Option<TlsSettings>, String> {
let mut identities = Vec::new();
for (c, k) in [
("OCPP_TLS_CERT", "OCPP_TLS_KEY"),
("OCPP_TLS_CERT2", "OCPP_TLS_KEY2"),
] {
match (env_path(c), env_path(k)) {
(Some(cert), Some(key)) => identities.push((cert, key)),
(None, None) => {}
_ => return Err(format!("{c} and {k} must be set together")),
}
}
if identities.is_empty() {
return Ok(None);
}
Ok(Some(TlsSettings {
identities,
client_ca: env_path("OCPP_TLS_CLIENT_CA"),
ciphers: std::env::var("OCPP_TLS_CIPHERS")
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| crate::tls::DEFAULT_CIPHERS.into()),
}))
}
fn env<T: std::str::FromStr>(name: &str, default: T) -> Result<T, String> {
match std::env::var(name) {
Ok(v) if !v.trim().is_empty() => v
.trim()
.parse()
.map_err(|_| format!("invalid value for {name}: {v}")),
_ => Ok(default),
}
}
fn env_bool(name: &str, default: bool) -> Result<bool, String> {
match std::env::var(name) {
Ok(v) => match v.trim().to_ascii_lowercase().as_str() {
"" => Ok(default),
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
_ => Err(format!("invalid boolean for {name}: {v}")),
},
Err(_) => Ok(default),
}
}
impl Config {
pub fn from_env() -> Result<Config, String> {
let versions = std::env::var("OCPP_VERSIONS")
.unwrap_or_else(|_| "1.6,2.0.1,2.1".into())
.split(',')
.filter(|s| !s.trim().is_empty())
.map(str::parse)
.collect::<Result<Vec<OcppVersion>, _>>()?;
if versions.is_empty() {
return Err("OCPP_VERSIONS must enable at least one version".into());
}
let tls = tls_settings()?;
if std::env::var("OCPP_TLS_LISTEN")
.is_ok_and(|v| !v.trim().is_empty() && v.trim() != "none")
&& tls.is_none()
{
return Err("OCPP_TLS_LISTEN requires OCPP_TLS_CERT and OCPP_TLS_KEY".into());
}
let cpus = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
Ok(Config {
ocpp_listen: env_addr("OCPP_LISTEN", Some("0.0.0.0:9000"))?,
ocpp_tls_listen: env_addr("OCPP_TLS_LISTEN", None)?,
tls,
cso_name: std::env::var("OCPP_CSO_NAME")
.ok()
.filter(|s| !s.is_empty()),
max_connections: env("OCPP_MAX_CONNECTIONS", 200_000)?,
api_listen: env("API_LISTEN", "127.0.0.1:8080".parse().unwrap())?,
database_url: std::env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL is required".to_string())?,
database_max_connections: env("DATABASE_MAX_CONNECTIONS", 20)?,
node_id: std::env::var("NODE_ID")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(default_node_id),
versions,
heartbeat_interval: Duration::from_secs(env("HEARTBEAT_INTERVAL_SECS", 300)?),
call_timeout: Duration::from_secs(env("CALL_TIMEOUT_SECS", 30)?),
ping_interval: Duration::from_secs(env("PING_INTERVAL_SECS", 60)?),
max_message_size: env("MAX_MESSAGE_SIZE", 4 << 20)?,
ws_deflate: env_bool("WS_DEFLATE", true)?,
ws_force_client_no_context_takeover: env_bool(
"WS_FORCE_CLIENT_NO_CONTEXT_TAKEOVER",
false,
)?,
require_password: env_bool("OCPP_REQUIRE_PASSWORD", false)?,
authorize_unknown_tokens: env_bool("AUTHORIZE_UNKNOWN_TOKENS", false)?,
api_token: std::env::var("API_TOKEN").ok().filter(|s| !s.is_empty()),
command_queue_size: env("COMMAND_QUEUE_SIZE", 16)?,
auth_concurrency: env("AUTH_CONCURRENCY", cpus)?,
nominal_voltage: env("NOMINAL_VOLTAGE", 230.0)?,
ev_charging_needs: env_bool("SMART_CHARGING_EV_NEEDS", true)?,
allow_bidirectional: env_bool("ALLOW_BIDIRECTIONAL", false)?,
reservation_sweep_interval: Duration::from_secs(
env("RESERVATION_SWEEP_SECS", 30)?.max(1),
),
default_timezone: std::env::var("DEFAULT_TIMEZONE")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "UTC".into()),
zoneinfo_dir: env_path("TZDIR").unwrap_or_else(|| PathBuf::from("/usr/share/zoneinfo")),
})
}
}
fn default_node_id() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| format!("node-{}", std::process::id()))
}