Skip to main content

codetics_ocpp_core/
version.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Kevin Vavelin, Codetics (https://codetics.fr)
3
4//! OCPP version negotiation through the WebSocket subprotocol.
5//!
6//! The `Sec-WebSocket-Protocol` header is the single source of truth for the
7//! OCPP version of a connection (OCPP-J 1.6 §3.1.2; 2.0.1/2.1 Part 4 §3.1.2:
8//! "If the OCPP version is part of the OCPP-J endpoint URL it SHALL NOT
9//! determine the OCPP version to use"). The client lists versions in order of
10//! preference; the server answers with exactly one, or with none and then
11//! closes the connection.
12
13use std::fmt;
14use std::str::FromStr;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub enum OcppVersion {
18    V16,
19    V201,
20    V21,
21}
22
23impl OcppVersion {
24    pub const ALL: [OcppVersion; 3] = [OcppVersion::V16, OcppVersion::V201, OcppVersion::V21];
25
26    /// IANA-registered WebSocket subprotocol name.
27    pub const fn subprotocol(self) -> &'static str {
28        match self {
29            OcppVersion::V16 => "ocpp1.6",
30            OcppVersion::V201 => "ocpp2.0.1",
31            OcppVersion::V21 => "ocpp2.1",
32        }
33    }
34
35    pub fn from_subprotocol(s: &str) -> Option<OcppVersion> {
36        // Subprotocol tokens are case-sensitive per RFC 6455, but some
37        // stations send e.g. "OCPP1.6"; accepting them is harmless.
38        match s.trim().to_ascii_lowercase().as_str() {
39            "ocpp1.6" => Some(OcppVersion::V16),
40            "ocpp2.0.1" => Some(OcppVersion::V201),
41            "ocpp2.1" => Some(OcppVersion::V21),
42            _ => None,
43        }
44    }
45
46    pub const fn dialect(self) -> crate::ErrorDialect {
47        match self {
48            OcppVersion::V16 => crate::ErrorDialect::Ocpp16,
49            _ => crate::ErrorDialect::Ocpp2,
50        }
51    }
52
53    /// Short identifier used in URLs, configuration and the database.
54    pub const fn as_str(self) -> &'static str {
55        match self {
56            OcppVersion::V16 => "1.6",
57            OcppVersion::V201 => "2.0.1",
58            OcppVersion::V21 => "2.1",
59        }
60    }
61}
62
63impl fmt::Display for OcppVersion {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        f.write_str(self.as_str())
66    }
67}
68
69impl FromStr for OcppVersion {
70    type Err = String;
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        match s.trim() {
73            "1.6" | "16" | "ocpp1.6" => Ok(OcppVersion::V16),
74            "2.0.1" | "201" | "ocpp2.0.1" => Ok(OcppVersion::V201),
75            "2.1" | "21" | "ocpp2.1" => Ok(OcppVersion::V21),
76            other => Err(format!("unknown OCPP version '{other}'")),
77        }
78    }
79}
80
81/// Picks the first version offered by the client (in its preference order)
82/// that is also acceptable to the server. Returns the version together with
83/// the exact token the client sent, which must be echoed back verbatim.
84pub fn negotiate<'a, I>(offered: I, acceptable: &[OcppVersion]) -> Option<(OcppVersion, &'a str)>
85where
86    I: IntoIterator<Item = &'a str>,
87{
88    offered
89        .into_iter()
90        .flat_map(|h| h.split(','))
91        .map(str::trim)
92        .filter(|t| !t.is_empty())
93        .find_map(|t| {
94            OcppVersion::from_subprotocol(t)
95                .filter(|v| acceptable.contains(v))
96                .map(|v| (v, t))
97        })
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn negotiation_respects_client_preference() {
106        let all = OcppVersion::ALL;
107        assert_eq!(
108            negotiate(["ocpp2.1, ocpp2.0.1, ocpp1.6"], &all),
109            Some((OcppVersion::V21, "ocpp2.1"))
110        );
111        assert_eq!(
112            negotiate(["ocpp1.6, ocpp2.0.1"], &all),
113            Some((OcppVersion::V16, "ocpp1.6"))
114        );
115        assert_eq!(
116            negotiate(["ocpp2.1", "ocpp1.6"], &[OcppVersion::V16]),
117            Some((OcppVersion::V16, "ocpp1.6"))
118        );
119        assert_eq!(negotiate(["ocpp1.5, ocpp1.8"], &all), None);
120        assert_eq!(negotiate([], &all), None);
121        assert_eq!(
122            negotiate(["OCPP1.6"], &all),
123            Some((OcppVersion::V16, "OCPP1.6"))
124        );
125    }
126}