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)

//! OCPP Charging Station Management System microservice.
//!
//! - `/<prefix>/<identity>` (OCPP listeners, plain and/or TLS): OCPP-J 1.6, 2.0.1 and 2.1 over WebSocket.
//! - `/api/v1/...` (API listener): station registry, search, commands.

pub mod api;
pub mod auth;
pub mod calls;
pub mod config;
pub mod db;
pub mod error;
pub mod hub;
pub mod ocpp;
pub mod reservation;
pub mod server;
pub mod smart;
pub mod state;
pub mod tariff;
pub mod tls;
pub mod tz;

use std::sync::Arc;

use axum::Router;
use axum::routing::any;
use tokio::net::TcpListener;
use tokio::sync::{Semaphore, watch};

use crate::config::Config;
use crate::state::{App, AppState};

pub fn ocpp_router(app: App) -> Router {
    // Any path: the station identity is the last segment.
    Router::new()
        .route("/{*path}", any(ocpp::handshake::upgrade))
        .with_state(app)
}

/// Builds the shared state. Run migrations before calling.
pub fn build_state(cfg: Config, db: db::Db, shutdown: watch::Receiver<bool>) -> App {
    Arc::new(AppState {
        auth: auth::Authenticator::new(cfg.auth_concurrency),
        cfg,
        db,
        hub: hub::Hub::new(),
        ids: ocpp_core::MessageIdGenerator::new(),
        zones: tz::Zones::default(),
        shutdown,
    })
}

pub struct Listeners {
    /// Plain OCPP listener (Security Profiles 0/1).
    pub ocpp: Option<TcpListener>,
    /// TLS OCPP listener (Security Profiles 2/3).
    pub ocpp_tls: Option<(TcpListener, Arc<tls::TlsAcceptor>)>,
    pub api: TcpListener,
}

/// Serves all listeners until `shutdown` resolves.
pub async fn serve(
    app: App,
    listeners: Listeners,
    shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> std::io::Result<()> {
    let (stop_tx, stop_rx) = watch::channel(false);
    let router = ocpp_router(app.clone());
    let mut tasks = Vec::new();
    if let Some(l) = listeners.ocpp {
        let limit = Arc::new(Semaphore::new(app.cfg.max_connections));
        tasks.push(tokio::spawn(server::serve_ocpp(
            l,
            None,
            router.clone(),
            limit,
            stop_rx.clone(),
        )));
    }
    if let Some((l, acceptor)) = listeners.ocpp_tls {
        let limit = Arc::new(Semaphore::new(app.cfg.max_connections));
        tasks.push(tokio::spawn(server::serve_ocpp(
            l,
            Some(acceptor),
            router.clone(),
            limit,
            stop_rx.clone(),
        )));
    }
    reservation::spawn_sweeper(app.clone(), stop_rx.clone());
    let mut api_stop = stop_rx.clone();
    let api_server =
        axum::serve(listeners.api, api::router(app.clone())).with_graceful_shutdown(async move {
            let _ = api_stop.changed().await;
        });
    tokio::spawn(async move {
        shutdown.await;
        let _ = stop_tx.send(true);
    });
    let result = api_server.await;
    for t in tasks {
        let _ = t.await;
    }
    result
}