Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions errors/powermeter.yaml

This file was deleted.

2 changes: 1 addition & 1 deletion interfaces/powermeter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ vars:
description: The public key for OCMF
type: string
errors:
- reference: /errors/powermeter
- reference: /errors/generic
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interfaces/ocpp.yaml,46a82fad56f8437d8567da9a1b7c4013ffa8eca9db187e034d02beda496
interfaces/ocpp_data_transfer.yaml,dec8974ba68bf9ef20ba68616873f714bd0a3255c6ea28fefc6f5c524666aebe
interfaces/over_voltage_monitor.yaml,e900756a2058fca6c24434ac153c34afdf109ad582ca33c5aad0a725b70ddbfa
interfaces/power_supply_DC.yaml,7cc64002367143c4898589610739acd80063962e97a1456f66897b0856f916b3
interfaces/powermeter.yaml,e976a19789e0e9dee51a4682821399b8e5e546e0f8770571044e8757bd5f6eb9
interfaces/powermeter.yaml,0a563ca6f885a13df69a570d7c02d39805c8eb4b6010fe93b1fb287d80c3b510
interfaces/session_cost.yaml,4afd6dd67938dbc50e3d92751b27313cdcc083fb016998236d32f329df0ac806
interfaces/slac.yaml,973bb6d035e7ada95a0a589e0c1453000d37f637cecac53af5c12afe73be05e7
interfaces/system.yaml,4a5eb3f88d7934c3b7d0945aed369e4a6d95028a2c97d4e3090a0f73c1e0dcaf
Expand Down
2 changes: 1 addition & 1 deletion modules/EVSE/EvseManager/ErrorHandling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ static const struct IgnoreErrors {
ErrorList ac_rcd{"ac_rcd/VendorWarning"};
ErrorList imd{"isolation_monitor/VendorWarning"};
ErrorList powersupply{"power_supply_DC/VendorWarning"};
ErrorList powermeter{};
ErrorList powermeter{"powermeter/VendorWarning"};
ErrorList over_voltage_monitor{"over_voltage_monitor/VendorWarning"};
} ignore_errors;

Expand Down
5 changes: 5 additions & 0 deletions modules/EVSE/EvseManager/docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ powermeter
Powermeter errors cause the EvseManager to become Inoperative, if fail_on_powermeter_errors is configured to true. If it is configured to false, errors from the powermeter will not cause the EvseManager to become Inoperative.

* powermeter/CommunicationFault
* powermeter/VendorError

Note that ``powermeter/VendorWarning`` is explicitly ignored by the EvseManager's inoperative logic (similar to other ``VendorWarning`` errors)
and will not block charging even if ``fail_on_powermeter_errors`` is set to true. It should be used to signal non-fatal conditions such as
high temperature warnings from the powermeter.

When a charging session is stopped because of an error, the EvseManager differentiates between **Emergency Shutdowns** and **Error Shutdowns**. The severity of the
error influences the type of the shudown. Emergency shutdowns are caused by errors with `Severity::High` and error shutdowns are caused by errors with `Severity::Medium` or `Severity::Low`.
Expand Down
123 changes: 103 additions & 20 deletions modules/HardwareDrivers/Payment/RsPaymentTerminal/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ use generated::{
BankSessionTokenProviderClientSubscriber, Context, Module, ModulePublisher, OnReadySubscriber,
PaymentTerminalServiceSubscriber, SessionCostClientSubscriber,
};
use std::cmp::min;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::{mpsc::channel, mpsc::Sender, Arc, Mutex};
Expand All @@ -58,6 +57,94 @@ use zvt_feig_terminal::feig::{CardInfo, Error};

const INVALID_BANK_TOKEN: &str = "PAYMENT_TERMINAL_INVALID";

