use alloc::boxed::Box;
use alloc::vec::Vec;
use core::fmt;
use crate::app_protocol::{SupportedAppProtocolReq, SupportedAppProtocolRes};
use crate::exi::{ExiDocument, ExiError};
use crate::v2gtp::PayloadType;
use crate::{Protocol, session::SessionId};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Message {
AppProtocolReq(Box<SupportedAppProtocolReq>),
AppProtocolRes(Box<SupportedAppProtocolRes>),
#[cfg(feature = "iso2")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso2")))]
Iso2(Box<crate::iso2::Document>),
#[cfg(feature = "iso20-common")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-common")))]
Iso20(Box<crate::iso20::messages::Document>),
#[cfg(feature = "iso20-ac")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-ac")))]
Iso20Ac(Box<crate::iso20::ac::Document>),
#[cfg(feature = "iso20-dc")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-dc")))]
Iso20Dc(Box<crate::iso20::dc::Document>),
#[cfg(feature = "iso20-wpt")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-wpt")))]
Iso20Wpt(Box<crate::iso20::wpt::Document>),
#[cfg(feature = "iso20-acdp")]
#[cfg_attr(docsrs, doc(cfg(feature = "iso20-acdp")))]
Iso20Acdp(Box<crate::iso20::acdp::Document>),
}
impl Message {
pub fn decode(
protocol: Option<Protocol>,
payload_type: PayloadType,
bytes: &[u8],
) -> Result<Self, MessageError> {
if !payload_type.is_exi() {
return Err(MessageError::NotAMessage(payload_type));
}
match (payload_type, protocol) {
(PayloadType::ExiEncodedV2gMessage, None) => SupportedAppProtocolReq::from_bytes(bytes)
.map(|m| Self::AppProtocolReq(Box::new(m)))
.or_else(|_| {
SupportedAppProtocolRes::from_bytes(bytes)
.map(|m| Self::AppProtocolRes(Box::new(m)))
})
.map_err(MessageError::Exi),
#[cfg(feature = "iso2")]
(PayloadType::ExiEncodedV2gMessage, Some(Protocol::Iso2)) => {
crate::iso2::Document::from_bytes(bytes)
.map(|m| Self::Iso2(Box::new(m)))
.map_err(MessageError::Exi)
}
#[cfg(feature = "iso20-common")]
(PayloadType::Part20Main, Some(Protocol::Iso20)) => {
crate::iso20::messages::Document::from_bytes(bytes)
.map(|m| Self::Iso20(Box::new(m)))
.map_err(MessageError::Exi)
}
#[cfg(feature = "iso20-ac")]
(PayloadType::Part20Ac, Some(Protocol::Iso20)) => {
crate::iso20::ac::Document::from_bytes(bytes)
.map(|m| Self::Iso20Ac(Box::new(m)))
.map_err(MessageError::Exi)
}
#[cfg(feature = "iso20-dc")]
(PayloadType::Part20Dc, Some(Protocol::Iso20)) => {
crate::iso20::dc::Document::from_bytes(bytes)
.map(|m| Self::Iso20Dc(Box::new(m)))
.map_err(MessageError::Exi)
}
#[cfg(feature = "iso20-wpt")]
(PayloadType::Part20Wpt, Some(Protocol::Iso20)) => {
crate::iso20::wpt::Document::from_bytes(bytes)
.map(|m| Self::Iso20Wpt(Box::new(m)))
.map_err(MessageError::Exi)
}
#[cfg(feature = "iso20-acdp")]
(PayloadType::Part20Acdp, Some(Protocol::Iso20)) => {
crate::iso20::acdp::Document::from_bytes(bytes)
.map(|m| Self::Iso20Acdp(Box::new(m)))
.map_err(MessageError::Exi)
}
_ if !payload_type.belongs_to(protocol) => {
Err(MessageError::NotForThisSession { payload_type, protocol })
}
_ => Err(MessageError::NoCodec { payload_type, protocol }),
}
}
pub fn encode(&self) -> Result<(PayloadType, Vec<u8>), MessageError> {
let bytes = match self {
Self::AppProtocolReq(m) => m.to_vec(),
Self::AppProtocolRes(m) => m.to_vec(),
#[cfg(feature = "iso2")]
Self::Iso2(m) => m.to_vec(),
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => m.to_vec(),
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => m.to_vec(),
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => m.to_vec(),
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => m.to_vec(),
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => m.to_vec(),
}
.map_err(MessageError::Exi)?;
Ok((self.payload_type(), bytes))
}
#[must_use]
pub const fn payload_type(&self) -> PayloadType {
match self {
Self::AppProtocolReq(_) | Self::AppProtocolRes(_) => PayloadType::ExiEncodedV2gMessage,
#[cfg(feature = "iso2")]
Self::Iso2(_) => PayloadType::ExiEncodedV2gMessage,
#[cfg(feature = "iso20-common")]
Self::Iso20(_) => PayloadType::Part20Main,
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(_) => PayloadType::Part20Ac,
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(_) => PayloadType::Part20Dc,
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(_) => PayloadType::Part20Wpt,
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(_) => PayloadType::Part20Acdp,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::AppProtocolReq(_) => "supportedAppProtocolReq",
Self::AppProtocolRes(_) => "supportedAppProtocolRes",
#[cfg(feature = "iso2")]
Self::Iso2(m) => match &**m {
crate::iso2::Document::V2GMessage(v) => match &v.body.choice {
Some(choice) => choice.name(),
None => "V2G_Message",
},
other => other.name(),
},
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => m.name(),
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => m.name(),
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => m.name(),
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => m.name(),
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => m.name(),
}
}
#[must_use]
pub fn outcome(&self) -> Option<Outcome> {
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
fn classify(is_ok: bool, is_warning: bool) -> Outcome {
match (is_ok, is_warning) {
(true, _) => Outcome::Ok,
(_, true) => Outcome::Warning,
_ => Outcome::Failed,
}
}
match self {
Self::AppProtocolReq(_) => None,
Self::AppProtocolRes(m) => {
Some(if m.response_code.is_ok() { Outcome::Ok } else { Outcome::Failed })
}
#[cfg(feature = "iso2")]
Self::Iso2(m) => {
let crate::iso2::Document::V2GMessage(v) = &**m else { return None };
let code = v.body.choice.as_ref()?.response_code()?;
Some(classify(code.is_ok(), code.is_warning()))
}
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => m.response_code().map(|c| classify(c.is_ok(), c.is_warning())),
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => m.response_code().map(|c| classify(c.is_ok(), c.is_warning())),
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => m.response_code().map(|c| classify(c.is_ok(), c.is_warning())),
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => m.response_code().map(|c| classify(c.is_ok(), c.is_warning())),
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => m.response_code().map(|c| classify(c.is_ok(), c.is_warning())),
}
}
#[must_use]
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "iso2", feature = "iso20-common"))))]
pub fn ev_energy_status(&self) -> Option<EvEnergyStatus> {
let status = match self {
#[cfg(feature = "iso2")]
Self::Iso2(m) => iso2_energy_status(m)?,
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => iso20_common_energy_status(m)?,
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => {
use crate::iso20::ac::ACChargeLoopReqChoice as C;
let crate::iso20::ac::Document::ACChargeLoopReq(r) = &**m else { return None };
let mut out = display_energy_status(r.display_parameters.as_ref());
match &r.choice {
C::DynamicACCLReqControlMode(c) => out.take_request(
c.departure_time,
&c.ev_target_energy_request,
&c.ev_minimum_energy_request,
&c.ev_maximum_energy_request,
),
C::BPTDynamicACCLReqControlMode(c) => out.take_request(
c.departure_time,
&c.ev_target_energy_request,
&c.ev_minimum_energy_request,
&c.ev_maximum_energy_request,
),
_ => {}
}
out
}
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => {
use crate::iso20::dc::DCChargeLoopReqChoice as C;
let crate::iso20::dc::Document::DCChargeLoopReq(r) = &**m else { return None };
let mut out = display_energy_status(r.display_parameters.as_ref());
match &r.choice {
C::DynamicDCCLReqControlMode(c) => out.take_request(
c.departure_time,
&c.ev_target_energy_request,
&c.ev_minimum_energy_request,
&c.ev_maximum_energy_request,
),
C::BPTDynamicDCCLReqControlMode(c) => out.take_request(
c.departure_time,
&c.ev_target_energy_request,
&c.ev_minimum_energy_request,
&c.ev_maximum_energy_request,
),
_ => {}
}
out
}
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => match &**m {
crate::iso20::wpt::Document::WPTChargeLoopReq(r) => {
display_energy_status(r.display_parameters.as_ref())
}
_ => return None,
},
_ => return None,
};
if status.is_empty() { None } else { Some(status) }
}
#[must_use]
pub fn is_request(&self) -> bool {
self.name().ends_with("Req")
}
#[must_use]
#[allow(unused_variables, reason = "every arm that uses `code` is behind a feature")]
pub fn refusal(&self, code: u8) -> Option<Self> {
match self {
Self::AppProtocolReq(_) | Self::AppProtocolRes(_) => None,
#[cfg(feature = "iso2")]
Self::Iso2(m) => {
let crate::iso2::Document::V2GMessage(v) = &**m else { return None };
let code = crate::iso2::ResponseCode::from_index(u64::from(code)).ok()?;
let body = v.body.choice.as_ref()?.refusal(code)?;
Some(Self::Iso2(alloc::boxed::Box::new(crate::iso2::Document::V2GMessage(
crate::iso2::V2GMessage {
header: crate::iso2::MessageHeader {
session_id: v.header.session_id.clone(),
notification: None,
signature: None,
},
body: crate::iso2::Body { choice: Some(body) },
},
))))
}
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => {
let code = crate::iso20::common::ResponseCode::from_index(u64::from(code)).ok()?;
Some(Self::Iso20(alloc::boxed::Box::new(m.refusal(code)?)))
}
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => {
let code = crate::iso20::common::ResponseCode::from_index(u64::from(code)).ok()?;
Some(Self::Iso20Ac(alloc::boxed::Box::new(m.refusal(code)?)))
}
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => {
let code = crate::iso20::common::ResponseCode::from_index(u64::from(code)).ok()?;
Some(Self::Iso20Dc(alloc::boxed::Box::new(m.refusal(code)?)))
}
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => {
let code = crate::iso20::common::ResponseCode::from_index(u64::from(code)).ok()?;
Some(Self::Iso20Wpt(alloc::boxed::Box::new(m.refusal(code)?)))
}
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => {
let code = crate::iso20::common::ResponseCode::from_index(u64::from(code)).ok()?;
Some(Self::Iso20Acdp(alloc::boxed::Box::new(m.refusal(code)?)))
}
}
}
#[must_use]
pub fn session_id(&self) -> Option<SessionId> {
match self {
Self::AppProtocolReq(_) | Self::AppProtocolRes(_) => None,
#[cfg(feature = "iso2")]
Self::Iso2(m) => match &**m {
crate::iso2::Document::V2GMessage(v) => {
SessionId::from_slice(&v.header.session_id).ok()
}
_ => None,
},
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => header_session_id(m.header()),
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => header_session_id(m.header()),
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => header_session_id(m.header()),
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => header_session_id(m.header()),
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => header_session_id(m.header()),
}
}
#[must_use]
pub fn answers(&self, request: &str) -> bool {
match (request.strip_suffix("Req"), self.name().strip_suffix("Res")) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
#[allow(unused_variables, reason = "every arm that uses `id` is behind a feature")]
pub fn set_session_id(&mut self, id: SessionId) -> bool {
match self {
Self::AppProtocolReq(_) | Self::AppProtocolRes(_) => false,
#[cfg(feature = "iso2")]
Self::Iso2(m) => match &mut **m {
crate::iso2::Document::V2GMessage(v) => {
v.header.session_id = id.as_bytes().to_vec();
true
}
_ => false,
},
#[cfg(feature = "iso20-common")]
Self::Iso20(m) => set_header_session_id(m.header_mut(), id),
#[cfg(feature = "iso20-ac")]
Self::Iso20Ac(m) => set_header_session_id(m.header_mut(), id),
#[cfg(feature = "iso20-dc")]
Self::Iso20Dc(m) => set_header_session_id(m.header_mut(), id),
#[cfg(feature = "iso20-wpt")]
Self::Iso20Wpt(m) => set_header_session_id(m.header_mut(), id),
#[cfg(feature = "iso20-acdp")]
Self::Iso20Acdp(m) => set_header_session_id(m.header_mut(), id),
}
}
}
#[cfg(feature = "iso2")]
fn iso2_energy_status(doc: &crate::iso2::Document) -> Option<EvEnergyStatus> {
use crate::iso2::{BodyChoice as B, ChargeParameterDiscoveryReqChoice as P};
let crate::iso2::Document::V2GMessage(v2g) = doc else { return None };
let mut out = EvEnergyStatus::default();
let dc_status = match v2g.body.choice.as_ref()? {
B::CableCheckReq(r) => Some(&r.dc_ev_status),
B::PreChargeReq(r) => Some(&r.dc_ev_status),
B::WeldingDetectionReq(r) => Some(&r.dc_ev_status),
B::CurrentDemandReq(r) => {
out.charging_complete = Some(r.charging_complete);
Some(&r.dc_ev_status)
}
B::PowerDeliveryReq(r) => match r.choice.as_ref() {
Some(crate::iso2::PowerDeliveryReqChoice::DCEVPowerDeliveryParameter(p)) => {
out.charging_complete = Some(p.charging_complete);
Some(&p.dc_ev_status)
}
_ => None,
},
B::ChargeParameterDiscoveryReq(r) => match &r.choice {
P::DCEVChargeParameter(p) => {
out.departure_in = p.departure_time;
out.full_soc = p.full_soc;
out.bulk_soc = p.bulk_soc;
out.energy_capacity = p.ev_energy_capacity.as_ref().and_then(energy_mwh);
out.target_energy_request = p.ev_energy_request.as_ref().and_then(energy_mwh);
Some(&p.dc_ev_status)
}
P::ACEVChargeParameter(p) => {
out.departure_in = p.departure_time;
out.target_energy_request = energy_mwh(&p.e_amount);
None
}
P::EVChargeParameter(_) => None,
},
_ => return None,
};
if let Some(dc) = dc_status {
out.present_soc = Some(dc.ev_ress_soc);
}
Some(out)
}
#[cfg(feature = "iso20-common")]
fn iso20_common_energy_status(doc: &crate::iso20::messages::Document) -> Option<EvEnergyStatus> {
use crate::iso20::messages::{Document as D, ScheduleExchangeReqChoice as C};
let D::ScheduleExchangeReq(req) = doc else { return None };
let mut out = EvEnergyStatus::default();
match &req.choice {
C::DynamicSEReqControlMode(m) => {
out.departure_in = Some(m.departure_time);
out.minimum_soc = m.minimum_soc;
out.target_soc = m.target_soc;
out.target_energy_request = rational_mwh(&m.ev_target_energy_request);
out.minimum_energy_request = rational_mwh(&m.ev_minimum_energy_request);
out.maximum_energy_request = rational_mwh(&m.ev_maximum_energy_request);
}
C::ScheduledSEReqControlMode(m) => {
out.departure_in = m.departure_time;
out.target_energy_request = m.ev_target_energy_request.as_ref().and_then(rational_mwh);
out.minimum_energy_request =
m.ev_minimum_energy_request.as_ref().and_then(rational_mwh);
out.maximum_energy_request =
m.ev_maximum_energy_request.as_ref().and_then(rational_mwh);
}
}
Some(out)
}
#[cfg(any(feature = "iso20-ac", feature = "iso20-dc", feature = "iso20-wpt"))]
fn display_energy_status(p: Option<&crate::iso20::common::DisplayParameters>) -> EvEnergyStatus {
let Some(p) = p else { return EvEnergyStatus::default() };
EvEnergyStatus {
present_soc: p.present_soc,
target_soc: p.target_soc,
minimum_soc: p.minimum_soc,
full_soc: p.maximum_soc,
bulk_soc: None,
energy_capacity: p.battery_energy_capacity.as_ref().and_then(rational_mwh),
target_energy_request: None,
minimum_energy_request: None,
maximum_energy_request: None,
departure_in: None,
charging_complete: p.charging_complete,
}
}
#[cfg(feature = "iso20-common")]
fn header_session_id(header: Option<&crate::iso20::common::MessageHeader>) -> Option<SessionId> {
SessionId::from_slice(&header?.session_id).ok()
}
#[cfg(feature = "iso20-common")]
fn set_header_session_id(
header: Option<&mut crate::iso20::common::MessageHeader>,
id: SessionId,
) -> bool {
match header {
Some(header) => {
header.session_id = id.as_bytes().to_vec();
true
}
None => false,
}
}
pub type MilliwattHours = i64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct EvEnergyStatus {
pub present_soc: Option<u8>,
pub target_soc: Option<u8>,
pub minimum_soc: Option<u8>,
pub full_soc: Option<u8>,
pub bulk_soc: Option<u8>,
pub energy_capacity: Option<MilliwattHours>,
pub target_energy_request: Option<MilliwattHours>,
pub minimum_energy_request: Option<MilliwattHours>,
pub maximum_energy_request: Option<MilliwattHours>,
pub departure_in: Option<u32>,
pub charging_complete: Option<bool>,
}
impl EvEnergyStatus {
#[cfg(any(feature = "iso20-ac", feature = "iso20-dc"))]
fn take_request(
&mut self,
departure: Option<u32>,
target: &crate::iso20::common::RationalNumber,
minimum: &crate::iso20::common::RationalNumber,
maximum: &crate::iso20::common::RationalNumber,
) {
self.departure_in = departure;
self.target_energy_request = rational_mwh(target);
self.minimum_energy_request = rational_mwh(minimum);
self.maximum_energy_request = rational_mwh(maximum);
}
#[must_use]
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
#[cfg(feature = "iso2")]
fn energy_mwh(value: &crate::iso2::PhysicalValue) -> Option<MilliwattHours> {
if value.unit != crate::iso2::UnitSymbol::Wh {
return None;
}
scale_mwh(i64::from(value.value), i32::from(value.multiplier))
}
#[cfg(feature = "iso20-common")]
fn rational_mwh(value: &crate::iso20::common::RationalNumber) -> Option<MilliwattHours> {
scale_mwh(i64::from(value.value), i32::from(value.exponent))
}
#[cfg(any(feature = "iso2", feature = "iso20-common"))]
fn scale_mwh(value: i64, exponent: i32) -> Option<MilliwattHours> {
let exponent = exponent.checked_add(3)?;
let scale = 10i64.checked_pow(u32::try_from(exponent).ok()?)?;
value.checked_mul(scale)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Outcome {
Ok,
Warning,
Failed,
}
impl Outcome {
#[must_use]
pub const fn is_failure(self) -> bool {
matches!(self, Self::Failed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MessageError {
Exi(ExiError),
NotAMessage(PayloadType),
NotForThisSession {
payload_type: PayloadType,
protocol: Option<Protocol>,
},
NoCodec {
payload_type: PayloadType,
protocol: Option<Protocol>,
},
}
impl From<ExiError> for MessageError {
fn from(e: ExiError) -> Self {
Self::Exi(e)
}
}
impl fmt::Display for MessageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exi(e) => write!(f, "EXI: {e}"),
Self::NotAMessage(t) => {
write!(f, "V2GTP payload type {:#06x} does not carry a V2G message", t.as_u16())
}
Self::NotForThisSession { payload_type, protocol } => write!(
f,
"V2GTP payload type {:#06x} does not belong to {}",
payload_type.as_u16(),
match protocol {
Some(p) => p.as_str(),
None => "an unnegotiated session",
}
),
Self::NoCodec { payload_type, protocol } => write!(
f,
"no message set compiled in for V2GTP payload type {:#06x} under {}",
payload_type.as_u16(),
match protocol {
Some(p) => p.as_str(),
None => "an unnegotiated session",
}
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MessageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Exi(e) => Some(e),
_ => None,
}
}
}