1use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::time::Duration;
9
10use ocpp_core::OcppVersion;
11
12use crate::tls::TlsSettings;
13
14#[derive(Debug, Clone)]
15pub struct Config {
16 pub ocpp_listen: Option<SocketAddr>,
18 pub ocpp_tls_listen: Option<SocketAddr>,
20 pub tls: Option<TlsSettings>,
23 pub cso_name: Option<String>,
25 pub max_connections: usize,
27 pub api_listen: SocketAddr,
29 pub database_url: String,
31 pub database_max_connections: u32,
33 pub node_id: String,
35 pub versions: Vec<OcppVersion>,
37 pub heartbeat_interval: Duration,
39 pub call_timeout: Duration,
41 pub ping_interval: Duration,
43 pub max_message_size: usize,
45 pub ws_deflate: bool,
47 pub ws_force_client_no_context_takeover: bool,
49 pub require_password: bool,
51 pub authorize_unknown_tokens: bool,
53 pub api_token: Option<String>,
55 pub command_queue_size: usize,
57 pub auth_concurrency: usize,
59 pub nominal_voltage: f64,
61 pub ev_charging_needs: bool,
63 pub allow_bidirectional: bool,
65 pub reservation_sweep_interval: Duration,
67 pub default_timezone: String,
69 pub zoneinfo_dir: PathBuf,
71}
72
73fn env_addr(name: &str, default: Option<&str>) -> Result<Option<SocketAddr>, String> {
74 let v = std::env::var(name).ok().filter(|v| !v.trim().is_empty());
75 match v.as_deref().or(default) {
76 None => Ok(None),
77 Some(v) if v.trim().eq_ignore_ascii_case("none") => Ok(None),
78 Some(v) => v
79 .trim()
80 .parse()
81 .map(Some)
82 .map_err(|_| format!("invalid address for {name}: {v}")),
83 }
84}
85
86fn env_path(name: &str) -> Option<PathBuf> {
87 std::env::var(name)
88 .ok()
89 .filter(|v| !v.trim().is_empty())
90 .map(PathBuf::from)
91}
92
93fn tls_settings() -> Result<Option<TlsSettings>, String> {
94 let mut identities = Vec::new();
95 for (c, k) in [
96 ("OCPP_TLS_CERT", "OCPP_TLS_KEY"),
97 ("OCPP_TLS_CERT2", "OCPP_TLS_KEY2"),
98 ] {
99 match (env_path(c), env_path(k)) {
100 (Some(cert), Some(key)) => identities.push((cert, key)),
101 (None, None) => {}
102 _ => return Err(format!("{c} and {k} must be set together")),
103 }
104 }
105 if identities.is_empty() {
106 return Ok(None);
107 }
108 Ok(Some(TlsSettings {
109 identities,
110 client_ca: env_path("OCPP_TLS_CLIENT_CA"),
111 ciphers: std::env::var("OCPP_TLS_CIPHERS")
112 .ok()
113 .filter(|v| !v.is_empty())
114 .unwrap_or_else(|| crate::tls::DEFAULT_CIPHERS.into()),
115 }))
116}
117
118fn env<T: std::str::FromStr>(name: &str, default: T) -> Result<T, String> {
119 match std::env::var(name) {
120 Ok(v) if !v.trim().is_empty() => v
121 .trim()
122 .parse()
123 .map_err(|_| format!("invalid value for {name}: {v}")),
124 _ => Ok(default),
125 }
126}
127
128fn env_bool(name: &str, default: bool) -> Result<bool, String> {
129 match std::env::var(name) {
130 Ok(v) => match v.trim().to_ascii_lowercase().as_str() {
131 "" => Ok(default),
132 "1" | "true" | "yes" | "on" => Ok(true),
133 "0" | "false" | "no" | "off" => Ok(false),
134 _ => Err(format!("invalid boolean for {name}: {v}")),
135 },
136 Err(_) => Ok(default),
137 }
138}
139
140impl Config {
141 pub fn from_env() -> Result<Config, String> {
142 let versions = std::env::var("OCPP_VERSIONS")
143 .unwrap_or_else(|_| "1.6,2.0.1,2.1".into())
144 .split(',')
145 .filter(|s| !s.trim().is_empty())
146 .map(str::parse)
147 .collect::<Result<Vec<OcppVersion>, _>>()?;
148 if versions.is_empty() {
149 return Err("OCPP_VERSIONS must enable at least one version".into());
150 }
151 let tls = tls_settings()?;
152 if std::env::var("OCPP_TLS_LISTEN")
153 .is_ok_and(|v| !v.trim().is_empty() && v.trim() != "none")
154 && tls.is_none()
155 {
156 return Err("OCPP_TLS_LISTEN requires OCPP_TLS_CERT and OCPP_TLS_KEY".into());
157 }
158 let cpus = std::thread::available_parallelism()
159 .map(|n| n.get())
160 .unwrap_or(4);
161 Ok(Config {
162 ocpp_listen: env_addr("OCPP_LISTEN", Some("0.0.0.0:9000"))?,
163 ocpp_tls_listen: env_addr("OCPP_TLS_LISTEN", None)?,
164 tls,
165 cso_name: std::env::var("OCPP_CSO_NAME")
166 .ok()
167 .filter(|s| !s.is_empty()),
168 max_connections: env("OCPP_MAX_CONNECTIONS", 200_000)?,
169 api_listen: env("API_LISTEN", "127.0.0.1:8080".parse().unwrap())?,
170 database_url: std::env::var("DATABASE_URL")
171 .map_err(|_| "DATABASE_URL is required".to_string())?,
172 database_max_connections: env("DATABASE_MAX_CONNECTIONS", 20)?,
173 node_id: std::env::var("NODE_ID")
174 .ok()
175 .filter(|s| !s.is_empty())
176 .unwrap_or_else(default_node_id),
177 versions,
178 heartbeat_interval: Duration::from_secs(env("HEARTBEAT_INTERVAL_SECS", 300)?),
179 call_timeout: Duration::from_secs(env("CALL_TIMEOUT_SECS", 30)?),
180 ping_interval: Duration::from_secs(env("PING_INTERVAL_SECS", 60)?),
181 max_message_size: env("MAX_MESSAGE_SIZE", 4 << 20)?,
182 ws_deflate: env_bool("WS_DEFLATE", true)?,
183 ws_force_client_no_context_takeover: env_bool(
184 "WS_FORCE_CLIENT_NO_CONTEXT_TAKEOVER",
185 false,
186 )?,
187 require_password: env_bool("OCPP_REQUIRE_PASSWORD", false)?,
188 authorize_unknown_tokens: env_bool("AUTHORIZE_UNKNOWN_TOKENS", false)?,
189 api_token: std::env::var("API_TOKEN").ok().filter(|s| !s.is_empty()),
190 command_queue_size: env("COMMAND_QUEUE_SIZE", 16)?,
191 auth_concurrency: env("AUTH_CONCURRENCY", cpus)?,
192 nominal_voltage: env("NOMINAL_VOLTAGE", 230.0)?,
193 ev_charging_needs: env_bool("SMART_CHARGING_EV_NEEDS", true)?,
194 allow_bidirectional: env_bool("ALLOW_BIDIRECTIONAL", false)?,
195 reservation_sweep_interval: Duration::from_secs(
196 env("RESERVATION_SWEEP_SECS", 30)?.max(1),
197 ),
198 default_timezone: std::env::var("DEFAULT_TIMEZONE")
199 .ok()
200 .filter(|s| !s.is_empty())
201 .unwrap_or_else(|| "UTC".into()),
202 zoneinfo_dir: env_path("TZDIR").unwrap_or_else(|| PathBuf::from("/usr/share/zoneinfo")),
203 })
204 }
205}
206
207fn default_node_id() -> String {
208 std::env::var("HOSTNAME")
209 .ok()
210 .filter(|s| !s.is_empty())
211 .unwrap_or_else(|| format!("node-{}", std::process::id()))
212}