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 1.6 charging profiles (§3.13, §5.5, §5.7, §5.16).

use ocpp16::{
    ChargingProfile, ChargingProfileKindType, ChargingRateUnitType, ChargingSchedule,
    RecurrencyKindType,
};
use serde_json::{Value, json};

use super::model::{
    self, Kind, Period, Profile, Purpose, Recurrency, RuleViolation, Schedule, Unit,
};

pub fn schedule(s: &ChargingSchedule) -> Schedule {
    Schedule {
        start: s.start_schedule,
        duration: s.duration,
        unit: match s.charging_rate_unit {
            ChargingRateUnitType::A => Unit::A,
            ChargingRateUnitType::W => Unit::W,
        },
        periods: s
            .charging_schedule_period
            .iter()
            .map(|p| Period {
                start: p.start_period,
                limit: Some(p.limit),
                phases: p.number_phases,
            })
            .collect(),
    }
}

pub fn normalize(connector_id: i64, p: &ChargingProfile) -> Profile {
    Profile {
        id: p.charging_profile_id,
        evse: connector_id,
        purpose: Purpose::parse(p.charging_profile_purpose.as_str())
            .expect("1.6 purposes are known"),
        stack_level: p.stack_level,
        kind: match p.charging_profile_kind {
            ChargingProfileKindType::Absolute => Kind::Absolute,
            ChargingProfileKindType::Recurring => Kind::Recurring,
            ChargingProfileKindType::Relative => Kind::Relative,
        },
        recurrency: p.recurrency_kind.map(|r| match r {
            RecurrencyKindType::Daily => Recurrency::Daily,
            RecurrencyKindType::Weekly => Recurrency::Weekly,
        }),
        valid_from: p.valid_from,
        valid_to: p.valid_to,
        transaction: p.transaction_id.map(|t| t.to_string()),
        schedule: schedule(&p.charging_schedule),
        dyn_updated_at: None,
    }
}

pub fn decode(v: &Value) -> Result<ChargingProfile, String> {
    ocpp_core::decode_value(v.clone()).map_err(|e| e.to_string())
}

/// Checks a profile and fills `transactionId` of a TxProfile from the active
/// transaction on the connector. Returns the SetChargingProfile payload.
pub fn prepare(
    connector_id: i64,
    mut p: ChargingProfile,
    active_tx: Option<&str>,
) -> Result<(Profile, Value), RuleViolation> {
    if p.charging_profile_purpose.as_str() == "TxProfile" {
        // §3.13.1: a TxProfile without active transaction is discarded by the Charge Point.
        let Some(active) = active_tx else {
            return Err(RuleViolation::new(
                "1.6 §3.13.1",
                "a TxProfile requires an active transaction on the connector",
            ));
        };
        let active: i64 = active.parse().map_err(|_| {
            RuleViolation::new("1.6 §5.16", "active transaction id is not an integer")
        })?;
        // §5.16: the Central System SHALL include the transactionId.
        match p.transaction_id {
            None => p.transaction_id = Some(active),
            Some(t) if t != active => {
                return Err(RuleViolation::new(
                    "1.6 §5.16",
                    format!(
                        "transactionId {t} is not the active transaction {active} of the connector"
                    ),
                ));
            }
            Some(_) => {}
        }
    }
    let profile = normalize(connector_id, &p);
    model::check_structure(&profile)?;
    let payload = json!({ "connectorId": connector_id, "csChargingProfiles": p });
    Ok((profile, payload))
}

/// §5.16.2: a profile in RemoteStartTransaction is a TxProfile without transactionId.
pub fn check_remote_start_profile(p: &ChargingProfile) -> Result<(), RuleViolation> {
    if p.charging_profile_purpose.as_str() != "TxProfile" || p.transaction_id.is_some() {
        return Err(RuleViolation::new(
            "1.6 §5.16.2",
            "the chargingProfile of RemoteStartTransaction must be a TxProfile without transactionId",
        ));
    }
    Ok(())
}