Files
cariflex/tools/EVerest-main/modules/EVSE/Auth/lib/AuthHandler.cpp
Eric F d398a6ced2 Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
2026-06-08 00:38:27 -04:00

1100 lines
51 KiB
C++

// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <AuthHandler.hpp>
#include <everest/helpers/helpers.hpp>
#include <everest/logging.hpp>
#include <generated/interfaces/kvs/Interface.hpp>
using everest::helpers::is_equal_case_insensitive;
namespace module {
/// \brief helper method to intersect referenced_connectors (from ProvidedIdToken) with evses that are listed
/// within ValidationResult
std::vector<int> intersect(std::vector<int>& a, std::vector<int>& b) {
std::vector<int> result;
std::sort(a.begin(), a.end());
std::sort(b.begin(), b.end());
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(result));
return result;
}
namespace conversions {
std::string token_handling_result_to_string(const TokenHandlingResult& result) {
switch (result) {
case TokenHandlingResult::ALREADY_IN_PROCESS:
return "ALREADY_IN_PROCESS";
case TokenHandlingResult::NO_CONNECTOR_AVAILABLE:
return "NO_CONNECTOR_AVAILABLE";
case TokenHandlingResult::REJECTED:
return "REJECTED";
case TokenHandlingResult::TIMEOUT:
return "TIMEOUT";
case TokenHandlingResult::USED_TO_START_TRANSACTION:
return "USED_TO_START_TRANSACTION";
case TokenHandlingResult::USED_TO_STOP_TRANSACTION:
return "USED_TO_STOP_TRANSACTION";
case TokenHandlingResult::WITHDRAWN:
return "WITHDRAWN";
default:
throw std::runtime_error("No known conversion for the given token handling result");
}
}
} // namespace conversions
AuthHandler::AuthHandler(const SelectionAlgorithm& selection_algorithm, const int connection_timeout,
bool plug_in_timeout_enabled, bool prioritize_authorization_over_stopping_transaction,
bool ignore_faults, const std::string& id, kvsIntf* store) :
selection_algorithm(selection_algorithm),
connection_timeout(connection_timeout),
plug_in_timeout_enabled(plug_in_timeout_enabled),
prioritize_authorization_over_stopping_transaction(prioritize_authorization_over_stopping_transaction),
ignore_faults(ignore_faults),
reservation_handler(evses, id, store) {
}
AuthHandler::~AuthHandler() {
}
void AuthHandler::init_evse(const int evse_id, const int evse_index, const std::vector<Connector>& connectors) {
std::lock_guard<std::mutex> lock(this->event_mutex);
EVLOG_debug << "Add evse with evse id " << evse_id;
if (evse_id <= 0) {
EVLOG_error << "Can not initialize EVSE: evse id is <= 0.";
return;
}
this->evses[evse_id] = std::make_unique<EVSEContext>(evse_id, evse_index, connectors);
}
int32_t AuthHandler::get_evse_id_by_index(const int evse_index) {
std::lock_guard<std::mutex> lock(this->event_mutex);
for (const auto& [evse_id, evse_context] : this->evses) {
if (evse_context->evse_index == evse_index) {
return evse_id;
}
}
throw std::out_of_range("No EVSE found for evse_index: " + std::to_string(evse_index));
}
void AuthHandler::initialize() {
std::lock_guard<std::mutex> lock(this->event_mutex);
this->reservation_handler.load_reservations();
check_evse_reserved_and_send_updates();
}
TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) {
std::unique_lock<std::mutex> lk(this->event_mutex);
if (!this->publish_token_validation_status_callback) {
return TokenHandlingResult::REJECTED;
}
TokenHandlingResult result;
ProvidedIdToken provided_token_copy = provided_token;
// check if token is already currently processed
EVLOG_info << "Received new token: " << everest::helpers::redact(provided_token);
const auto referenced_evses = this->get_referenced_evses(provided_token);
if (!this->is_token_already_in_process(provided_token, referenced_evses)) {
// process token if not already in process
this->tokens_in_process.insert(provided_token);
this->publish_token_validation_status(provided_token, TokenValidationStatus::Processing);
result = this->handle_token(provided_token_copy, lk);
} else {
// do nothing if token is currently processed
EVLOG_info << "Received token " << everest::helpers::redact(provided_token.id_token.value)
<< " repeatedly while still processing it";
result = TokenHandlingResult::ALREADY_IN_PROCESS;
}
switch (result) {
case TokenHandlingResult::ALREADY_IN_PROCESS:
break;
case TokenHandlingResult::TIMEOUT: // Timeout means accepted but failed to pick contactor
this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::TimedOut);
break;
case TokenHandlingResult::NO_CONNECTOR_AVAILABLE:
case TokenHandlingResult::REJECTED:
this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::Rejected);
break;
case TokenHandlingResult::USED_TO_START_TRANSACTION:
this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::UsedToStart);
break;
case TokenHandlingResult::USED_TO_STOP_TRANSACTION:
this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::UsedToStop);
break;
case TokenHandlingResult::WITHDRAWN:
this->publish_token_validation_status(provided_token_copy, TokenValidationStatus::Withdrawn);
break;
}
if (result != TokenHandlingResult::ALREADY_IN_PROCESS) {
this->tokens_in_process.erase(provided_token);
}
EVLOG_info << "Result for token: " << everest::helpers::redact(provided_token.id_token.value) << ": "
<< conversions::token_handling_result_to_string(result);
this->processing_finished_cv.notify_all();
return result;
}
void AuthHandler::handle_token_validation_result_update(const ValidationResultUpdate& validation_result_update) {
std::unique_lock<std::mutex> lk(this->event_mutex);
auto connector_id = validation_result_update.connector_id;
if (this->evses.find(connector_id) != this->evses.end() and this->evses.at(connector_id)->identifier.has_value()) {
// "connector" is old OCPP speak and corrsponds to "EVSE":
EVLOG_info << "Updating validation result on evse#" << connector_id << ": "
<< validation_result_update.validation_result.authorization_status;
// Currently we only support updating the parent id token
this->evses.at(connector_id)->identifier->authorization_status =
validation_result_update.validation_result.authorization_status;
this->evses.at(connector_id)->identifier->parent_id_token =
validation_result_update.validation_result.parent_id_token;
types::authorization::ProvidedIdToken provided_token;
provided_token.id_token = this->evses.at(connector_id)->identifier->id_token;
provided_token.authorization_type = this->evses.at(connector_id)->identifier->type;
provided_token.parent_id_token = validation_result_update.validation_result.parent_id_token;
std::vector<int32_t> connectors_allowed{connector_id};
provided_token.connectors = connectors_allowed;
this->publish_token_validation_status(provided_token, types::authorization::TokenValidationStatus::Accepted,
validation_result_update.validation_result.tariff_messages);
} else {
EVLOG_error << "Unknown evse#" << connector_id
<< " or unknown authorization identifier on the evse for validation result update.";
}
}
TokenHandlingResult AuthHandler::handle_token(ProvidedIdToken& provided_token, std::unique_lock<std::mutex>& lk) {
std::vector<int> referenced_evses = this->get_referenced_evses(provided_token);
// Only provided token with type RFID can be used to stop a transaction
if (provided_token.authorization_type == AuthorizationType::RFID) {
// check if id_token is used for an active transaction
const auto evse_used_for_transaction =
this->used_for_transaction(referenced_evses, provided_token.id_token.value);
if (evse_used_for_transaction != -1) {
StopTransactionRequest req;
req.reason = StopTransactionReason::Local;
req.id_tag.emplace(provided_token);
this->stop_transaction_callback(this->evses.at(evse_used_for_transaction)->evse_index, req);
EVLOG_info << "Transaction was stopped because id_token was used for transaction for evse#"
<< evse_used_for_transaction;
if (this->evses.at(evse_used_for_transaction)->identifier->parent_id_token.has_value()) {
provided_token.parent_id_token =
this->evses.at(evse_used_for_transaction)->identifier->parent_id_token.value();
}
provided_token.connectors = std::vector<int32_t>{evse_used_for_transaction};
return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
}
}
/** Check if validation of token shall be requested. In some situations its not useful to validate
* the token because either no evse is available anyways or the provided token does not match a present
* reservation. Yet it has to be checked if the incoming token can be used to stop an active transaction or if the
* parent id of the token (that is only known after validation) can be used to stop or start transactions
*/
/* If no evse is available AND no parent_id is deposited at any evse and no master pass group id is
configured, we can immediately respond with NO_CONNECTOR_AVAILABLE */
if (!this->any_evse_available(referenced_evses) and !this->any_parent_id_present(referenced_evses) and
!this->master_pass_group_id.has_value()) {
return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
}
/* If all evses are reserved and the given identifier doesnt match any reserved identifier and no parent id is
* deposited for a reservation, we can immediately respond with NO_CONNECTOR_AVAILABLE */
bool all_evses_reserved_and_tag_does_not_match = true;
for (const auto evse_id : referenced_evses) {
if (evse_id < 0) {
EVLOG_warning << "Handle token: Evse id is negative: that should not be possible.";
continue;
}
const uint32_t evse_id_u = static_cast<uint32_t>(evse_id);
if (!this->reservation_handler.is_evse_reserved(evse_id_u) &&
this->reservation_handler.is_charging_possible(evse_id_u)) {
all_evses_reserved_and_tag_does_not_match = false;
break;
}
const std::optional<int32_t> reservation_id = this->reservation_handler.matches_reserved_identifier(
provided_token.id_token.value, evse_id_u, std::nullopt);
if (reservation_id.has_value()) {
all_evses_reserved_and_tag_does_not_match = false;
break;
}
if (this->reservation_handler.has_reservation_parent_id(evse_id_u)) {
all_evses_reserved_and_tag_does_not_match = false;
break;
}
}
if (all_evses_reserved_and_tag_does_not_match) {
return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
}
// Validate the provided token using the available validators
std::vector<ValidationResult> validation_results;
// only validate if token is not prevalidated
if (provided_token.prevalidated && provided_token.prevalidated.value()) {
ValidationResult validation_result;
validation_result.authorization_status = AuthorizationStatus::Accepted;
validation_result.parent_id_token = provided_token.parent_id_token;
validation_results.push_back(validation_result);
} else {
// release event_mutex before potentially blocking callback(s) via MQTT
// validate_token_callback does not touch any shared state
lk.unlock();
validation_results = this->validate_token_callback(provided_token);
lk.lock();
}
bool attempt_stop_with_parent_id_token = false;
if (this->prioritize_authorization_over_stopping_transaction) {
// check if any evse is available
if (!this->any_evse_available(referenced_evses)) {
// check if parent_id_token can be used to finish transaction
attempt_stop_with_parent_id_token = true;
}
} else {
attempt_stop_with_parent_id_token = true;
}
if (attempt_stop_with_parent_id_token) {
for (const auto& validation_result : validation_results) {
if (validation_result.authorization_status == AuthorizationStatus::Accepted &&
validation_result.parent_id_token.has_value()) {
// check if parent_id_token is equal to master_pass_group_id
if (this->equals_master_pass_group_id(validation_result.parent_id_token)) {
EVLOG_info << "Provided parent_id_token is equal to master_pass_group_id. Stopping all active "
"transactions!";
std::vector<int32_t> connectors;
for (const auto evse_id : referenced_evses) {
if (this->evses[evse_id]->transaction_active) {
StopTransactionRequest req;
req.reason = StopTransactionReason::MasterPass;
req.id_tag.emplace(provided_token);
this->stop_transaction_callback(this->evses.at(evse_id)->evse_index, req);
connectors.emplace_back(evse_id);
}
}
// TOOD: Add handling in case there is a display which can be used which transaction should stop
// (see C16 of OCPP2.0.1 spec)
provided_token.parent_id_token = validation_result.parent_id_token.value();
provided_token.connectors = connectors;
return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
}
const auto evse_used_for_transaction =
this->used_for_transaction(referenced_evses, validation_result.parent_id_token.value().value);
if (evse_used_for_transaction != -1) {
provided_token.connectors = std::vector<int32_t>{evse_used_for_transaction};
if (!this->evses[evse_used_for_transaction]->transaction_active) {
return TokenHandlingResult::ALREADY_IN_PROCESS;
} else {
StopTransactionRequest req;
req.reason = StopTransactionReason::Local;
req.id_tag.emplace(provided_token);
this->stop_transaction_callback(this->evses.at(evse_used_for_transaction)->evse_index, req);
EVLOG_info << "Transaction was stopped because parent_id_token was used for transaction";
provided_token.parent_id_token = validation_result.parent_id_token.value();
return TokenHandlingResult::USED_TO_STOP_TRANSACTION;
}
}
}
}
}
// check if any evse is available
if (!this->any_evse_available(referenced_evses)) {
return TokenHandlingResult::NO_CONNECTOR_AVAILABLE;
}
// We can remove evse_ids from referenced_evses that already have an identifier assigend, since we don't want to
// consider those when selecting an evse
referenced_evses.erase(
std::remove_if(referenced_evses.begin(), referenced_evses.end(),
[this](int32_t evse_id) { return this->evses.at(evse_id)->identifier != std::nullopt; }),
referenced_evses.end());
types::authorization::ValidationResult validation_result = {types::authorization::AuthorizationStatus::Unknown};
if (!validation_results.empty()) {
bool authorized = false;
std::vector<ValidationResult>::size_type i = 0;
// iterate over validation results
while (i < validation_results.size() && !authorized && !referenced_evses.empty()) {
validation_result = validation_results.at(i);
if (validation_result.authorization_status == AuthorizationStatus::Accepted) {
if (this->equals_master_pass_group_id(validation_result.parent_id_token)) {
EVLOG_info << "parent_id_token of validation result is equal to master_pass_group_id. Not allowed "
"to authorize this token for starting transactions!";
return TokenHandlingResult::REJECTED;
}
if (validation_result.parent_id_token.has_value()) {
provided_token.parent_id_token = validation_result.parent_id_token.value();
}
this->publish_token_validation_status(provided_token,
types::authorization::TokenValidationStatus::Accepted,
validation_result.tariff_messages);
/* although validator accepts the authorization request, the Auth module still needs to
- select the evse for the authorization request
- process it against placed reservations
- compare referenced_evses against the evses listed in the validation_result
- check if request has been withdrawn while selecting an evse
*/
const auto select_evse_result =
this->select_evse(referenced_evses, provided_token.id_token, lk); // might block
if (not select_evse_result.evse_id.has_value()) {
if (select_evse_result.status == SelectEvseReturnStatus::TimeOut) {
return TokenHandlingResult::TIMEOUT;
} else if (select_evse_result.status == SelectEvseReturnStatus::Interrupted) {
return TokenHandlingResult::WITHDRAWN;
}
}
int evse_id = select_evse_result.evse_id.value();
EVLOG_debug << "Selected evse#" << evse_id
<< " for token: " << everest::helpers::redact(provided_token.id_token.value);
std::optional<std::string> parent_id_token;
if (validation_result.parent_id_token.has_value()) {
parent_id_token = validation_result.parent_id_token.value().value;
}
const std::optional<int32_t> reservation_id = this->reservation_handler.matches_reserved_identifier(
provided_token.id_token.value, static_cast<uint32_t>(evse_id), parent_id_token);
if (validation_result.evse_ids.has_value() and
intersect(referenced_evses, validation_result.evse_ids.value()).empty()) {
EVLOG_debug << "Empty intersection between referenced evses and evses that are authorized";
validation_result.authorization_status = AuthorizationStatus::NotAtThisLocation;
} else if (reservation_id == std::nullopt &&
!this->reservation_handler.is_charging_possible(static_cast<uint32_t>(evse_id))) {
validation_result.authorization_status = AuthorizationStatus::NotAtThisTime;
} else if (!this->reservation_handler.is_evse_reserved(static_cast<uint32_t>(evse_id)) &&
(reservation_id == std::nullopt)) {
EVLOG_info << "Providing authorization to evse#" << evse_id;
authorized = true;
provided_token.connectors = std::vector<int32_t>{evse_id};
} else {
EVLOG_debug << "Evse is reserved. Checking if token matches...";
if (reservation_id.has_value()) {
EVLOG_info << "Evse#" << evse_id << " is reserved and token is valid for this reservation";
this->reservation_handler.on_reservation_used(reservation_id.value());
authorized = true;
provided_token.connectors = std::vector<int32_t>{evse_id};
validation_result.reservation_id = reservation_id.value();
} else {
EVLOG_info << "Evse#" << evse_id << " is reserved but token is not valid for this reservation";
validation_result.authorization_status = AuthorizationStatus::NotAtThisTime;
}
}
this->notify_evse(evse_id, provided_token, validation_result, lk);
}
i++;
}
if (authorized) {
return TokenHandlingResult::USED_TO_START_TRANSACTION;
} else {
EVLOG_debug << "id_token could not be validated by any validator";
// in case the validation was not successful, we need to notify the evse and transmit the validation result.
// This is especially required for Plug&Charge with ISO15118 in order to allow the ISO15118 state machine to
// escape the Authorize loop. We do this for all evses that were referenced
if (provided_token.connectors.has_value()) {
const auto connectors = provided_token.connectors.value();
std::for_each(connectors.begin(), connectors.end(),
[this, provided_token, validation_result, &lk](int32_t connector) {
this->notify_evse(connector, provided_token, validation_result, lk);
});
}
return TokenHandlingResult::REJECTED;
}
} else {
EVLOG_warning << "No validation result was received by any validator.";
return TokenHandlingResult::REJECTED;
}
}
std::vector<int> AuthHandler::get_referenced_evses(const ProvidedIdToken& provided_token) {
std::vector<int> evse_ids;
// either insert the given connector references of the provided token
if (provided_token.connectors) {
std::copy_if(provided_token.connectors.value().begin(), provided_token.connectors.value().end(),
std::back_inserter(evse_ids), [this](int evse_id) {
if (this->evses.find(evse_id) != this->evses.end()) {
return !this->evses.at(evse_id)->is_unavailable();
} else {
EVLOG_warning << "Provided token included references to evse_id that does not exist";
return false;
}
});
}
// or if there is no reference to connectors take all connectors
else {
for (const auto& entry : this->evses) {
if (!entry.second->is_unavailable()) {
evse_ids.push_back(entry.first);
}
}
}
return evse_ids;
}
int AuthHandler::used_for_transaction(const std::vector<int>& evse_ids, const std::string& token) {
for (const auto evse_id : evse_ids) {
if (this->evses.at(evse_id)->identifier.has_value()) {
const auto& identifier = this->evses.at(evse_id)->identifier.value();
// check against id_token
if (is_equal_case_insensitive(identifier.id_token.value, token)) {
return evse_id;
}
// check against parent_id_token
else if (identifier.parent_id_token.has_value() &&
is_equal_case_insensitive(identifier.parent_id_token.value().value, token)) {
return evse_id;
}
}
}
return -1;
}
bool AuthHandler::is_token_already_in_process(const ProvidedIdToken& provided_id_token,
const std::vector<int>& referenced_evses) {
// checks if the token is currently already processed by the module (because already swiped)
if (this->tokens_in_process.find(provided_id_token) != this->tokens_in_process.end()) {
return true;
} else {
// check if id_token was already used to authorize evse but no transaction has been started yet
for (const auto evse_id : referenced_evses) {
const auto& evse = this->evses.at(evse_id);
if (evse->identifier.has_value() &&
is_equal_case_insensitive(evse->identifier.value().id_token, provided_id_token.id_token) &&
!evse->transaction_active) {
return true;
}
}
}
return false;
}
bool AuthHandler::any_evse_available(const std::vector<int>& evse_ids) {
EVLOG_debug << "Checking availability of evses...";
for (const auto evse_id : evse_ids) {
if (this->evses.at(evse_id)->is_available()) {
EVLOG_debug << "There is at least one evse available";
return true;
}
}
EVLOG_debug << "No evse is available for this id_token";
return false;
}
bool AuthHandler::any_parent_id_present(const std::vector<int>& evse_ids) {
for (const auto evse_id : evse_ids) {
if (this->evses.at(evse_id)->identifier.has_value() and
this->evses.at(evse_id)->identifier.value().parent_id_token.has_value()) {
EVLOG_debug << "Parent id is currently present";
return true;
}
}
EVLOG_debug << "No parent id is currently present";
return false;
}
bool AuthHandler::equals_master_pass_group_id(const std::optional<types::authorization::IdToken> parent_id_token) {
if (!this->master_pass_group_id.has_value()) {
return false;
}
if (!parent_id_token.has_value()) {
return false;
}
return is_equal_case_insensitive(parent_id_token.value().value, this->master_pass_group_id.value());
}
int AuthHandler::get_latest_plugin(const std::vector<int>& evse_ids) {
for (const auto evse_id : this->plug_in_queue) {
if (std::find(evse_ids.begin(), evse_ids.end(), evse_id) != evse_ids.end()) {
return evse_id;
}
}
return -1;
}
AuthHandler::SelectEvseResult AuthHandler::select_evse(const std::vector<int>& selected_evses, const IdToken& id_token,
std::unique_lock<std::mutex>& lk) {
SelectEvseResult result;
if (selected_evses.size() == 1) {
result.status = SelectEvseReturnStatus::EvseSelected;
result.evse_id = selected_evses.at(0);
return result;
}
if (this->selection_algorithm == SelectionAlgorithm::PlugEvents) {
// locks all referenced evses for this request. Subsequent requests referencing one or more of the locked
// evses are blocked until handle_token returns
if (this->get_latest_plugin(selected_evses) == -1) {
// no EV has been plugged in yet at the referenced evses
EVLOG_debug << "No evse in authorization queue. Waiting for a plug in...";
// blocks until respective plugin for evse occurred or until timeout
if (!this->cv.wait_for(lk, std::chrono::seconds(this->connection_timeout),
[this, selected_evses, id_token] {
return this->get_latest_plugin(selected_evses) != -1 ||
this->is_authorization_withdrawn(selected_evses, id_token);
})) {
result.status = SelectEvseReturnStatus::TimeOut;
return result;
}
EVLOG_debug << "Plug in at evse occured or authorization withdrawn";
}
if (this->is_authorization_withdrawn(selected_evses, id_token)) {
result.status = SelectEvseReturnStatus::Interrupted;
} else {
result.status = SelectEvseReturnStatus::EvseSelected;
result.evse_id = this->get_latest_plugin(selected_evses);
}
return result;
} else if (this->selection_algorithm == SelectionAlgorithm::FindFirst) {
EVLOG_debug << "SelectionAlgorithm FindFirst: Selecting first available evse without an active transaction";
const auto selected_evse_id = this->get_latest_plugin(selected_evses);
if (selected_evse_id != -1 and !this->evses.at(selected_evse_id)->transaction_active) {
// an EV has been plugged in yet at the referenced evses
result.status = SelectEvseReturnStatus::EvseSelected;
result.evse_id = this->get_latest_plugin(selected_evses);
return result;
} else {
// no EV has been plugged in yet at the referenced evses; choosing the first one where no
// transaction is active
for (const auto& evse_id : selected_evses) {
const auto& evse = this->evses.at(evse_id);
if (evse->is_available()) {
result.status = SelectEvseReturnStatus::EvseSelected;
result.evse_id = evse_id;
return result;
}
}
}
result.status = SelectEvseReturnStatus::TimeOut;
return result;
} else {
throw std::runtime_error("SelectionAlgorithm not implemented: " +
selection_algorithm_to_string(this->selection_algorithm));
}
}
/// Checks if given \p withdraw_request matches given \p id_token and/or one evse of given \p selected_evses
bool does_withdraw_request_match(const WithdrawAuthorizationRequest& withdraw_request,
const std::vector<int>& selected_evses, const IdToken& id_token) {
// Check if the withdraw request is specific to an EVSE
const bool has_evse_id = withdraw_request.evse_id.has_value();
const bool is_evse_in_selected =
has_evse_id and (std::find(selected_evses.begin(), selected_evses.end(), withdraw_request.evse_id.value()) !=
selected_evses.end());
// Check if the ID token matches or is absent
const bool id_token_matches = not withdraw_request.id_token.has_value() or
(is_equal_case_insensitive(withdraw_request.id_token.value(), id_token));
return id_token_matches and (not has_evse_id or is_evse_in_selected);
}
bool AuthHandler::is_authorization_withdrawn(const std::vector<int>& selected_evses, const IdToken& id_token) {
if (withdraw_request == nullptr) {
return false;
}
return does_withdraw_request_match(*this->withdraw_request, selected_evses, id_token);
}
void AuthHandler::notify_evse(int evse_id, const ProvidedIdToken& provided_token,
const ValidationResult& validation_result, std::unique_lock<std::mutex>& lk) {
const auto evse_index = this->evses.at(evse_id)->evse_index;
if (validation_result.authorization_status == AuthorizationStatus::Accepted) {
Identifier identifier{provided_token.id_token, provided_token.authorization_type,
validation_result.authorization_status, validation_result.expiry_time,
validation_result.parent_id_token};
this->evses.at(evse_id)->identifier.emplace(identifier);
// wait for potentially running timeout task to finish execution
this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
this->evses.at(evse_id)->timeout_timer.stop();
this->evses.at(evse_id)->timeout_timer.timeout(
[this, evse_index, &evse = this->evses.at(evse_id), provided_token]() {
evse->timeout_in_progress = true;
std::lock_guard<std::mutex> lk(this->event_mutex);
EVLOG_debug << "Authorization timeout for evse#" << evse_index;
evse->identifier.reset();
this->withdraw_authorization_callback(evse_index);
this->publish_token_validation_status(provided_token, TokenValidationStatus::TimedOut);
evse->timeout_in_progress = false;
this->cv.notify_all();
},
std::chrono::seconds(this->connection_timeout));
this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
}
this->notify_evse_callback(evse_index, provided_token, validation_result);
}
types::reservation::ReservationResult AuthHandler::handle_reservation(const Reservation& reservation) {
std::lock_guard<std::mutex> lk(this->event_mutex);
std::optional<uint32_t> evse;
if (reservation.evse_id.has_value()) {
if (reservation.evse_id.value() >= 0) {
evse = static_cast<uint32_t>(reservation.evse_id.value());
}
}
return reservation_handler.make_reservation(evse, reservation);
}
std::pair<bool, std::optional<int32_t>> AuthHandler::handle_cancel_reservation(const int32_t reservation_id) {
std::lock_guard<std::mutex> lk(this->event_mutex);
std::pair<bool, std::optional<uint32_t>> reservation_cancelled = this->reservation_handler.cancel_reservation(
reservation_id, false, types::reservation::ReservationEndReason::Cancelled);
if (reservation_cancelled.first) {
if (reservation_cancelled.second.has_value()) {
return {true, static_cast<int32_t>(reservation_cancelled.second.value())};
}
return {true, std::nullopt};
}
return {false, std::nullopt};
}
ReservationCheckStatus AuthHandler::handle_reservation_exists(std::string& id_token, const std::optional<int>& evse_id,
std::optional<std::string>& group_id_token) {
std::lock_guard<std::mutex> lk(this->event_mutex);
// Evse id has no value.
std::optional<int32_t> reservation_id =
this->reservation_handler.matches_reserved_identifier(id_token, evse_id, group_id_token);
if (!evse_id.has_value()) {
if (reservation_id.has_value()) {
return ReservationCheckStatus::ReservedForToken;
}
return ReservationCheckStatus::NotReserved;
}
// Evse id has a value.
if (!this->reservation_handler.is_evse_reserved(evse_id.has_value())) {
// There is an evse id, but the evse is not reserved.
return ReservationCheckStatus::NotReserved;
}
if (reservation_id.has_value()) {
// There is an evse id and the reservation is for the given token.
return ReservationCheckStatus::ReservedForToken;
}
// Evse is reserved. No reservation for the given id_token. But there is also the group id token, let's do some
// checks here.
if (!group_id_token.has_value()) {
if (reservation_handler.has_reservation_parent_id(evse_id)) {
// Group id token has no value, but the reservation for this evse has a parent token. It might be that
// this token will be checked later.
return ReservationCheckStatus::ReservedForOtherTokenAndHasParentToken;
}
// Group id token has no value and the reservation for this evse has no parent token.
return ReservationCheckStatus::ReservedForOtherToken;
}
// Group id token has a value but it is not valid for this reservation
return ReservationCheckStatus::ReservedForOtherToken;
}
bool AuthHandler::call_reserved(const int reservation_id, const std::optional<int>& evse_id) {
const bool reserved = this->reserved_callback(evse_id, reservation_id);
if (reserved) {
this->check_evse_reserved_and_send_updates();
}
return reserved;
}
void AuthHandler::call_reservation_cancelled(const int32_t reservation_id,
const types::reservation::ReservationEndReason reason,
const std::optional<int>& evse_id, const bool send_reservation_update) {
std::optional<int32_t> evse_index;
if (evse_id.has_value() && evse_id.value() > 0) {
EVLOG_info << "Cancel reservation for evse id " << evse_id.value();
}
this->reservation_cancelled_callback(evse_id, reservation_id, reason, send_reservation_update);
// If this was a global reservation or there are global reservations which made evse's go to reserved because of
// this reservation, they should be cancelled.
this->check_evse_reserved_and_send_updates();
}
void AuthHandler::handle_permanent_fault_raised(const int evse_id, const int32_t connector_id) {
std::lock_guard<std::mutex> lk(this->event_mutex);
if (not ignore_faults) {
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::FAULTED);
}
}
void AuthHandler::handle_permanent_fault_cleared(const int evse_id, const int32_t connector_id) {
std::lock_guard<std::mutex> lk(this->event_mutex);
if (not ignore_faults) {
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ERROR_CLEARED);
}
}
void AuthHandler::handle_session_event(const int evse_id, const SessionEvent& event) {
std::unique_lock<std::mutex> lk(this->event_mutex);
// When connector id is not specified, it is assumed to be '1'.
const int32_t connector_id = event.connector_id.value_or(1);
if (evse_id <= 0) {
EVLOG_error << "Handle session event: Evse id is <= 0: That should not be possible.";
return;
}
if (connector_id <= 0) {
EVLOG_error << "Handle session event: connector id is <= 0: That should not be possible.";
return;
}
if (this->evses.count(evse_id) == 0) {
EVLOG_warning << "Handle session event: no evse found with evse id " << evse_id;
return;
}
const auto event_type = event.event;
bool check_reservations = false;
switch (event_type) {
case SessionEventEnum::SessionStarted: {
// only set plug in timeout when SessionStart is caused by plug in
if (event.session_started.value().reason == StartSessionReason::EVConnected) {
this->plug_in_queue.push_back(evse_id);
this->cv.notify_all();
this->evses.at(evse_id)->plugged_in = true;
EVLOG_info << "Plug In event for evse#" << evse_id << ", starting auth";
// only set timeout if there are multiple evses managed by this auth handler
// or plug in timeout is explicitly enabled
if (this->evses.size() == 1 or !this->plug_in_timeout_enabled) {
break;
}
this->evses.at(evse_id)->timeout_timer.timeout(
[this, evse_id]() {
this->evses.at(evse_id)->timeout_in_progress = true;
std::lock_guard<std::mutex> lk(this->event_mutex);
EVLOG_info << "Plug In timeout for evse#" << evse_id << ". Replug required for this EVSE";
this->withdraw_authorization_callback(this->evses.at(evse_id)->evse_index);
this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
this->evses.at(evse_id)->plug_in_timeout = true;
this->evses.at(evse_id)->timeout_in_progress = false;
this->cv.notify_all();
},
std::chrono::seconds(this->connection_timeout));
}
} break;
case SessionEventEnum::TransactionStarted: {
this->evses.at(evse_id)->plugged_in = true;
this->evses.at(evse_id)->transaction_active = true;
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::TRANSACTION_STARTED);
// wait for potentially running timeout task to finish execution
this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
this->evses.at(evse_id)->timeout_timer.stop();
check_reservations = true;
break;
}
case SessionEventEnum::TransactionFinished:
this->evses.at(evse_id)->transaction_active = false;
this->evses.at(evse_id)->identifier.reset();
break;
case SessionEventEnum::SessionFinished: {
this->evses.at(evse_id)->plugged_in = false;
this->evses.at(evse_id)->plug_in_timeout = false;
this->evses.at(evse_id)->identifier.reset();
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::SESSION_FINISHED);
// wait for potentially running timeout task to finish execution
this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
this->evses.at(evse_id)->timeout_timer.stop();
this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; });
check_reservations = true;
break;
}
case SessionEventEnum::Disabled:
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::DISABLE);
check_reservations = true;
break;
case SessionEventEnum::Enabled:
this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ENABLE);
check_reservations = true;
break;
case SessionEventEnum::Deauthorized:
this->evses.at(evse_id)->identifier.reset();
// wait for potentially running timeout task to finish execution
this->cv.wait(lk, [&evse = this->evses.at(evse_id)]() { return !evse->timeout_in_progress.load(); });
this->evses.at(evse_id)->timeout_timer.stop();
break;
case SessionEventEnum::ReservationStart:
break;
case SessionEventEnum::ReservationEnd: {
if (reservation_handler.is_evse_reserved(evse_id)) {
reservation_handler.cancel_reservation(evse_id, true);
}
break;
}
/// explicitly fall through all the SessionEventEnum values we are not handling
case SessionEventEnum::Authorized:
[[fallthrough]];
case SessionEventEnum::AuthRequired:
[[fallthrough]];
case SessionEventEnum::PrepareCharging:
[[fallthrough]];
case SessionEventEnum::ChargingStarted:
[[fallthrough]];
case SessionEventEnum::ChargingPausedEV:
[[fallthrough]];
case SessionEventEnum::ChargingPausedEVSE:
[[fallthrough]];
case SessionEventEnum::StoppingCharging:
[[fallthrough]];
case SessionEventEnum::ChargingFinished:
[[fallthrough]];
case SessionEventEnum::PluginTimeout:
[[fallthrough]];
case SessionEventEnum::SwitchingPhases:
[[fallthrough]];
case SessionEventEnum::SessionResumed:
break;
}
// When reservation is started or ended, check if the number of reservations match the number of evses and
// send 'reserved' notifications to the evse manager accordingly if needed.
if (check_reservations) {
this->check_evse_reserved_and_send_updates();
}
}
void AuthHandler::set_connection_timeout(const int connection_timeout) {
std::lock_guard<std::mutex> lk(this->event_mutex);
this->connection_timeout = connection_timeout;
};
void AuthHandler::set_plug_in_timeout_enabled(bool enabled) {
std::lock_guard<std::mutex> lk(this->event_mutex);
this->plug_in_timeout_enabled = enabled;
}
void AuthHandler::set_master_pass_group_id(const std::string& master_pass_group_id) {
std::lock_guard<std::mutex> lk(this->event_mutex);
if (master_pass_group_id.empty()) {
this->master_pass_group_id = std::nullopt;
} else {
this->master_pass_group_id = master_pass_group_id;
}
}
void AuthHandler::set_prioritize_authorization_over_stopping_transaction(bool b) {
std::lock_guard<std::mutex> lk(this->event_mutex);
this->prioritize_authorization_over_stopping_transaction = b;
}
void AuthHandler::register_notify_evse_callback(
const std::function<void(const int evse_index, const ProvidedIdToken& provided_token,
const ValidationResult& validation_result)>& callback) {
this->notify_evse_callback = callback;
}
void AuthHandler::register_withdraw_authorization_callback(const std::function<void(const int evse_index)>& callback) {
this->withdraw_authorization_callback = callback;
}
void AuthHandler::register_validate_token_callback(
const std::function<std::vector<ValidationResult>(const ProvidedIdToken& provided_token)>& callback) {
this->validate_token_callback = callback;
}
void AuthHandler::register_stop_transaction_callback(
const std::function<void(const int evse_index, const StopTransactionRequest& request)>& callback) {
this->stop_transaction_callback = callback;
}
void AuthHandler::register_reserved_callback(
const std::function<bool(const std::optional<int>& evse_id, const int& reservation_id)>& callback) {
this->reserved_callback = callback;
}
void AuthHandler::register_reservation_cancelled_callback(
const std::function<void(const std::optional<int32_t>& evse_id, const int32_t reservation_id,
const ReservationEndReason reason, const bool send_reservation_update)>& callback) {
this->reservation_cancelled_callback = callback;
this->reservation_handler.register_reservation_cancelled_callback(
[this](const std::optional<int32_t>& evse_id, const int32_t reservation_id,
const types::reservation::ReservationEndReason reason, const bool send_reservation_update) {
if (evse_id.has_value() && evse_id.value() < 0) {
EVLOG_warning << "Reservation cancelled: evse id is negative (" << evse_id.value()
<< "), that should not be possible.";
return;
}
this->call_reservation_cancelled(reservation_id, reason, evse_id, send_reservation_update);
});
}
void AuthHandler::register_publish_token_validation_status_callback(
const std::function<void(const ProvidedIdToken&, TokenValidationStatus, const std::vector<MessageContent>&)>&
callback) {
this->publish_token_validation_status_callback = callback;
}
void AuthHandler::publish_token_validation_status(const ProvidedIdToken& token, TokenValidationStatus status,
const std::vector<MessageContent>& tariff_messages) {
this->publish_token_validation_status_callback(token, status, tariff_messages);
}
WithdrawAuthorizationResult AuthHandler::handle_withdraw_authorization(const WithdrawAuthorizationRequest& request) {
std::lock_guard<std::mutex> lg(this->withdraw_mutex);
EVLOG_info << "Witdrawing authorization"
<< (request.evse_id.has_value() ? " evse: " + std::to_string(request.evse_id.value()) : "")
<< (request.id_token.has_value() ? " id token: " + request.id_token.value().value : "");
if (request.evse_id.has_value() and this->evses.find(request.evse_id.value()) == this->evses.end()) {
return WithdrawAuthorizationResult::EvseNotFound;
}
auto result = WithdrawAuthorizationResult::AuthorizationNotFound; // default
// Wait for processing threads to finish executing
std::unique_lock lock(this->event_mutex);
const auto is_withdraw_request_targeting_token_in_process = [this](const WithdrawAuthorizationRequest& request) {
for (const auto& token_in_process : this->tokens_in_process) {
auto referenced_evses = this->get_referenced_evses(token_in_process);
if (does_withdraw_request_match(request, referenced_evses, token_in_process.id_token)) {
return true;
}
}
return false;
};
this->withdraw_request = std::make_unique<WithdrawAuthorizationRequest>(request);
if (is_withdraw_request_targeting_token_in_process(request)) {
// Notify processing threads that wait within select_evse
// This will interrupt the wait for a plug in in case the authorization is withdrawn by this request
this->cv.notify_all();
// Release the event_mutex lock and wait for threads to finish...
this->processing_finished_cv.wait(
lock, [&]() { return not is_withdraw_request_targeting_token_in_process(request); });
result = WithdrawAuthorizationResult::Accepted;
}
// It might still be the case that the withdraw request is also targeting already granted authorization so we
// continue checking for this
const auto withdraw_authorization_or_stop_transaction = [this](const EVSEContext& evse) {
if (evse.transaction_active) {
StopTransactionRequest req;
req.reason = StopTransactionReason::DeAuthorized;
this->stop_transaction_callback(evse.evse_index, req);
} else {
this->withdraw_authorization_callback(evse.evse_index);
}
};
if (request.evse_id.has_value() and request.id_token.has_value()) {
// evse_id and id_token is specified
// find if there is a granted authorization for id_token and evse_id
const auto evse_id = request.evse_id.value();
const auto id_token = request.id_token.value();
const auto& evse = this->evses.at(evse_id);
if (evse->identifier.has_value() &&
is_equal_case_insensitive(request.id_token.value(), evse->identifier.value().id_token)) {
withdraw_authorization_or_stop_transaction(*evse);
result = WithdrawAuthorizationResult::Accepted;
}
} else if (request.evse_id.has_value()) {
// only evse_id is specified
// find if there is a granted authorization for evse_id
const auto evse_id = request.evse_id.value();
const auto& evse = this->evses.at(evse_id);
if (evse->identifier.has_value()) {
withdraw_authorization_or_stop_transaction(*evse);
result = WithdrawAuthorizationResult::Accepted;
}
} else if (request.id_token.has_value()) {
// only id_token is specified
// find if there is a granted authorization for id_token
for (const auto& evse : this->evses) {
if (evse.second->identifier.has_value() &&
is_equal_case_insensitive(request.id_token.value(), evse.second->identifier.value().id_token)) {
withdraw_authorization_or_stop_transaction(*evse.second);
result = WithdrawAuthorizationResult::Accepted;
}
}
} else {
// neither evse_id nor id_token is specified, withdraw all authorizations that have been granted
for (const auto& evse : this->evses) {
if (evse.second->identifier.has_value()) {
withdraw_authorization_or_stop_transaction(*evse.second);
result = WithdrawAuthorizationResult::Accepted;
}
}
}
// result was either set in one of the if statements above or is still AuthorizationNotFound
return result;
}
void AuthHandler::submit_event_for_connector(const int32_t evse_id, const int32_t connector_id,
const ConnectorEvent connector_event) {
for (auto& connector : this->evses.at(evse_id)->connectors) {
if (connector.id == connector_id) {
connector.submit_event(connector_event);
this->reservation_handler.on_connector_state_changed(connector.get_state(), evse_id, connector_id);
break;
}
}
}
void AuthHandler::check_evse_reserved_and_send_updates() {
ReservationEvseStatus reservation_status =
this->reservation_handler.check_number_global_reservations_match_number_available_evses();
for (const auto& available_evse : reservation_status.available) {
EVLOG_debug << "Evse " << available_evse << " is now available";
this->reservation_cancelled_callback(
available_evse, -1, types::reservation::ReservationEndReason::GlobalReservationRequirementDropped, false);
}
for (const auto& reserved_evse : reservation_status.reserved) {
EVLOG_debug << "Evse " << reserved_evse << " is now reserved";
if (this->reserved_callback != nullptr) {
const bool reserved = this->reserved_callback(reserved_evse, -1);
if (!reserved) {
EVLOG_warning << "Could not reserve " << reserved_evse << " for non evse specific reservations";
}
}
}
}
} // namespace module