Skip to content

Commit 42654e8

Browse files
authored
Add temperature monitoring with warning/error thresholds to LEM DCBM 400/600 (EVerest#1936)
Summary: Implements configurable temperature monitoring for the LEM DCBM 400/600 powermeter module, monitoring temperatureH and temperatureL sensors against configurable warning and error thresholds with hysteresis and minimum exceedance duration. Features: - Temperature monitoring with independent warning and error thresholds - Hysteresis-based clearing to prevent oscillation - Minimum time requirement before raising events (configurable) - Uses max(temperatureH, temperatureL) for evaluation - Raises powermeter/VendorWarning (non-blocking) and powermeter/VendorError (stops charging) via error framework Configuration Options (manifest.yaml): - temperature_warning_level_C: Warning threshold in °C (default: 5000 = disabled) - temperature_error_level_C: Error threshold in °C (default: 5000 = disabled) - temperature_hysteresis_K: Hysteresis in Kelvin for clearing (default: 3) - temperature_min_time_as_valid_ms: Minimum exceedance duration in ms (default: 5000) Error Framework Integration: - Added powermeter/VendorWarning to EvseManager ignore list (allows charging to continue) - Updated EvseManager documentation to reflect new error types Signed-off-by: Florin Mihut <florinmihut1@gmail.com>
1 parent f7b01b5 commit 42654e8

File tree

13 files changed

+454
-13
lines changed

13 files changed

+454
-13
lines changed

errors/powermeter.yaml

Lines changed: 0 additions & 6 deletions
This file was deleted.

interfaces/powermeter.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ vars:
3434
description: The public key for OCMF
3535
type: string
3636
errors:
37-
- reference: /errors/powermeter
37+
- reference: /errors/generic

lib/everest/everest_api_types/tests/expected_interfaces_file_hashes.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interfaces/ocpp.yaml,46a82fad56f8437d8567da9a1b7c4013ffa8eca9db187e034d02beda496
1616
interfaces/ocpp_data_transfer.yaml,dec8974ba68bf9ef20ba68616873f714bd0a3255c6ea28fefc6f5c524666aebe
1717
interfaces/over_voltage_monitor.yaml,e900756a2058fca6c24434ac153c34afdf109ad582ca33c5aad0a725b70ddbfa
1818
interfaces/power_supply_DC.yaml,7cc64002367143c4898589610739acd80063962e97a1456f66897b0856f916b3
19-
interfaces/powermeter.yaml,e976a19789e0e9dee51a4682821399b8e5e546e0f8770571044e8757bd5f6eb9
19+
interfaces/powermeter.yaml,0a563ca6f885a13df69a570d7c02d39805c8eb4b6010fe93b1fb287d80c3b510
2020
interfaces/session_cost.yaml,4afd6dd67938dbc50e3d92751b27313cdcc083fb016998236d32f329df0ac806
2121
interfaces/slac.yaml,973bb6d035e7ada95a0a589e0c1453000d37f637cecac53af5c12afe73be05e7
2222
interfaces/system.yaml,4a5eb3f88d7934c3b7d0945aed369e4a6d95028a2c97d4e3090a0f73c1e0dcaf

modules/EVSE/EvseManager/ErrorHandling.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ static const struct IgnoreErrors {
4242
ErrorList ac_rcd{"ac_rcd/VendorWarning"};
4343
ErrorList imd{"isolation_monitor/VendorWarning"};
4444
ErrorList powersupply{"power_supply_DC/VendorWarning"};
45-
ErrorList powermeter{};
45+
ErrorList powermeter{"powermeter/VendorWarning"};
4646
ErrorList over_voltage_monitor{"over_voltage_monitor/VendorWarning"};
4747
} ignore_errors;
4848

modules/EVSE/EvseManager/docs/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ powermeter
288288
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.
289289

290290
* powermeter/CommunicationFault
291+
* powermeter/VendorError
292+
293+
Note that ``powermeter/VendorWarning`` is explicitly ignored by the EvseManager's inoperative logic (similar to other ``VendorWarning`` errors)
294+
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
295+
high temperature warnings from the powermeter.
291296

292297
When a charging session is stopped because of an error, the EvseManager differentiates between **Emergency Shutdowns** and **Error Shutdowns**. The severity of the
293298
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`.

modules/HardwareDrivers/PowerMeters/LemDCBM400600/LemDCBM400600.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ struct Conf {
3939
int SC;
4040
std::string UV;
4141
std::string UD;
42+
double temperature_warning_level_C;
43+
double temperature_error_level_C;
44+
double temperature_hysteresis_K;
45+
int temperature_min_time_as_valid_ms;
4246
int command_timeout_ms;
4347
};
4448

modules/HardwareDrivers/PowerMeters/LemDCBM400600/main/powermeterImpl.cpp

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include "http_client.hpp"
66
#include "lem_dcbm_time_sync_helper.hpp"
77
#include <chrono>
8+
#include <everest/logging.hpp>
9+
#include <fmt/core.h>
810
#include <string>
911
#include <thread>
1012

@@ -27,6 +29,21 @@ void powermeterImpl::init() {
2729
mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay,
2830
mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst,
2931
mod->config.SC, mod->config.UV, mod->config.UD, mod->config.command_timeout_ms});
32+
33+
// Validate and normalize temperature thresholds for the monitor.
34+
// If the error level is configured below the warning level, clamp it and log a warning.
35+
double warning_level_C = mod->config.temperature_warning_level_C;
36+
double error_level_C = mod->config.temperature_error_level_C;
37+
if (error_level_C < warning_level_C) {
38+
EVLOG_warning << "LEM DCBM 400/600: temperature_error_level_C (" << error_level_C
39+
<< " °C) is below temperature_warning_level_C (" << warning_level_C
40+
<< " °C). Clamping error level to the warning level.";
41+
error_level_C = warning_level_C;
42+
}
43+
44+
this->temperature_monitor = std::make_unique<TemperatureMonitor>(
45+
TemperatureMonitor::Config{warning_level_C, error_level_C, mod->config.temperature_hysteresis_K,
46+
std::chrono::milliseconds(mod->config.temperature_min_time_as_valid_ms)});
3047
}
3148

3249
void powermeterImpl::ready() {
@@ -41,14 +58,23 @@ void powermeterImpl::ready() {
4158
std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay));
4259
} else {
4360
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
44-
this->publish_powermeter(this->controller->get_powermeter());
61+
auto powermeter_data = this->controller->get_powermeter();
62+
this->publish_powermeter(powermeter_data);
4563
// if the communication error is set, clear the error
4664
if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault",
4765
"Communication timed out")) {
4866
// need to update LEM status since we have recovered from a communication loss
4967
this->controller->update_lem_status();
5068
clear_error("powermeter/CommunicationFault", "Communication timed out");
5169
}
70+
71+
// Evaluate temperature thresholds
72+
if (powermeter_data.temperatures.has_value() && powermeter_data.temperatures->size() >= 2) {
73+
const double temp_H = powermeter_data.temperatures->at(0).temperature;
74+
const double temp_L = powermeter_data.temperatures->at(1).temperature;
75+
auto events = this->temperature_monitor->update(temp_H, temp_L);
76+
handle_temperature_events(events, this->temperature_monitor->last_max_temperature());
77+
}
5278
}
5379
} catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& dcbm_exception) {
5480
EVLOG_error << "Failed to publish powermeter value due to an invalid device response: "
@@ -77,4 +103,39 @@ types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transacti
77103
return this->controller->stop_transaction(transaction_id);
78104
}
79105

106+
void powermeterImpl::handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature) {
107+
if (events.warning_raised) {
108+
EVLOG_warning << fmt::format(
109+
"LEM DCBM 400/600: Temperature warning raised — max temperature {:.1f} °C exceeds warning level {:.1f} °C",
110+
max_temperature, mod->config.temperature_warning_level_C);
111+
auto error =
112+
this->error_factory->create_error("powermeter/VendorWarning", "TemperatureWarning",
113+
fmt::format("Max temperature {:.1f} °C exceeds warning level {:.1f} °C",
114+
max_temperature, mod->config.temperature_warning_level_C));
115+
raise_error(error);
116+
}
117+
if (events.warning_cleared) {
118+
EVLOG_info << fmt::format(
119+
"LEM DCBM 400/600: Temperature warning cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
120+
max_temperature, mod->config.temperature_warning_level_C - mod->config.temperature_hysteresis_K);
121+
clear_error("powermeter/VendorWarning", "TemperatureWarning");
122+
}
123+
if (events.error_raised) {
124+
EVLOG_error << fmt::format(
125+
"LEM DCBM 400/600: Temperature error raised — max temperature {:.1f} °C exceeds error level {:.1f} °C",
126+
max_temperature, mod->config.temperature_error_level_C);
127+
auto error =
128+
this->error_factory->create_error("powermeter/VendorError", "TemperatureError",
129+
fmt::format("Max temperature {:.1f} °C exceeds error level {:.1f} °C",
130+
max_temperature, mod->config.temperature_error_level_C));
131+
raise_error(error);
132+
}
133+
if (events.error_cleared) {
134+
EVLOG_info << fmt::format(
135+
"LEM DCBM 400/600: Temperature error cleared — max temperature {:.1f} °C dropped below {:.1f} °C",
136+
max_temperature, mod->config.temperature_error_level_C - mod->config.temperature_hysteresis_K);
137+
clear_error("powermeter/VendorError", "TemperatureError");
138+
}
139+
}
140+
80141
} // namespace module::main

modules/HardwareDrivers/PowerMeters/LemDCBM400600/main/powermeterImpl.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// insert your custom include headers here
1717
#include "http_client_interface.hpp"
1818
#include "lem_dcbm_400600_controller.hpp"
19+
#include "temperature_monitor.hpp"
1920
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
2021

2122
namespace module {
@@ -61,6 +62,10 @@ class powermeterImpl : public powermeterImplBase {
6162
// Initially it's a default-constructed thread (which is valid, but doesn't represent an actual running thread)
6263
// In ready(), the live_measure_publisher thread is started and placed in this field.
6364
std::thread live_measure_publisher_thread;
65+
66+
// Temperature monitoring with warning/error thresholds and hysteresis
67+
std::unique_ptr<TemperatureMonitor> temperature_monitor;
68+
void handle_temperature_events(const TemperatureMonitor::Events& events, double max_temperature);
6469
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
6570
};
6671

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Pionix GmbH and Contributors to EVerest
3+
4+
#ifndef LEM_DCBM_TEMPERATURE_MONITOR_HPP
5+
#define LEM_DCBM_TEMPERATURE_MONITOR_HPP
6+
7+
#include <algorithm>
8+
#include <chrono>
9+
#include <optional>
10+
11+
namespace module::main {
12+
13+
/// Monitors temperature readings against warning and error thresholds
14+
/// with hysteresis and a minimum exceedance duration before raising events.
15+
class TemperatureMonitor {
16+
public:
17+
struct Config {
18+
double warning_level_C;
19+
double error_level_C;
20+
double hysteresis_K;
21+
std::chrono::milliseconds min_time_as_valid;
22+
};
23+
24+
struct Events {
25+
bool warning_raised{false};
26+
bool warning_cleared{false};
27+
bool error_raised{false};
28+
bool error_cleared{false};
29+
};
30+
31+
explicit TemperatureMonitor(const Config& config) : config_(config) {
32+
}
33+
34+
/// Feed new temperature readings and get back any state-change events.
35+
/// The evaluation uses max(temperature_H, temperature_L).
36+
Events update(double temperature_H_C, double temperature_L_C) {
37+
last_max_temperature_ = std::max(temperature_H_C, temperature_L_C);
38+
const auto now = std::chrono::steady_clock::now();
39+
40+
Events events;
41+
evaluate_level(warning_active_, warning_exceeded_since_, config_.warning_level_C, now, events.warning_raised,
42+
events.warning_cleared);
43+
evaluate_level(error_active_, error_exceeded_since_, config_.error_level_C, now, events.error_raised,
44+
events.error_cleared);
45+
return events;
46+
}
47+
48+
/// Returns the current max temperature from the last update (for logging).
49+
[[nodiscard]] double last_max_temperature() const {
50+
return last_max_temperature_;
51+
}
52+
53+
private:
54+
Config config_;
55+
56+
bool warning_active_{false};
57+
std::optional<std::chrono::steady_clock::time_point> warning_exceeded_since_;
58+
59+
bool error_active_{false};
60+
std::optional<std::chrono::steady_clock::time_point> error_exceeded_since_;
61+
62+
double last_max_temperature_{0.0};
63+
64+
void evaluate_level(bool& active, std::optional<std::chrono::steady_clock::time_point>& exceeded_since,
65+
double threshold, std::chrono::steady_clock::time_point now, bool& raised_event,
66+
bool& cleared_event) {
67+
68+
if (!active) {
69+
// Not yet active — check if we should start or continue timing
70+
if (last_max_temperature_ >= threshold) {
71+
if (!exceeded_since.has_value()) {
72+
// First time exceeding: start the timer
73+
exceeded_since = now;
74+
}
75+
// Check if minimum exceedance duration has elapsed
76+
if ((now - exceeded_since.value()) >= config_.min_time_as_valid) {
77+
active = true;
78+
exceeded_since.reset();
79+
raised_event = true;
80+
}
81+
} else {
82+
// Temperature dropped below threshold before timer expired — reset
83+
exceeded_since.reset();
84+
}
85+
} else {
86+
// Active — check if we should clear (with hysteresis)
87+
if (last_max_temperature_ < (threshold - config_.hysteresis_K)) {
88+
active = false;
89+
exceeded_since.reset();
90+
cleared_event = true;
91+
}
92+
}
93+
}
94+
};
95+
96+
} // namespace module::main
97+
98+
#endif // LEM_DCBM_TEMPERATURE_MONITOR_HPP

modules/HardwareDrivers/PowerMeters/LemDCBM400600/manifest.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,35 @@ config:
7575
description: UD (OCMF/transaction fields)
7676
type: string
7777
default: ""
78+
temperature_warning_level_C:
79+
description: >-
80+
Temperature warning threshold in degrees Celsius. If the maximum of temperatureH
81+
and temperatureL exceeds this value for at least temperature_min_time_as_valid_ms,
82+
a VendorWarning is raised. Charging continues. Default of 5000 effectively disables this.
83+
type: number
84+
default: 5000
85+
temperature_error_level_C:
86+
description: >-
87+
Temperature error threshold in degrees Celsius. If the maximum of temperatureH
88+
and temperatureL exceeds this value for at least temperature_min_time_as_valid_ms,
89+
a VendorError is raised. Charging is stopped. Default of 5000 effectively disables this.
90+
type: number
91+
default: 5000
92+
temperature_hysteresis_K:
93+
description: >-
94+
Hysteresis in Kelvin for clearing temperature warnings and errors.
95+
A warning is cleared when max temperature drops below warning_level - hysteresis.
96+
An error is cleared when max temperature drops below error_level - hysteresis.
97+
type: number
98+
default: 3
99+
minimum: 0
100+
temperature_min_time_as_valid_ms:
101+
description: >-
102+
Minimum time in milliseconds that the temperature must continuously exceed
103+
the warning or error level before the corresponding event is raised.
104+
type: integer
105+
default: 5000
106+
minimum: 0
78107
command_timeout_ms:
79108
description: The timeout in milliseconds for a HTTP command to the LEM power meter.
80109
type: integer

0 commit comments

Comments
 (0)