mod backoff {
use std::cmp::min;
use std::time::{Duration, Instant};

pub struct Backoff {
next_retry: Instant,
backoff_secs: u64,
max_backoff_secs: u64,
}

impl Backoff {
pub fn from_secs(max_backoff_secs: u64) -> Self {
Self {
next_retry: Instant::now(),
backoff_secs: 1,
max_backoff_secs,
}
}

pub fn is_ready(&self) -> bool {
Instant::now() >= self.next_retry
}

pub fn record_failure(&mut self) {
self.backoff_secs = min(self.backoff_secs * 2, self.max_backoff_secs);
log::info!(
"Recorded failure: next retry will be in {}",
self.backoff_secs
);
self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
}

pub fn record_success(&mut self) {
self.backoff_secs = 1;
log::debug!("Next retry will be in {}", self.backoff_secs);
self.next_retry = Instant::now() + Duration::from_secs(self.backoff_secs);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ready_immediately_after_creation() {
let b = Backoff::from_secs(60);
assert!(b.is_ready());
}

#[test]
fn not_ready_after_failure() {
let mut b = Backoff::from_secs(60);
b.record_failure();
assert!(!b.is_ready());
assert_eq!(b.backoff_secs, 2);
}

#[test]
fn exponential_increase() {
let mut b = Backoff::from_secs(60);
b.record_failure();
assert_eq!(b.backoff_secs, 2);
b.record_failure();
assert_eq!(b.backoff_secs, 4);
b.record_failure();
assert_eq!(b.backoff_secs, 8);
}

#[test]
fn caps_at_max() {
let mut b = Backoff::from_secs(8);
for _ in 0..10 {
b.record_failure();
}
assert_eq!(b.backoff_secs, 8);
}

#[test]
fn success_resets() {
let mut b = Backoff::from_secs(60);
b.record_failure();
b.record_failure();
b.record_success();
assert_eq!(b.backoff_secs, 1);
}
}
}

mod sync_feig {
use anyhow::Result;
use std::sync::Mutex;
Expand Down Expand Up @@ -271,17 +358,20 @@ impl PaymentTerminalModule {

// Wait for the card.
let mut read_card_loop = || -> CardInfo {
let mut timeout = std::time::Instant::now();
let mut backoff_seconds = 1;

// Long backoff to the payment provider backend to not overload it.
let mut configure_backoff = backoff::Backoff::from_secs(3600);
let mut bank_token_backoff = backoff::Backoff::from_secs(60);
loop {
if let Err(inner) = self.feig.configure() {
log::warn!("Failed to configure: {inner:?}");
let inner: PTError = inner.into();
publishers.payment_terminal.raise_error(inner.into());
continue;
} else {
publishers.payment_terminal.clear_all_errors();
if configure_backoff.is_ready() {
if let Err(inner) = self.feig.configure() {
log::warn!("Failed to configure: {inner:?}");
let inner: PTError = inner.into();
publishers.payment_terminal.raise_error(inner.into());
configure_backoff.record_failure();
} else {
configure_backoff.record_success();
publishers.payment_terminal.clear_all_errors();
}
}

let bank_cards_enabled = {
Expand All @@ -296,18 +386,11 @@ impl PaymentTerminalModule {
// Attempting to get an invoice token
if token.is_none() && bank_cards_enabled {
if let Some(publisher) = publishers.bank_session_token_slots.get(0) {
if timeout.elapsed() > Duration::from_secs(0) {
if bank_token_backoff.is_ready() {
token = publisher.get_bank_session_token().map_or(None, |v| v.token);

// Poor man's backoff to avoid a busy loop
const MAX_BACKOFF_SECONDS: u64 = 60;
if token.is_none() {
backoff_seconds = min(backoff_seconds * 2, MAX_BACKOFF_SECONDS);
timeout = std::time::Instant::now()
+ Duration::from_secs(backoff_seconds);
log::info!(
"Failed to receive invoice token, retrying in {backoff_seconds} seconds"
);
bank_token_backoff.record_failure();
} else {
log::info!("Received the invoice token {token:?}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ struct Conf {
int SC;
std::string UV;
std::string UD;
double temperature_warning_level_C;
double temperature_error_level_C;
double temperature_hysteresis_K;
int temperature_min_time_as_valid_ms;
int command_timeout_ms;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "http_client.hpp"
#include "lem_dcbm_time_sync_helper.hpp"
#include <chrono>
#include <everest/logging.hpp>
#include <fmt/core.h>
#include <string>
#include <thread>

Expand All @@ -27,6 +29,21 @@ void powermeterImpl::init() {
mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay,
mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst,
mod->config.SC, mod->config.UV, mod->config.UD, mod->config.command_timeout_ms});

// Validate and normalize temperature thresholds for the monitor.
// If the error level is configured below the warning level, clamp it and log a warning.
double warning_level_C = mod->config.temperature_warning_level_C;
double error_level_C = mod->config.temperature_error_level_C;
if (error_level_C < warning_level_C) {
EVLOG_warning << "LEM DCBM 400/600: temperature_error_level_C (" << error_level_C
<< " °C) is below temperature_warning_level_C (" << warning_level_C
<< " °C). Clamping error level to the warning level.";
error_level_C = warning_level_C;
}

this->temperature_monitor = std::make_unique<TemperatureMonitor>(
TemperatureMonitor::Config{warning_level_C, error_level_C, mod->config.temperature_hysteresis_K,
std::chrono::milliseconds(mod->config.temperature_min_time_as_valid_ms)});
}

void powermeterImpl::ready() {
Expand All @@ -41,14 +58,23 @@ void powermeterImpl::ready() {
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
this->publish_powermeter(this->controller->get_powermeter());
auto powermeter_data = this->controller->get_powermeter();
this->publish_powermeter(powermeter_data);
// if the communication error is set, clear the error
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
"Communication timed out")) {
// need to update LEM status since we have recovered from a communication loss
this->controller->update_lem_status();
clear_error("powermeter/CommunicationFault", "Communication timed out");
}

// Evaluate temperature thresholds
if (powermeter_data.temperatures.has_value() && powermeter_data.temperatures->size() >= 2) {
const double temp_H = powermeter_data.temperatures->at(0).temperature;
const double temp_L = powermeter_data.temperatures->at(1).temperature;
auto events = this->temperature_monitor->update(temp_H, temp_L);
handle_temperature_events(events, this->temperature_monitor->last_max_temperature());
}
}
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& dcbm_exception) {
EVLOG_error << "Failed to publish powermeter value due to an invalid device response: "
Expand Down Expand Up @@ -77,4 +103,39 @@ types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transacti
return this->controller->stop_transaction(transaction_id);
}

void powermeterImpl::handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature) {
if (events.warning_raised) {
EVLOG_warning << fmt::format(
"LEM DCBM 400/600: Temperature warning raised — max temperature {:.1f} °C exceeds warning level {:.1f} °C",
max_temperature, mod->config.temperature_warning_level_C);
auto error =
this->error_factory->create_error("powermeter/VendorWarning", "TemperatureWarning",
fmt::format("Max temperature {:.1f} °C exceeds warning level {:.1f} °C",
max_temperature, mod->config.temperature_warning_level_C));
raise_error(error);
}
if (events.warning_cleared) {
EVLOG_info << fmt::format(
"LEM DCBM 400/600: Temperature warning cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
max_temperature, mod->config.temperature_warning_level_C - mod->config.temperature_hysteresis_K);
clear_error("powermeter/VendorWarning", "TemperatureWarning");
}
if (events.error_raised) {
EVLOG_error << fmt::format(
"LEM DCBM 400/600: Temperature error raised — max temperature {:.1f} °C exceeds error level {:.1f} °C",
max_temperature, mod->config.temperature_error_level_C);
auto error =
this->error_factory->create_error("powermeter/VendorError", "TemperatureError",
fmt::format("Max temperature {:.1f} °C exceeds error level {:.1f} °C",
max_temperature, mod->config.temperature_error_level_C));
raise_error(error);
}
if (events.error_cleared) {
EVLOG_info << fmt::format(
"LEM DCBM 400/600: Temperature error cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
max_temperature, mod->config.temperature_error_level_C - mod->config.temperature_hysteresis_K);
clear_error("powermeter/VendorError", "TemperatureError");
}
}

} // namespace module::main
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// insert your custom include headers here
#include "http_client_interface.hpp"
#include "lem_dcbm_400600_controller.hpp"
#include "temperature_monitor.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1

namespace module {
Expand Down Expand Up @@ -61,6 +62,10 @@ class powermeterImpl : public powermeterImplBase {
// Initially it's a default-constructed thread (which is valid, but doesn't represent an actual running thread)
// In ready(), the live_measure_publisher thread is started and placed in this field.
std::thread live_measure_publisher_thread;

// Temperature monitoring with warning/error thresholds and hysteresis
std::unique_ptr<TemperatureMonitor> temperature_monitor;
void handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature);
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};

Expand Down
Loading
Loading