codetics-ocpp-service 0.1.0

Codetics OCPP: Charging Station Management System (CSMS) service for OCPP 1.6J, 2.0.1 and 2.1, with TLS security profiles, smart charging, reservations and tariffs
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)

use std::time::Duration;

use std::sync::Arc;

use ocpp_service::tls::TlsAcceptor;
use ocpp_service::{Listeners, build_state, config::Config, db, serve};
use tokio::net::TcpListener;
use tokio::sync::watch;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() {
    let json_logs = std::env::var("LOG_FORMAT").is_ok_and(|v| v == "json");
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
    if json_logs {
        tracing_subscriber::fmt()
            .with_env_filter(filter)
            .json()
            .init();
    } else {
        tracing_subscriber::fmt().with_env_filter(filter).init();
    }

    if let Err(e) = run().await {
        tracing::error!(error = %e, "fatal");
        std::process::exit(1);
    }
}

async fn run() -> Result<(), Box<dyn std::error::Error>> {
    let cfg = Config::from_env()?;
    let db = db::connect(&cfg.database_url, cfg.database_max_connections).await?;
    db::migrate(&db).await?;
    let stale = db::stations::reset_node(&db, &cfg.node_id).await?;
    if stale > 0 {
        tracing::info!(
            count = stale,
            "cleared stale connection state from a previous run"
        );
    }

    let ocpp = match cfg.ocpp_listen {
        Some(addr) => Some(TcpListener::bind(addr).await?),
        None => None,
    };
    let ocpp_tls = match (cfg.ocpp_tls_listen, &cfg.tls) {
        (Some(addr), Some(settings)) => {
            let acceptor = Arc::new(TlsAcceptor::new(settings.clone())?);
            spawn_reload_on_sighup(acceptor.clone());
            Some((TcpListener::bind(addr).await?, acceptor))
        }
        _ => None,
    };
    if ocpp.is_none() && ocpp_tls.is_none() {
        return Err(
            "no OCPP listener configured (OCPP_LISTEN and OCPP_TLS_LISTEN are both disabled)"
                .into(),
        );
    }
    if ocpp_tls.is_some() && cfg.cso_name.is_none() {
        tracing::warn!(
            "OCPP_CSO_NAME is not set: the organizationName of station certificates is not checked (A00.FR.404)"
        );
    }
    let api = TcpListener::bind(cfg.api_listen).await?;
    tracing::info!(ocpp = ?cfg.ocpp_listen, ocpp_tls = ?cfg.ocpp_tls_listen, api = %cfg.api_listen, node = %cfg.node_id, versions = ?cfg.versions, "OCPP service listening");

    let (shutdown_tx, shutdown_rx) = watch::channel(false);
    let app = build_state(cfg, db, shutdown_rx);
    let signal = async move {
        shutdown_signal().await;
        tracing::info!("shutting down: closing station connections");
        let _ = shutdown_tx.send(true);
        // Let sessions send their close frames.
        tokio::time::sleep(Duration::from_millis(500)).await;
    };
    serve(
        app,
        Listeners {
            ocpp,
            ocpp_tls,
            api,
        },
        signal,
    )
    .await?;
    Ok(())
}

/// Reloads TLS certificates on SIGHUP (certificate rotation without restart).
fn spawn_reload_on_sighup(acceptor: Arc<TlsAcceptor>) {
    #[cfg(unix)]
    tokio::spawn(async move {
        let Ok(mut hup) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())
        else {
            return;
        };
        while hup.recv().await.is_some() {
            match acceptor.reload() {
                Ok(()) => tracing::info!("TLS certificates reloaded"),
                Err(e) => {
                    tracing::error!(error = %e, "TLS reload failed, keeping previous certificates")
                }
            }
        }
    });
    #[cfg(not(unix))]
    let _ = acceptor;
}

async fn shutdown_signal() {
    let ctrl_c = async {
        let _ = tokio::signal::ctrl_c().await;
    };
    #[cfg(unix)]
    let term = async {
        if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
        {
            s.recv().await;
        }
    };
    #[cfg(not(unix))]
    let term = std::future::pending::<()>();
    tokio::select! {
        _ = ctrl_c => {}
        _ = term => {}
    }
}